Fix VPN bypass by updating VPN filter
This commit addresses an issue where the VPN bypass feature was not functioning correctly. The VPN filter has been updated to properly handle bypass rules, ensuring that designated traffic can now bypass the VPN as intended. Test: TH, manual Flag: EXEMPT guarded by default-off legacy flag Change-Id: Ie6073db4e23a3132042da21c25b696fa3abc4c91
This commit is contained in:
parent
4910c42dc7
commit
e4f5f85647
6 changed files with 354 additions and 67 deletions
|
|
@ -5889,6 +5889,11 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
|||
wakeupModifyInterface(iface, nai, false);
|
||||
}
|
||||
nai.networkMonitor().notifyNetworkDisconnected();
|
||||
if (!nai.getCaptivePortalDelegateUids().isEmpty()) {
|
||||
final Set<Integer> oldDelegateBypassUids = getAllCaptivePortalDelegateUids();
|
||||
nai.clearCaptivePortalDelegateUids();
|
||||
updateAllVpnForDelegateUid(oldDelegateBypassUids, getAllCaptivePortalDelegateUids());
|
||||
}
|
||||
mNetworkAgentInfos.remove(nai);
|
||||
if (mDeps.isShortNetworkSuspensionEnforced(mContext)) {
|
||||
// Remove the message to disconnect the network if it's in suspended state for too long,
|
||||
|
|
@ -6819,6 +6824,21 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
|||
}
|
||||
}
|
||||
|
||||
private Set<Integer> getAllCaptivePortalDelegateUids() {
|
||||
final Set<Integer> uids = new ArraySet<>();
|
||||
forEachNetworkAgentInfo(nai -> uids.addAll(nai.getCaptivePortalDelegateUids()));
|
||||
return uids;
|
||||
}
|
||||
|
||||
private void updateAllVpnForDelegateUid(Set<Integer> oldDelegateBypassUids,
|
||||
Set<Integer> newDelegateBypassUids) {
|
||||
forEachNetworkAgentInfo(vpnNai -> {
|
||||
if (!vpnNai.isVPN()) return;
|
||||
updateVpnFiltering(vpnNai.linkProperties, vpnNai.linkProperties, vpnNai,
|
||||
oldDelegateBypassUids, newDelegateBypassUids);
|
||||
});
|
||||
}
|
||||
|
||||
public class CaptivePortalImpl extends ICaptivePortal.Stub implements IBinder.DeathRecipient {
|
||||
private final Network mNetwork;
|
||||
// Binder object to track the lifetime of the setDelegateUid caller for cleanup purposes.
|
||||
|
|
@ -6872,7 +6892,22 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
|||
}
|
||||
}
|
||||
|
||||
private int updateDelegateUid(@NonNull NetworkAgentInfo nai, int uid) {
|
||||
if (nai.isCurrentUidDelegate(this, uid)) {
|
||||
return 0;
|
||||
}
|
||||
// TODO: consider using exceptions instead of errnos.
|
||||
final int errno = nai.removeCaptivePortalDelegateUid(this);
|
||||
if (errno != 0) return errno;
|
||||
|
||||
// If uid == INVALID_UID, we are done.
|
||||
if (uid == INVALID_UID) return 0;
|
||||
|
||||
return nai.setCaptivePortalDelegateUid(this, uid);
|
||||
}
|
||||
|
||||
private int handleSetDelegateUid(int uid, @NonNull final IBinder callerBinder) {
|
||||
ensureRunningOnConnectivityServiceThread();
|
||||
if (mDelegateUidCaller == null) {
|
||||
mDelegateUidCaller = callerBinder;
|
||||
try {
|
||||
|
|
@ -6884,7 +6919,6 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
|||
return ENOTCONN;
|
||||
}
|
||||
}
|
||||
|
||||
final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(mNetwork);
|
||||
if (nai == null) return ENOENT; // network does not exist anymore.
|
||||
if (nai.isDestroyed()) return ENOENT; // network has already been destroyed.
|
||||
|
|
@ -6892,16 +6926,13 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
|||
// TODO: consider allowing the uid to bypass VPN on all networks before V.
|
||||
if (!mDeps.isAtLeastV()) return EOPNOTSUPP;
|
||||
|
||||
// Check whether there has already been a delegate UID configured, if so, perform
|
||||
// cleanup and disallow bypassing VPN for that UID if no other caller is delegating
|
||||
// this UID.
|
||||
// TODO: consider using exceptions instead of errnos.
|
||||
final int errno = nai.removeCaptivePortalDelegateUid(this);
|
||||
if (errno != 0) return errno;
|
||||
|
||||
// If uid == INVALID_UID, we are done.
|
||||
if (uid == INVALID_UID) return 0;
|
||||
return nai.setCaptivePortalDelegateUid(this, uid);
|
||||
final Set<Integer> oldDelegateBypassUids = getAllCaptivePortalDelegateUids();
|
||||
int ret = updateDelegateUid(nai, uid);
|
||||
// updateDelegateUid() updates mCaptivePortalDelegateUids even if it returns non-zero
|
||||
// value. Therefore, we need to call updateAllVpnForDelegateUid regardless of the
|
||||
// returned value.
|
||||
updateAllVpnForDelegateUid(oldDelegateBypassUids, getAllCaptivePortalDelegateUids());
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -10189,7 +10220,10 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
|||
|
||||
// update filtering rules, need to happen after the interface update so netd knows about the
|
||||
// new interface (the interface name -> index map becomes initialized)
|
||||
updateVpnFiltering(newLp, oldLp, networkAgent);
|
||||
if (networkAgent.isVPN()) {
|
||||
Set<Integer> delegateBypassUids = getAllCaptivePortalDelegateUids();
|
||||
updateVpnFiltering(newLp, oldLp, networkAgent, delegateBypassUids, delegateBypassUids);
|
||||
}
|
||||
|
||||
updateIngressToVpnAddressFiltering(newLp, oldLp, networkAgent);
|
||||
|
||||
|
|
@ -10783,7 +10817,8 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
|||
}
|
||||
|
||||
private void updateVpnFiltering(@NonNull LinkProperties newLp, @Nullable LinkProperties oldLp,
|
||||
@NonNull NetworkAgentInfo nai) {
|
||||
@NonNull NetworkAgentInfo nai, Set<Integer> oldDelegateBypassUids,
|
||||
Set<Integer> newDelegateBypassUids) {
|
||||
final String oldIface = getVpnIsolationInterface(nai, nai.networkCapabilities, oldLp);
|
||||
final String newIface = getVpnIsolationInterface(nai, nai.networkCapabilities, newLp);
|
||||
final boolean wasFiltering = requiresVpnAllowRule(nai, oldLp, oldIface);
|
||||
|
|
@ -10794,7 +10829,8 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
|||
return;
|
||||
}
|
||||
|
||||
if (Objects.equals(oldIface, newIface) && (wasFiltering == needsFiltering)) {
|
||||
if (Objects.equals(oldIface, newIface) && (wasFiltering == needsFiltering)
|
||||
&& oldDelegateBypassUids.equals(newDelegateBypassUids)) {
|
||||
// Nothing changed.
|
||||
return;
|
||||
}
|
||||
|
|
@ -10810,10 +10846,12 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
|||
// make eBPF support two allowlisted interfaces so here new rules can be added before the
|
||||
// old rules are being removed.
|
||||
if (wasFiltering) {
|
||||
mPermissionMonitor.onVpnUidRangesRemoved(oldIface, ranges, vpnAppUid);
|
||||
mPermissionMonitor.onVpnUidRangesRemoved(oldIface, ranges, vpnAppUid,
|
||||
oldDelegateBypassUids);
|
||||
}
|
||||
if (needsFiltering) {
|
||||
mPermissionMonitor.onVpnUidRangesAdded(newIface, ranges, vpnAppUid);
|
||||
mPermissionMonitor.onVpnUidRangesAdded(newIface, ranges, vpnAppUid,
|
||||
newDelegateBypassUids);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -11565,10 +11603,11 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
|||
// to be removed will never overlap with the new range to be added.
|
||||
if (wasFiltering && !prevRanges.isEmpty()) {
|
||||
mPermissionMonitor.onVpnUidRangesRemoved(oldIface, prevRanges,
|
||||
prevNc.getOwnerUid());
|
||||
prevNc.getOwnerUid(), getAllCaptivePortalDelegateUids());
|
||||
}
|
||||
if (shouldFilter && !newRanges.isEmpty()) {
|
||||
mPermissionMonitor.onVpnUidRangesAdded(newIface, newRanges, newNc.getOwnerUid());
|
||||
mPermissionMonitor.onVpnUidRangesAdded(newIface, newRanges, newNc.getOwnerUid(),
|
||||
getAllCaptivePortalDelegateUids());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Never crash!
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET;
|
|||
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
|
||||
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
|
||||
import static android.net.NetworkCapabilities.transportNamesOf;
|
||||
import static android.os.Process.INVALID_UID;
|
||||
import static android.system.OsConstants.EEXIST;
|
||||
import static android.system.OsConstants.EIO;
|
||||
import static android.system.OsConstants.ENOENT;
|
||||
|
|
@ -93,6 +94,7 @@ import java.util.Arrays;
|
|||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
import java.util.function.Consumer;
|
||||
|
|
@ -1653,6 +1655,27 @@ public class NetworkAgentInfo implements NetworkRanker.Scoreable {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the delegate UIDs of the apps that are allowed to perform network traffic for captive.
|
||||
*/
|
||||
public Set<Integer> getCaptivePortalDelegateUids() {
|
||||
return new ArraySet<>(mCaptivePortalDelegateUids.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all the delegate UIDs.
|
||||
*/
|
||||
public void clearCaptivePortalDelegateUids() {
|
||||
mCaptivePortalDelegateUids.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given uid is the delegate uid of the given caller.
|
||||
*/
|
||||
public boolean isCurrentUidDelegate(@NonNull CaptivePortalImpl caller, int uid) {
|
||||
return uid == mCaptivePortalDelegateUids.getOrDefault(caller, INVALID_UID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the delegate UID of the app that is allowed to perform network traffic for captive
|
||||
* portal login, and configure the netd bypass rule with this delegated UID.
|
||||
|
|
|
|||
|
|
@ -1076,14 +1076,14 @@ public class PermissionMonitor {
|
|||
* @param vpnAppUid The uid of the VPN app
|
||||
*/
|
||||
public synchronized void onVpnUidRangesAdded(@Nullable String iface, Set<UidRange> rangesToAdd,
|
||||
int vpnAppUid) {
|
||||
int vpnAppUid, Set<Integer> delegatedBypassUids) {
|
||||
// Calculate the list of new app uids under the VPN due to the new UID ranges and update
|
||||
// Netd about them. Because mAllApps only contains appIds instead of uids, the result might
|
||||
// be an overestimation if an app is not installed on the user on which the VPN is running,
|
||||
// but that's safe: if an app is not installed, it cannot receive any packets, so dropping
|
||||
// packets to that UID is fine.
|
||||
final Set<Integer> changedUids = intersectUids(rangesToAdd, mAllApps);
|
||||
removeBypassingUids(changedUids, vpnAppUid);
|
||||
removeBypassingUids(changedUids, vpnAppUid, delegatedBypassUids);
|
||||
updateVpnUidsInterfaceRules(iface, changedUids, true /* add */);
|
||||
if (mVpnInterfaceUidRanges.containsKey(iface)) {
|
||||
mVpnInterfaceUidRanges.get(iface).addAll(rangesToAdd);
|
||||
|
|
@ -1101,11 +1101,11 @@ public class PermissionMonitor {
|
|||
* @param vpnAppUid The uid of the VPN app
|
||||
*/
|
||||
public synchronized void onVpnUidRangesRemoved(@Nullable String iface,
|
||||
Set<UidRange> rangesToRemove, int vpnAppUid) {
|
||||
Set<UidRange> rangesToRemove, int vpnAppUid, Set<Integer> delegatedBypassUids) {
|
||||
// Calculate the list of app uids that are no longer under the VPN due to the removed UID
|
||||
// ranges and update Netd about them.
|
||||
final Set<Integer> changedUids = intersectUids(rangesToRemove, mAllApps);
|
||||
removeBypassingUids(changedUids, vpnAppUid);
|
||||
removeBypassingUids(changedUids, vpnAppUid, delegatedBypassUids);
|
||||
updateVpnUidsInterfaceRules(iface, changedUids, false /* add */);
|
||||
Set<UidRange> existingRanges = mVpnInterfaceUidRanges.getOrDefault(iface, null);
|
||||
if (existingRanges == null) {
|
||||
|
|
@ -1201,8 +1201,10 @@ public class PermissionMonitor {
|
|||
* @param uids The list of uids to operate on
|
||||
* @param vpnAppUid The uid of the VPN app
|
||||
*/
|
||||
private void removeBypassingUids(Set<Integer> uids, int vpnAppUid) {
|
||||
private void removeBypassingUids(Set<Integer> uids, int vpnAppUid,
|
||||
Set<Integer> delegateBypassUids) {
|
||||
uids.remove(vpnAppUid);
|
||||
uids.removeAll(delegateBypassUids);
|
||||
uids.removeIf(this::hasRestrictedNetworksPermission);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5072,68 +5072,142 @@ public class ConnectivityServiceTest {
|
|||
|
||||
@Test
|
||||
@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
public void testCaptivePortalApp_SetDelegateUid() throws Exception {
|
||||
final TestNetworkCallback captivePortalCallback = new TestNetworkCallback();
|
||||
final NetworkRequest captivePortalRequest = new NetworkRequest.Builder()
|
||||
.addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build();
|
||||
mCm.registerNetworkCallback(captivePortalRequest, captivePortalCallback);
|
||||
public void testCaptivePortalApp_SetDelegateUidWithVpn() throws Exception {
|
||||
LinkProperties lp = new LinkProperties();
|
||||
InOrder inOrder = inOrder(mMockNetd, mBpfNetMaps);
|
||||
lp.setInterfaceName("tun0");
|
||||
lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null));
|
||||
lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE));
|
||||
// The uid range needs to cover the test app so the network is visible to it.
|
||||
final Set<UidRange> vpnRange = Collections.singleton(PRIMARY_UIDRANGE);
|
||||
mMockVpn.establish(lp, VPN_UID, vpnRange);
|
||||
assertVpnUidRangesUpdated(true, vpnRange, VPN_UID);
|
||||
// A connected VPN should have interface rules set up. There are two expected invocations,
|
||||
// one during the VPN initial connection, one during the VPN LinkProperties update.
|
||||
ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class);
|
||||
inOrder.verify(mBpfNetMaps, times(2)).addUidInterfaceRules(eq("tun0"), uidCaptor.capture());
|
||||
assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID);
|
||||
assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID);
|
||||
|
||||
|
||||
// bring up Wi-Fi with captive portal
|
||||
final TestNetworkCallback wifiCaptivePortalCallback = new TestNetworkCallback();
|
||||
mCm.registerNetworkCallback(
|
||||
new NetworkRequest.Builder()
|
||||
.addCapability(NET_CAPABILITY_CAPTIVE_PORTAL)
|
||||
.addTransportType(TRANSPORT_WIFI)
|
||||
.build(),
|
||||
wifiCaptivePortalCallback);
|
||||
mWiFiAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI);
|
||||
mWiFiAgent.connectWithCaptivePortal(TEST_REDIRECT_URL, false);
|
||||
captivePortalCallback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
|
||||
|
||||
final Intent signInIntent = startCaptivePortalApp(mWiFiAgent);
|
||||
final CaptivePortal captivePortal = signInIntent
|
||||
wifiCaptivePortalCallback.expectAvailableCallbacksUnvalidated(mWiFiAgent);
|
||||
mCm.unregisterNetworkCallback(wifiCaptivePortalCallback);
|
||||
final Intent wifiSignInIntent = startCaptivePortalApp(mWiFiAgent);
|
||||
final CaptivePortal wificaptivePortal = wifiSignInIntent
|
||||
.getParcelableExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL);
|
||||
|
||||
InOrder inOrder = inOrder(mMockNetd);
|
||||
// bring up ethernet with captive portal
|
||||
final TestNetworkCallback ethernetCaptivePortalCallback = new TestNetworkCallback();
|
||||
mCm.registerNetworkCallback(
|
||||
new NetworkRequest.Builder()
|
||||
.addCapability(NET_CAPABILITY_CAPTIVE_PORTAL)
|
||||
.addTransportType(TRANSPORT_ETHERNET)
|
||||
.build(),
|
||||
ethernetCaptivePortalCallback);
|
||||
mEthernetAgent = new TestNetworkAgentWrapper(TRANSPORT_ETHERNET);
|
||||
mEthernetAgent.connectWithCaptivePortal(TEST_REDIRECT_URL, false);
|
||||
ethernetCaptivePortalCallback.expectAvailableCallbacksUnvalidated(mEthernetAgent);
|
||||
mCm.unregisterNetworkCallback(ethernetCaptivePortalCallback);
|
||||
final Intent ethernetSignInIntent = startCaptivePortalApp(mEthernetAgent);
|
||||
final CaptivePortal ethernetCaptivePortal = ethernetSignInIntent
|
||||
.getParcelableExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL);
|
||||
|
||||
// Add the UID and check that it's added to the bypass list.
|
||||
FakeOutcomeReceiver or = new FakeOutcomeReceiver();
|
||||
captivePortal.setDelegateUid(APP2_UID, r -> r.run(), or);
|
||||
// Set delegate UID
|
||||
FakeOutcomeReceiver<Void, ServiceSpecificException> or = new FakeOutcomeReceiver<>();
|
||||
wificaptivePortal.setDelegateUid(APP1_UID, Runnable::run, or);
|
||||
or.awaitOutcome();
|
||||
inOrder.verify(mMockNetd).networkAllowBypassVpnOnNetwork(true, APP2_UID,
|
||||
mWiFiAgent.getNetwork().netId);
|
||||
|
||||
// Remove the UID and check that it's removed from the list.
|
||||
or = new FakeOutcomeReceiver();
|
||||
captivePortal.setDelegateUid(Process.INVALID_UID, r -> r.run(), or);
|
||||
or.awaitOutcome();
|
||||
inOrder.verify(mMockNetd).networkAllowBypassVpnOnNetwork(false, APP2_UID,
|
||||
mWiFiAgent.getNetwork().netId);
|
||||
|
||||
// Add the UID again.
|
||||
or = new FakeOutcomeReceiver();
|
||||
captivePortal.setDelegateUid(APP2_UID, r -> r.run(), or);
|
||||
or.awaitOutcome();
|
||||
inOrder.verify(mMockNetd).networkAllowBypassVpnOnNetwork(true, APP2_UID,
|
||||
mWiFiAgent.getNetwork().netId);
|
||||
|
||||
// Add the UID again. Nothing should change.
|
||||
or = new FakeOutcomeReceiver();
|
||||
captivePortal.setDelegateUid(APP2_UID, r -> r.run(), or);
|
||||
or.awaitOutcome();
|
||||
// BUG: the code should not call into netd.
|
||||
inOrder.verify(mMockNetd).networkAllowBypassVpnOnNetwork(false, APP2_UID,
|
||||
mWiFiAgent.getNetwork().netId);
|
||||
inOrder.verify(mMockNetd).networkAllowBypassVpnOnNetwork(true, APP2_UID,
|
||||
mWiFiAgent.getNetwork().netId);
|
||||
|
||||
// Add another UID again. The old UID should be removed and the new one added.
|
||||
or = new FakeOutcomeReceiver();
|
||||
captivePortal.setDelegateUid(APP1_UID, r -> r.run(), or);
|
||||
or.awaitOutcome();
|
||||
inOrder.verify(mMockNetd).networkAllowBypassVpnOnNetwork(false, APP2_UID,
|
||||
mWiFiAgent.getNetwork().netId);
|
||||
inOrder.verify(mMockNetd).networkAllowBypassVpnOnNetwork(true, APP1_UID,
|
||||
mWiFiAgent.getNetwork().netId);
|
||||
uidCaptor = ArgumentCaptor.forClass(int[].class);
|
||||
inOrder.verify(mBpfNetMaps, times(1)).removeUidInterfaceRules(uidCaptor.capture());
|
||||
assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID);
|
||||
uidCaptor = ArgumentCaptor.forClass(int[].class);
|
||||
inOrder.verify(mBpfNetMaps, times(1)).addUidInterfaceRules(eq("tun0"), uidCaptor.capture());
|
||||
assertContainsExactly(uidCaptor.getValue(), APP2_UID);
|
||||
|
||||
// Add and remove delegate UID from another client for same UID should not trigger any VPN
|
||||
// rule update.
|
||||
or = new FakeOutcomeReceiver<>();
|
||||
ethernetCaptivePortal.setDelegateUid(APP1_UID, Runnable::run, or);
|
||||
or.awaitOutcome();
|
||||
inOrder.verify(mMockNetd).networkAllowBypassVpnOnNetwork(true, APP1_UID,
|
||||
mEthernetAgent.getNetwork().netId);
|
||||
inOrder.verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
|
||||
inOrder.verify(mBpfNetMaps, never()).removeUidInterfaceRules(any());
|
||||
|
||||
or = new FakeOutcomeReceiver<>();
|
||||
ethernetCaptivePortal.setDelegateUid(INVALID_UID, Runnable::run, or);
|
||||
or.awaitOutcome();
|
||||
inOrder.verify(mMockNetd).networkAllowBypassVpnOnNetwork(false, APP1_UID,
|
||||
mEthernetAgent.getNetwork().netId);
|
||||
inOrder.verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
|
||||
inOrder.verify(mBpfNetMaps, never()).removeUidInterfaceRules(any());
|
||||
|
||||
// Disconnect VPN
|
||||
mMockVpn.disconnect();
|
||||
waitForIdle();
|
||||
uidCaptor = ArgumentCaptor.forClass(int[].class);
|
||||
inOrder.verify(mBpfNetMaps, times(1)).removeUidInterfaceRules(uidCaptor.capture());
|
||||
assertContainsExactly(uidCaptor.getValue(), APP2_UID);
|
||||
|
||||
// Reconnect VPN
|
||||
mMockVpn.establish(lp, VPN_UID, vpnRange);
|
||||
assertVpnUidRangesUpdated(true, vpnRange, VPN_UID);
|
||||
uidCaptor = ArgumentCaptor.forClass(int[].class);
|
||||
inOrder.verify(mBpfNetMaps, times(2)).addUidInterfaceRules(eq("tun0"), uidCaptor.capture());
|
||||
assertContainsExactly(uidCaptor.getAllValues().get(0), APP2_UID);
|
||||
assertContainsExactly(uidCaptor.getAllValues().get(1), APP2_UID);
|
||||
|
||||
// Remove delegate UID
|
||||
or = new FakeOutcomeReceiver<>();
|
||||
wificaptivePortal.setDelegateUid(Process.INVALID_UID, Runnable::run, or);
|
||||
or.awaitOutcome();
|
||||
inOrder.verify(mMockNetd).networkAllowBypassVpnOnNetwork(false, APP1_UID,
|
||||
mWiFiAgent.getNetwork().netId);
|
||||
inOrder.verify(mBpfNetMaps, times(1)).removeUidInterfaceRules(uidCaptor.capture());
|
||||
assertContainsExactly(uidCaptor.getValue(), APP2_UID);
|
||||
uidCaptor = ArgumentCaptor.forClass(int[].class);
|
||||
inOrder.verify(mBpfNetMaps, times(1)).addUidInterfaceRules(eq("tun0"), uidCaptor.capture());
|
||||
assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID);
|
||||
|
||||
// Add back delegate UID
|
||||
or = new FakeOutcomeReceiver<>();
|
||||
wificaptivePortal.setDelegateUid(APP1_UID, Runnable::run, or);
|
||||
or.awaitOutcome();
|
||||
inOrder.verify(mMockNetd).networkAllowBypassVpnOnNetwork(true, APP1_UID,
|
||||
mWiFiAgent.getNetwork().netId);
|
||||
uidCaptor = ArgumentCaptor.forClass(int[].class);
|
||||
inOrder.verify(mBpfNetMaps, times(1)).removeUidInterfaceRules(uidCaptor.capture());
|
||||
assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID);
|
||||
uidCaptor = ArgumentCaptor.forClass(int[].class);
|
||||
inOrder.verify(mBpfNetMaps, times(1)).addUidInterfaceRules(eq("tun0"), uidCaptor.capture());
|
||||
assertContainsExactly(uidCaptor.getValue(), APP2_UID);
|
||||
|
||||
// Wi-Fi network go away should trigger VPN rule being cleaned up.
|
||||
mWiFiAgent.disconnect();
|
||||
waitForIdle();
|
||||
uidCaptor = ArgumentCaptor.forClass(int[].class);
|
||||
inOrder.verify(mBpfNetMaps, times(1)).removeUidInterfaceRules(uidCaptor.capture());
|
||||
assertContainsExactly(uidCaptor.getValue(), APP2_UID);
|
||||
uidCaptor = ArgumentCaptor.forClass(int[].class);
|
||||
inOrder.verify(mBpfNetMaps, times(1)).addUidInterfaceRules(eq("tun0"), uidCaptor.capture());
|
||||
assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID);
|
||||
|
||||
// TODO: check VPN interface filtering rules, see
|
||||
// testFullyRoutedVpnResultsInInterfaceFilteringRules
|
||||
mMockVpn.disconnect();
|
||||
waitForIdle();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testAvoidOrIgnoreCaptivePortals() throws Exception {
|
||||
final TestNetworkCallback captivePortalCallback = new TestNetworkCallback();
|
||||
|
|
|
|||
|
|
@ -949,7 +949,7 @@ public class PermissionMonitorTest {
|
|||
final Set<UidRange> vpnRange2 = Set.of(new UidRange(MOCK_UID12, MOCK_UID12));
|
||||
|
||||
// When VPN is connected, expect a rule to be set up for user app MOCK_UID11
|
||||
mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange1, VPN_UID);
|
||||
mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange1, VPN_UID, Set.of());
|
||||
verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
|
||||
|
||||
reset(mBpfNetMaps);
|
||||
|
|
@ -964,15 +964,15 @@ public class PermissionMonitorTest {
|
|||
|
||||
// During VPN uid update (vpnRange1 -> vpnRange2), ConnectivityService first deletes the
|
||||
// old UID rules then adds the new ones. Expect netd to be updated
|
||||
mPermissionMonitor.onVpnUidRangesRemoved(ifName, vpnRange1, VPN_UID);
|
||||
mPermissionMonitor.onVpnUidRangesRemoved(ifName, vpnRange1, VPN_UID, Set.of());
|
||||
verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[] {MOCK_UID11}));
|
||||
mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange2, VPN_UID);
|
||||
mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange2, VPN_UID, Set.of());
|
||||
verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID12}));
|
||||
|
||||
reset(mBpfNetMaps);
|
||||
|
||||
// When VPN is disconnected, expect rules to be torn down
|
||||
mPermissionMonitor.onVpnUidRangesRemoved(ifName, vpnRange2, VPN_UID);
|
||||
mPermissionMonitor.onVpnUidRangesRemoved(ifName, vpnRange2, VPN_UID, Set.of());
|
||||
verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[] {MOCK_UID12}));
|
||||
}
|
||||
|
||||
|
|
@ -1000,7 +1000,7 @@ public class PermissionMonitorTest {
|
|||
onUserAddedWithInstalledPackageList(MOCK_USER2, pkgs);
|
||||
final Set<UidRange> vpnRange = Set.of(UidRange.createForUser(MOCK_USER1),
|
||||
UidRange.createForUser(MOCK_USER2));
|
||||
mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange, VPN_UID);
|
||||
mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange, VPN_UID, Set.of());
|
||||
|
||||
// Newly-installed package should have uid rules added
|
||||
addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_APPID1);
|
||||
|
|
@ -1025,6 +1025,36 @@ public class PermissionMonitorTest {
|
|||
doTestUidFilteringDuringPackageInstallAndUninstall(null /* ifName */);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUidFilteringWithDelegatedBypassUids() throws Exception {
|
||||
final String ifName = "tun0";
|
||||
final int delegatedUid = MOCK_UID12;
|
||||
final List<PackageInfo> pkgs = List.of(
|
||||
buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
|
||||
CONNECTIVITY_USE_RESTRICTED_NETWORKS),
|
||||
buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
|
||||
buildPackageInfo(MOCK_PACKAGE2, delegatedUid),
|
||||
buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID));
|
||||
initialize();
|
||||
onUserAddedWithInstalledPackageList(MOCK_USER1, pkgs);
|
||||
|
||||
final Set<UidRange> vpnRange = Set.of(UidRange.createForUser(MOCK_USER1));
|
||||
|
||||
// When VPN is connected, expect a rule to be set up for user app MOCK_UID11, but not for
|
||||
// delegatedUid.
|
||||
final Set<Integer> delegatedUids = Set.of(delegatedUid);
|
||||
mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange, VPN_UID, delegatedUids);
|
||||
verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
|
||||
verify(mBpfNetMaps, never()).addUidInterfaceRules(eq(ifName),
|
||||
aryEq(new int[]{delegatedUid}));
|
||||
|
||||
// When VPN is disconnected, expect rules to be torn down for MOCK_UID11 and still no rule
|
||||
// for delegatedUid.
|
||||
mPermissionMonitor.onVpnUidRangesRemoved(ifName, vpnRange, VPN_UID, delegatedUids);
|
||||
verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[]{MOCK_UID11}));
|
||||
verify(mBpfNetMaps, never()).removeUidInterfaceRules(aryEq(new int[]{delegatedUid}));
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnableCompatChanges(RESTRICT_LOCAL_NETWORK)
|
||||
public void testLockdownUidFilteringWithLockdownEnableDisable() {
|
||||
|
|
|
|||
|
|
@ -30,17 +30,30 @@ import android.net.NetworkRequest
|
|||
import android.net.NetworkStack
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.ConditionVariable
|
||||
import android.os.OutcomeReceiver
|
||||
import android.os.ServiceSpecificException
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.testutils.DevSdkIgnoreRule
|
||||
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
|
||||
import com.android.testutils.DevSdkIgnoreRunner
|
||||
import com.android.testutils.TestableNetworkCallback
|
||||
import kotlin.test.DefaultAsserter.assertTrue
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
import kotlin.test.fail
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentMatchers.anyInt
|
||||
import org.mockito.Mockito.inOrder
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.verify
|
||||
|
||||
private const val APP1_UID = 10001
|
||||
private const val APP2_UID = 10002
|
||||
private const val TIMEOUT_MS = 2_000L
|
||||
|
||||
@DevSdkIgnoreRunner.MonitorThreadLeak
|
||||
@RunWith(DevSdkIgnoreRunner::class)
|
||||
@SmallTest
|
||||
|
|
@ -48,7 +61,113 @@ import org.mockito.Mockito.verify
|
|||
class CSCaptivePortalAppTest : CSTest() {
|
||||
private val WIFI_IFACE = "wifi0"
|
||||
private val TEST_REDIRECT_URL = "http://example.com/firstPath"
|
||||
private val TIMEOUT_MS = 2_000L
|
||||
|
||||
@get:Rule val ignoreRule = DevSdkIgnoreRule()
|
||||
|
||||
private class FakeOutcomeReceiver<R, E : Throwable> : OutcomeReceiver<R, E> {
|
||||
private val mCv = ConditionVariable()
|
||||
private var mError: E? = null
|
||||
|
||||
override fun onResult(result: R) {
|
||||
mCv.open()
|
||||
}
|
||||
|
||||
override fun onError(error: E) {
|
||||
mError = error
|
||||
mCv.open()
|
||||
}
|
||||
|
||||
fun awaitOutcome() {
|
||||
assertTrue(
|
||||
"OutcomeReceiver did not receive outcome after $TIMEOUT_MS ms",
|
||||
mCv.block(TIMEOUT_MS)
|
||||
)
|
||||
if (mError != null) {
|
||||
fail("OutcomeReceiver got: " + mError!!.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper extension function to reduce boilerplate in the test.
|
||||
*/
|
||||
private fun CaptivePortal.setDelegateUidAndAwait(uid: Int) {
|
||||
val or = FakeOutcomeReceiver<Void, ServiceSpecificException>()
|
||||
this.setDelegateUid(uid, CSTestExecutor, or)
|
||||
or.awaitOutcome()
|
||||
}
|
||||
|
||||
@Test
|
||||
@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||
fun testCaptivePortalApp_SetDelegateUid() {
|
||||
val captivePortalCallback = TestableNetworkCallback()
|
||||
val captivePortalRequest = NetworkRequest.Builder()
|
||||
.addCapability(NET_CAPABILITY_CAPTIVE_PORTAL).build()
|
||||
cm.registerNetworkCallback(captivePortalRequest, captivePortalCallback)
|
||||
val wifiAgent = Agent(WIFI_IFACE, TRANSPORT_WIFI, NET_CAPABILITY_INTERNET)
|
||||
wifiAgent.connectWithCaptivePortal(TEST_REDIRECT_URL)
|
||||
captivePortalCallback.expectAvailableCallbacksUnvalidated(wifiAgent)
|
||||
|
||||
val signInIntent = startCaptivePortalApp(wifiAgent)
|
||||
val captivePortal = signInIntent.getParcelableExtra(
|
||||
EXTRA_CAPTIVE_PORTAL,
|
||||
CaptivePortal::class.java
|
||||
)!!
|
||||
|
||||
val inOrder = inOrder(netd)
|
||||
|
||||
// Add the UID and check that it's added to the bypass list.
|
||||
captivePortal.setDelegateUidAndAwait(APP2_UID)
|
||||
inOrder.verify(netd).networkAllowBypassVpnOnNetwork(
|
||||
true /* allow */,
|
||||
APP2_UID,
|
||||
wifiAgent.network.netId
|
||||
)
|
||||
|
||||
// Remove the UID and check that it's removed from the list.
|
||||
captivePortal.setDelegateUidAndAwait(android.os.Process.INVALID_UID)
|
||||
inOrder.verify(netd).networkAllowBypassVpnOnNetwork(
|
||||
false /* allow */,
|
||||
APP2_UID,
|
||||
wifiAgent.network.netId
|
||||
)
|
||||
|
||||
// Add the UID again.
|
||||
captivePortal.setDelegateUidAndAwait(APP2_UID)
|
||||
inOrder.verify(netd).networkAllowBypassVpnOnNetwork(
|
||||
true /* allow */,
|
||||
APP2_UID,
|
||||
wifiAgent.network.netId
|
||||
)
|
||||
|
||||
// Add the UID again. Nothing should change.
|
||||
captivePortal.setDelegateUidAndAwait(APP2_UID)
|
||||
inOrder.verify(netd, never()).networkAllowBypassVpnOnNetwork(
|
||||
false /* allow */,
|
||||
APP2_UID,
|
||||
wifiAgent.network.netId
|
||||
)
|
||||
inOrder.verify(netd, never()).networkAllowBypassVpnOnNetwork(
|
||||
true /* allow */,
|
||||
APP2_UID,
|
||||
wifiAgent.network.netId
|
||||
)
|
||||
|
||||
// Add another UID again. The old UID should be removed and the new one added.
|
||||
captivePortal.setDelegateUidAndAwait(APP1_UID)
|
||||
inOrder.verify(netd).networkAllowBypassVpnOnNetwork(
|
||||
false /* allow */,
|
||||
APP2_UID,
|
||||
wifiAgent.network.netId
|
||||
)
|
||||
inOrder.verify(netd).networkAllowBypassVpnOnNetwork(
|
||||
true /* allow */,
|
||||
APP1_UID,
|
||||
wifiAgent.network.netId
|
||||
)
|
||||
|
||||
wifiAgent.disconnect()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCaptivePortalApp_Reevaluate_Nopermission() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue