Inform ConnectivityService about always-on VPN lockdown.

Currently, when an always-on VPN is set in lockdown mode, Vpn
configures prohibit UID rules in netd directly and does not
inform ConnectivityService of the fact.

This means that ConnectivityService cannot send NetworkCallbacks
that tells apps that they are blocked or unblocked. It also means
that ConnectivityService has to take the mVpns lock and call into
Vpn to allow synchronous APIs such as getActiveNetwork to return
BLOCKED if the app is blocked.

Move all this to ConnectivityService:
- Add a setRequireVpnForUids API to ConnectivityManager, and have
  that pass the routing rules to netd.
- Update VpnTest to expect calls to ConnectivityManager instead
  of to netd.
- Whenever setRequireVpnForUids is called, ensure that
  ConnectivityService sends onBlockedStatusChanged to the
  affected callbacks.
- Update existing unit tests to check for callbacks.
- Add a way to find the VPN that applies to a given UID without
  taking the VPN lock, by instead scanning all connected VPNs.
  Use this as a replacement for direct access to mVpns.

For simplicity, and in order to ensure proper ordering between
the NetworkCallbacks sent for VPNs connecting and disconnecting,
process blocked UID ranges on the handler thread. This means that
when setRequireVpnForUids returns, the rule changes might not
have been applied. This shouldn't impact apps using network
connectivity, but it might mean that apps setting an always-on
package, and then immediately checking whether networking is
blocked, will see a behaviour change.

Bug: 173331190
Fix: 175670887
Test: new test coverage in ConnectivityServiceTest
Test: atest MixedDeviceOwnerTest#testAlwaysOnVpn \
            MixedDeviceOwnerTest#testAlwaysOnVpnLockDown \
	    MixedDeviceOwnerTest#testAlwaysOnVpnAcrossReboot \
	    MixedDeviceOwnerTest#testAlwaysOnVpnPackageUninstalled \
	    MixedDeviceOwnerTest#testAlwaysOnVpnUnsupportedPackage \
	    MixedDeviceOwnerTest#testAlwaysOnVpnUnsupportedPackageReplaced \
	    MixedDeviceOwnerTest#testAlwaysOnVpnPackageLogged \
            MixedProfileOwnerTest#testAlwaysOnVpn \
            MixedProfileOwnerTest#testAlwaysOnVpnLockDown \
	    MixedProfileOwnerTest#testAlwaysOnVpnAcrossReboot \
	    MixedProfileOwnerTest#testAlwaysOnVpnPackageUninstalled \
	    MixedProfileOwnerTest#testAlwaysOnVpnUnsupportedPackage \
	    MixedProfileOwnerTest#testAlwaysOnVpnUnsupportedPackageReplaced \
	    MixedProfileOwnerTest#testAlwaysOnVpnPackageLogged \
            MixedManagedProfileOwnerTest#testAlwaysOnVpn \
            MixedManagedProfileOwnerTest#testAlwaysOnVpnLockDown \
	    MixedManagedProfileOwnerTest#testAlwaysOnVpnAcrossReboot \
	    MixedManagedProfileOwnerTest#testAlwaysOnVpnPackageUninstalled \
	    MixedManagedProfileOwnerTest#testAlwaysOnVpnUnsupportedPackage \
	    MixedManagedProfileOwnerTest#testAlwaysOnVpnUnsupportedPackageReplaced \
	    MixedManagedProfileOwnerTest#testAlwaysOnVpnPackageLogged
Test: atest FrameworksNetTests HostsideVpnTests \
            CtsNetTestCases:VpnServiceTest \
	    CtsNetTestCases:Ikev2VpnTest
Change-Id: Iaca8a7cc343aef52706cff62a7735f338cb1b772
This commit is contained in:
Lorenzo Colitti
2020-12-12 00:51:11 +09:00
parent 012452aab4
commit 3f54f106dd
6 changed files with 248 additions and 87 deletions

View File

@@ -299,6 +299,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import kotlin.reflect.KClass;
@@ -412,6 +413,7 @@ public class ConnectivityServiceTest {
@Spy private Resources mResources;
private final LinkedBlockingQueue<Intent> mStartedActivities = new LinkedBlockingQueue<>();
// Map of permission name -> PermissionManager.Permission_{GRANTED|DENIED} constant
private final HashMap<String, Integer> mMockedPermissions = new HashMap<>();
@@ -6509,6 +6511,26 @@ public class ConnectivityServiceTest {
checkNetworkInfo(mCm.getNetworkInfo(type), type, state);
}
// Checks that each of the |agents| receive a blocked status change callback with the specified
// |blocked| value, in any order. This is needed because when an event affects multiple
// networks, ConnectivityService does not guarantee the order in which callbacks are fired.
private void assertBlockedCallbackInAnyOrder(TestNetworkCallback callback, boolean blocked,
TestNetworkAgentWrapper... agents) {
final List<Network> expectedNetworks = Arrays.asList(agents).stream()
.map((agent) -> agent.getNetwork())
.collect(Collectors.toList());
// Expect exactly one blocked callback for each agent.
for (int i = 0; i < agents.length; i++) {
CallbackEntry e = callback.expectCallbackThat(TIMEOUT_MS, (c) ->
c instanceof CallbackEntry.BlockedStatus
&& ((CallbackEntry.BlockedStatus) c).getBlocked() == blocked);
Network network = e.getNetwork();
assertTrue("Received unexpected blocked callback for network " + network,
expectedNetworks.remove(network));
}
}
@Test
public void testNetworkBlockedStatusAlwaysOnVpn() throws Exception {
mServiceContext.setPermission(
@@ -6555,9 +6577,10 @@ public class ConnectivityServiceTest {
assertNetworkInfo(TYPE_WIFI, DetailedState.BLOCKED);
// Disable lockdown, expect to see the network unblocked.
// There are no callbacks because they are not implemented yet.
mService.setAlwaysOnVpnPackage(userId, null, false /* lockdown */, allowList);
expectNetworkRejectNonSecureVpn(inOrder, false, firstHalf, secondHalf);
callback.expectBlockedStatusCallback(false, mWiFiNetworkAgent);
defaultCallback.expectBlockedStatusCallback(false, mWiFiNetworkAgent);
vpnUidCallback.assertNoCallback();
assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
@@ -6605,6 +6628,8 @@ public class ConnectivityServiceTest {
allowList.clear();
mService.setAlwaysOnVpnPackage(userId, ALWAYS_ON_PACKAGE, true /* lockdown */, allowList);
expectNetworkRejectNonSecureVpn(inOrder, true, firstHalf, secondHalf);
defaultCallback.expectBlockedStatusCallback(true, mWiFiNetworkAgent);
assertBlockedCallbackInAnyOrder(callback, true, mWiFiNetworkAgent, mCellNetworkAgent);
vpnUidCallback.assertNoCallback();
assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
assertNull(mCm.getActiveNetwork());
@@ -6614,6 +6639,8 @@ public class ConnectivityServiceTest {
// Disable lockdown. Everything is unblocked.
mService.setAlwaysOnVpnPackage(userId, null, false /* lockdown */, allowList);
defaultCallback.expectBlockedStatusCallback(false, mWiFiNetworkAgent);
assertBlockedCallbackInAnyOrder(callback, false, mWiFiNetworkAgent, mCellNetworkAgent);
vpnUidCallback.assertNoCallback();
assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
@@ -6647,6 +6674,8 @@ public class ConnectivityServiceTest {
// Enable lockdown and connect a VPN. The VPN is not blocked.
mService.setAlwaysOnVpnPackage(userId, ALWAYS_ON_PACKAGE, true /* lockdown */, allowList);
defaultCallback.expectBlockedStatusCallback(true, mWiFiNetworkAgent);
assertBlockedCallbackInAnyOrder(callback, true, mWiFiNetworkAgent, mCellNetworkAgent);
vpnUidCallback.assertNoCallback();
assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
assertNull(mCm.getActiveNetwork());
@@ -6658,7 +6687,7 @@ public class ConnectivityServiceTest {
defaultCallback.expectAvailableThenValidatedCallbacks(mMockVpn);
vpnUidCallback.assertNoCallback(); // vpnUidCallback has NOT_VPN capability.
assertEquals(mMockVpn.getNetwork(), mCm.getActiveNetwork());
assertEquals(null, mCm.getActiveNetworkForUid(VPN_UID)); // BUG?
assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetworkForUid(VPN_UID));
assertActiveNetworkInfo(TYPE_WIFI, DetailedState.CONNECTED);
assertNetworkInfo(TYPE_MOBILE, DetailedState.DISCONNECTED);
assertNetworkInfo(TYPE_VPN, DetailedState.CONNECTED);

View File

@@ -27,7 +27,6 @@ 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;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -89,6 +88,7 @@ import android.security.Credentials;
import android.security.KeyStore;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Range;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -350,7 +350,7 @@ public class VpnTest {
// Set always-on with lockdown.
assertTrue(vpn.setAlwaysOnPackage(PKGS[1], true, null, mKeyStore));
verify(mNetd).networkRejectNonSecureVpn(eq(true), aryEq(new UidRangeParcel[] {
verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
new UidRangeParcel(user.start, user.start + PKG_UIDS[1] - 1),
new UidRangeParcel(user.start + PKG_UIDS[1] + 1, user.stop)
}));
@@ -361,12 +361,11 @@ public class VpnTest {
// Switch to another app.
assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true, null, mKeyStore));
verify(mNetd).networkRejectNonSecureVpn(eq(false), aryEq(new UidRangeParcel[] {
verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
new UidRangeParcel(user.start, user.start + PKG_UIDS[1] - 1),
new UidRangeParcel(user.start + PKG_UIDS[1] + 1, user.stop)
}));
verify(mNetd).networkRejectNonSecureVpn(eq(true), aryEq(new UidRangeParcel[] {
verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
new UidRangeParcel(user.start, user.start + PKG_UIDS[3] - 1),
new UidRangeParcel(user.start + PKG_UIDS[3] + 1, user.stop)
}));
@@ -383,7 +382,7 @@ public class VpnTest {
// Set always-on with lockdown and allow app PKGS[2] from lockdown.
assertTrue(vpn.setAlwaysOnPackage(
PKGS[1], true, Collections.singletonList(PKGS[2]), mKeyStore));
verify(mNetd).networkRejectNonSecureVpn(eq(true), aryEq(new UidRangeParcel[] {
verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
new UidRangeParcel(user.start, user.start + PKG_UIDS[1] - 1),
new UidRangeParcel(user.start + PKG_UIDS[2] + 1, user.stop)
}));
@@ -392,10 +391,10 @@ public class VpnTest {
// Change allowed app list to PKGS[3].
assertTrue(vpn.setAlwaysOnPackage(
PKGS[1], true, Collections.singletonList(PKGS[3]), mKeyStore));
verify(mNetd).networkRejectNonSecureVpn(eq(false), aryEq(new UidRangeParcel[] {
verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
new UidRangeParcel(user.start + PKG_UIDS[2] + 1, user.stop)
}));
verify(mNetd).networkRejectNonSecureVpn(eq(true), aryEq(new UidRangeParcel[] {
verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
new UidRangeParcel(user.start + PKG_UIDS[1] + 1, user.start + PKG_UIDS[3] - 1),
new UidRangeParcel(user.start + PKG_UIDS[3] + 1, user.stop)
}));
@@ -405,11 +404,11 @@ public class VpnTest {
// Change the VPN app.
assertTrue(vpn.setAlwaysOnPackage(
PKGS[0], true, Collections.singletonList(PKGS[3]), mKeyStore));
verify(mNetd).networkRejectNonSecureVpn(eq(false), aryEq(new UidRangeParcel[] {
verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
new UidRangeParcel(user.start, user.start + PKG_UIDS[1] - 1),
new UidRangeParcel(user.start + PKG_UIDS[1] + 1, user.start + PKG_UIDS[3] - 1)
}));
verify(mNetd).networkRejectNonSecureVpn(eq(true), aryEq(new UidRangeParcel[] {
verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
new UidRangeParcel(user.start, user.start + PKG_UIDS[0] - 1),
new UidRangeParcel(user.start + PKG_UIDS[0] + 1, user.start + PKG_UIDS[3] - 1)
}));
@@ -418,11 +417,11 @@ public class VpnTest {
// Remove the list of allowed packages.
assertTrue(vpn.setAlwaysOnPackage(PKGS[0], true, null, mKeyStore));
verify(mNetd).networkRejectNonSecureVpn(eq(false), aryEq(new UidRangeParcel[] {
verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
new UidRangeParcel(user.start + PKG_UIDS[0] + 1, user.start + PKG_UIDS[3] - 1),
new UidRangeParcel(user.start + PKG_UIDS[3] + 1, user.stop)
}));
verify(mNetd).networkRejectNonSecureVpn(eq(true), aryEq(new UidRangeParcel[] {
verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
new UidRangeParcel(user.start + PKG_UIDS[0] + 1, user.stop),
}));
assertBlocked(vpn, user.start + PKG_UIDS[1], user.start + PKG_UIDS[2],
@@ -432,10 +431,10 @@ public class VpnTest {
// Add the list of allowed packages.
assertTrue(vpn.setAlwaysOnPackage(
PKGS[0], true, Collections.singletonList(PKGS[1]), mKeyStore));
verify(mNetd).networkRejectNonSecureVpn(eq(false), aryEq(new UidRangeParcel[] {
verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
new UidRangeParcel(user.start + PKG_UIDS[0] + 1, user.stop)
}));
verify(mNetd).networkRejectNonSecureVpn(eq(true), aryEq(new UidRangeParcel[] {
verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
new UidRangeParcel(user.start + PKG_UIDS[0] + 1, user.start + PKG_UIDS[1] - 1),
new UidRangeParcel(user.start + PKG_UIDS[1] + 1, user.stop)
}));
@@ -450,11 +449,11 @@ public class VpnTest {
// allowed package should change from PGKS[1] to PKGS[2].
assertTrue(vpn.setAlwaysOnPackage(
PKGS[0], true, Arrays.asList("com.foo.app", PKGS[2], "com.bar.app"), mKeyStore));
verify(mNetd).networkRejectNonSecureVpn(eq(false), aryEq(new UidRangeParcel[]{
verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
new UidRangeParcel(user.start + PKG_UIDS[0] + 1, user.start + PKG_UIDS[1] - 1),
new UidRangeParcel(user.start + PKG_UIDS[1] + 1, user.stop)
}));
verify(mNetd).networkRejectNonSecureVpn(eq(true), aryEq(new UidRangeParcel[]{
verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
new UidRangeParcel(user.start + PKG_UIDS[0] + 1, user.start + PKG_UIDS[2] - 1),
new UidRangeParcel(user.start + PKG_UIDS[2] + 1, user.stop)
}));
@@ -475,7 +474,7 @@ public class VpnTest {
// Set lockdown.
assertTrue(vpn.setAlwaysOnPackage(PKGS[3], true, null, mKeyStore));
verify(mNetd).networkRejectNonSecureVpn(eq(true), aryEq(new UidRangeParcel[] {
verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
new UidRangeParcel(user.start, user.start + PKG_UIDS[3] - 1),
new UidRangeParcel(user.start + PKG_UIDS[3] + 1, user.stop)
}));
@@ -485,7 +484,7 @@ public class VpnTest {
// Add the restricted user.
setMockedUsers(primaryUser, tempProfile);
vpn.onUserAdded(tempProfile.id);
verify(mNetd).networkRejectNonSecureVpn(eq(true), aryEq(new UidRangeParcel[] {
verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(new UidRangeParcel[] {
new UidRangeParcel(profile.start, profile.start + PKG_UIDS[3] - 1),
new UidRangeParcel(profile.start + PKG_UIDS[3] + 1, profile.stop)
}));
@@ -493,7 +492,7 @@ public class VpnTest {
// Remove the restricted user.
tempProfile.partial = true;
vpn.onUserRemoved(tempProfile.id);
verify(mNetd).networkRejectNonSecureVpn(eq(false), aryEq(new UidRangeParcel[] {
verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(new UidRangeParcel[] {
new UidRangeParcel(profile.start, profile.start + PKG_UIDS[3] - 1),
new UidRangeParcel(profile.start + PKG_UIDS[3] + 1, profile.stop)
}));
@@ -506,22 +505,29 @@ public class VpnTest {
new UidRangeParcel(PRI_USER_RANGE.start, PRI_USER_RANGE.stop)};
// Given legacy lockdown is already enabled,
vpn.setLockdown(true);
verify(mNetd).networkRejectNonSecureVpn(eq(true), aryEq(primaryUserRangeParcel));
verify(mConnectivityManager, times(1)).setRequireVpnForUids(true,
toRanges(primaryUserRangeParcel));
// Enabling legacy lockdown twice should do nothing.
vpn.setLockdown(true);
verify(mNetd, times(1))
.networkRejectNonSecureVpn(anyBoolean(), any(UidRangeParcel[].class));
verify(mConnectivityManager, times(1)).setRequireVpnForUids(anyBoolean(), any());
// And disabling should remove the rules exactly once.
vpn.setLockdown(false);
verify(mNetd).networkRejectNonSecureVpn(eq(false), aryEq(primaryUserRangeParcel));
verify(mConnectivityManager, times(1)).setRequireVpnForUids(false,
toRanges(primaryUserRangeParcel));
// Removing the lockdown again should have no effect.
vpn.setLockdown(false);
verify(mNetd, times(2)).networkRejectNonSecureVpn(
anyBoolean(), any(UidRangeParcel[].class));
verify(mConnectivityManager, times(2)).setRequireVpnForUids(anyBoolean(), any());
}
private ArrayList<Range<Integer>> toRanges(UidRangeParcel[] ranges) {
ArrayList<Range<Integer>> rangesArray = new ArrayList<>(ranges.length);
for (int i = 0; i < ranges.length; i++) {
rangesArray.add(new Range<>(ranges[i].start, ranges[i].stop));
}
return rangesArray;
}
@Test
@@ -535,21 +541,21 @@ public class VpnTest {
new UidRangeParcel(entireUser[0].start + PKG_UIDS[0] + 1, entireUser[0].stop)
};
final InOrder order = inOrder(mNetd);
final InOrder order = inOrder(mConnectivityManager);
// Given lockdown is enabled with no package (legacy VPN),
vpn.setLockdown(true);
order.verify(mNetd).networkRejectNonSecureVpn(eq(true), aryEq(entireUser));
order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(entireUser));
// When a new VPN package is set the rules should change to cover that package.
vpn.prepare(null, PKGS[0], VpnManager.TYPE_VPN_SERVICE);
order.verify(mNetd).networkRejectNonSecureVpn(eq(false), aryEq(entireUser));
order.verify(mNetd).networkRejectNonSecureVpn(eq(true), aryEq(exceptPkg0));
order.verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(entireUser));
order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(exceptPkg0));
// When that VPN package is unset, everything should be undone again in reverse.
vpn.prepare(null, VpnConfig.LEGACY_VPN, VpnManager.TYPE_VPN_SERVICE);
order.verify(mNetd).networkRejectNonSecureVpn(eq(false), aryEq(exceptPkg0));
order.verify(mNetd).networkRejectNonSecureVpn(eq(true), aryEq(entireUser));
order.verify(mConnectivityManager).setRequireVpnForUids(false, toRanges(exceptPkg0));
order.verify(mConnectivityManager).setRequireVpnForUids(true, toRanges(entireUser));
}
@Test