Merge changes If52ece61,Iedf344f6 am: f6131c483b

Original change: https://android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/2095365

Change-Id: Ib621c73e18a33bcf2464588f07123ed969784898
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Motomu Utsumi
2022-05-17 02:17:48 +00:00
committed by Automerger Merge Worker
11 changed files with 589 additions and 79 deletions

View File

@@ -132,6 +132,7 @@ enum UidOwnerMatchType {
RESTRICTED_MATCH = (1 << 5),
LOW_POWER_STANDBY_MATCH = (1 << 6),
IIF_MATCH = (1 << 7),
LOCKDOWN_VPN_MATCH = (1 << 8),
};
enum BpfPermissionMatch {

View File

@@ -214,9 +214,16 @@ static inline int bpf_owner_match(struct __sk_buff* skb, uint32_t uid, int direc
return BPF_DROP;
}
}
if (direction == BPF_INGRESS && (uidRules & IIF_MATCH)) {
// Drops packets not coming from lo nor the allowlisted interface
if (allowed_iif && skb->ifindex != 1 && skb->ifindex != allowed_iif) {
if (direction == BPF_INGRESS && skb->ifindex != 1) {
if (uidRules & IIF_MATCH) {
if (allowed_iif && skb->ifindex != allowed_iif) {
// Drops packets not coming from lo nor the allowed interface
// allowed interface=0 is a wildcard and does not drop packets
return BPF_DROP_UNLESS_DNS;
}
} else if (uidRules & LOCKDOWN_VPN_MATCH) {
// Drops packets not coming from lo and rule does not have IIF_MATCH but has
// LOCKDOWN_VPN_MATCH
return BPF_DROP_UNLESS_DNS;
}
}

View File

@@ -982,6 +982,16 @@ public class ConnectivityManager {
@SystemApi(client = MODULE_LIBRARIES)
public static final int FIREWALL_CHAIN_LOW_POWER_STANDBY = 5;
/**
* Firewall chain used for lockdown VPN.
* Denylist of apps that cannot receive incoming packets except on loopback because they are
* subject to an always-on VPN which is not currently connected.
*
* @see #BLOCKED_REASON_LOCKDOWN_VPN
* @hide
*/
public static final int FIREWALL_CHAIN_LOCKDOWN_VPN = 6;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = false, prefix = "FIREWALL_CHAIN_", value = {
@@ -989,7 +999,8 @@ public class ConnectivityManager {
FIREWALL_CHAIN_STANDBY,
FIREWALL_CHAIN_POWERSAVE,
FIREWALL_CHAIN_RESTRICTED,
FIREWALL_CHAIN_LOW_POWER_STANDBY
FIREWALL_CHAIN_LOW_POWER_STANDBY,
FIREWALL_CHAIN_LOCKDOWN_VPN
})
public @interface FirewallChain {}
// LINT.ThenChange(packages/modules/Connectivity/service/native/include/Common.h)

View File

@@ -133,12 +133,16 @@ static jint native_setUidRule(JNIEnv* env, jobject clazz, jint childChain, jint
static jint native_addUidInterfaceRules(JNIEnv* env, jobject clazz, jstring ifName,
jintArray jUids) {
const ScopedUtfChars ifNameUtf8(env, ifName);
if (ifNameUtf8.c_str() == nullptr) {
return -EINVAL;
// Null ifName is a wildcard to allow apps to receive packets on all interfaces and ifIndex is
// set to 0.
int ifIndex;
if (ifName != nullptr) {
const ScopedUtfChars ifNameUtf8(env, ifName);
const std::string interfaceName(ifNameUtf8.c_str());
ifIndex = if_nametoindex(interfaceName.c_str());
} else {
ifIndex = 0;
}
const std::string interfaceName(ifNameUtf8.c_str());
const int ifIndex = if_nametoindex(interfaceName.c_str());
ScopedIntArrayRO uids(env, jUids);
if (uids.get() == nullptr) {

View File

@@ -98,6 +98,7 @@ const std::string uidMatchTypeToString(uint32_t match) {
FLAG_MSG_TRANS(matchType, RESTRICTED_MATCH, match);
FLAG_MSG_TRANS(matchType, LOW_POWER_STANDBY_MATCH, match);
FLAG_MSG_TRANS(matchType, IIF_MATCH, match);
FLAG_MSG_TRANS(matchType, LOCKDOWN_VPN_MATCH, match);
if (match) {
return StringPrintf("Unknown match: %u", match);
}
@@ -286,16 +287,13 @@ Status TrafficController::removeRule(uint32_t uid, UidOwnerMatchType match) {
}
Status TrafficController::addRule(uint32_t uid, UidOwnerMatchType match, uint32_t iif) {
// iif should be non-zero if and only if match == MATCH_IIF
if (match == IIF_MATCH && iif == 0) {
return statusFromErrno(EINVAL, "Interface match must have nonzero interface index");
} else if (match != IIF_MATCH && iif != 0) {
if (match != IIF_MATCH && iif != 0) {
return statusFromErrno(EINVAL, "Non-interface match must have zero interface index");
}
auto oldMatch = mUidOwnerMap.readValue(uid);
if (oldMatch.ok()) {
UidOwnerValue newMatch = {
.iif = iif ? iif : oldMatch.value().iif,
.iif = (match == IIF_MATCH) ? iif : oldMatch.value().iif,
.rule = oldMatch.value().rule | match,
};
RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
@@ -335,6 +333,8 @@ FirewallType TrafficController::getFirewallType(ChildChain chain) {
return ALLOWLIST;
case LOW_POWER_STANDBY:
return ALLOWLIST;
case LOCKDOWN:
return DENYLIST;
case NONE:
default:
return DENYLIST;
@@ -360,6 +360,9 @@ int TrafficController::changeUidOwnerRule(ChildChain chain, uid_t uid, FirewallR
case LOW_POWER_STANDBY:
res = updateOwnerMapEntry(LOW_POWER_STANDBY_MATCH, uid, rule, type);
break;
case LOCKDOWN:
res = updateOwnerMapEntry(LOCKDOWN_VPN_MATCH, uid, rule, type);
break;
case NONE:
default:
ALOGW("Unknown child chain: %d", chain);
@@ -399,9 +402,6 @@ Status TrafficController::replaceRulesInMap(const UidOwnerMatchType match,
Status TrafficController::addUidInterfaceRules(const int iif,
const std::vector<int32_t>& uidsToAdd) {
if (!iif) {
return statusFromErrno(EINVAL, "Interface rule must specify interface");
}
std::lock_guard guard(mMutex);
for (auto uid : uidsToAdd) {

View File

@@ -307,6 +307,7 @@ TEST_F(TrafficControllerTest, TestChangeUidOwnerRule) {
checkUidOwnerRuleForChain(POWERSAVE, POWERSAVE_MATCH);
checkUidOwnerRuleForChain(RESTRICTED, RESTRICTED_MATCH);
checkUidOwnerRuleForChain(LOW_POWER_STANDBY, LOW_POWER_STANDBY_MATCH);
checkUidOwnerRuleForChain(LOCKDOWN, LOCKDOWN_VPN_MATCH);
ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(NONE, TEST_UID, ALLOW, ALLOWLIST));
ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(INVALID_CHAIN, TEST_UID, ALLOW, ALLOWLIST));
}
@@ -491,6 +492,70 @@ TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesCoexistWithNewMatche
checkEachUidValue({10001, 10002}, IIF_MATCH);
}
TEST_F(TrafficControllerTest, TestAddUidInterfaceFilteringRulesWithWildcard) {
// iif=0 is a wildcard
int iif = 0;
// Add interface rule with wildcard to uids
ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000, 1001})));
expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif);
}
TEST_F(TrafficControllerTest, TestRemoveUidInterfaceFilteringRulesWithWildcard) {
// iif=0 is a wildcard
int iif = 0;
// Add interface rule with wildcard to two uids
ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000, 1001})));
expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif);
// Remove interface rule from one of the uids
ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
expectUidOwnerMapValues({1001}, IIF_MATCH, iif);
checkEachUidValue({1001}, IIF_MATCH);
// Remove interface rule from the remaining uid
ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1001})));
expectMapEmpty(mFakeUidOwnerMap);
}
TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesWithWildcardAndExistingMatches) {
// Set up existing DOZABLE_MATCH and POWERSAVE_MATCH rule
ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
TrafficController::IptOpInsert)));
ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
TrafficController::IptOpInsert)));
// iif=0 is a wildcard
int iif = 0;
// Add interface rule with wildcard to the existing uid
ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000})));
expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif);
// Remove interface rule with wildcard from the existing uid
ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH, 0);
}
TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesWithWildcardAndNewMatches) {
// iif=0 is a wildcard
int iif = 0;
// Set up existing interface rule with wildcard
ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000})));
// Add DOZABLE_MATCH and POWERSAVE_MATCH rule to the existing uid
ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
TrafficController::IptOpInsert)));
ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
TrafficController::IptOpInsert)));
expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif);
// Remove DOZABLE_MATCH and POWERSAVE_MATCH rule from the existing uid
ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
TrafficController::IptOpDelete)));
ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
TrafficController::IptOpDelete)));
expectUidOwnerMapValues({1000}, IIF_MATCH, iif);
}
TEST_F(TrafficControllerTest, TestGrantInternetPermission) {
std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};

View File

@@ -35,6 +35,7 @@ enum ChildChain {
POWERSAVE = 3,
RESTRICTED = 4,
LOW_POWER_STANDBY = 5,
LOCKDOWN = 6,
INVALID_CHAIN
};
// LINT.ThenChange(packages/modules/Connectivity/framework/src/android/net/ConnectivityManager.java)

View File

@@ -5969,6 +5969,10 @@ public class ConnectivityService extends IConnectivityManager.Stub
+ Arrays.toString(ranges) + "): netd command failed: " + e);
}
if (SdkLevel.isAtLeastT()) {
mPermissionMonitor.updateVpnLockdownUidRanges(requireVpn, ranges);
}
for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
final boolean curMetered = nai.networkCapabilities.isMetered();
maybeNotifyNetworkBlocked(nai, curMetered, curMetered,
@@ -7732,10 +7736,10 @@ public class ConnectivityService extends IConnectivityManager.Stub
private void updateVpnFiltering(LinkProperties newLp, LinkProperties oldLp,
NetworkAgentInfo nai) {
final String oldIface = oldLp != null ? oldLp.getInterfaceName() : null;
final String newIface = newLp != null ? newLp.getInterfaceName() : null;
final boolean wasFiltering = requiresVpnIsolation(nai, nai.networkCapabilities, oldLp);
final boolean needsFiltering = requiresVpnIsolation(nai, nai.networkCapabilities, newLp);
final String oldIface = getVpnIsolationInterface(nai, nai.networkCapabilities, oldLp);
final String newIface = getVpnIsolationInterface(nai, nai.networkCapabilities, newLp);
final boolean wasFiltering = requiresVpnAllowRule(nai, oldLp, oldIface);
final boolean needsFiltering = requiresVpnAllowRule(nai, newLp, newIface);
if (!wasFiltering && !needsFiltering) {
// Nothing to do.
@@ -7748,11 +7752,19 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
final Set<UidRange> ranges = nai.networkCapabilities.getUidRanges();
if (ranges == null || ranges.isEmpty()) {
return;
}
final int vpnAppUid = nai.networkCapabilities.getOwnerUid();
// TODO: this create a window of opportunity for apps to receive traffic between the time
// when the old rules are removed and the time when new rules are added. To fix this,
// make eBPF support two allowlisted interfaces so here new rules can be added before the
// old rules are being removed.
// Null iface given to onVpnUidRangesAdded/Removed is a wildcard to allow apps to receive
// packets on all interfaces. This is required to accept incoming traffic in Lockdown mode
// by overriding the Lockdown blocking rule.
if (wasFiltering) {
mPermissionMonitor.onVpnUidRangesRemoved(oldIface, ranges, vpnAppUid);
}
@@ -8041,15 +8053,14 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
/**
* Returns whether VPN isolation (ingress interface filtering) should be applied on the given
* network.
* Returns the interface which requires VPN isolation (ingress interface filtering).
*
* Ingress interface filtering enforces that all apps under the given network can only receive
* packets from the network's interface (and loopback). This is important for VPNs because
* apps that cannot bypass a fully-routed VPN shouldn't be able to receive packets from any
* non-VPN interfaces.
*
* As a result, this method should return true iff
* As a result, this method should return Non-null interface iff
* 1. the network is an app VPN (not legacy VPN)
* 2. the VPN does not allow bypass
* 3. the VPN is fully-routed
@@ -8058,16 +8069,32 @@ public class ConnectivityService extends IConnectivityManager.Stub
* @see INetd#firewallAddUidInterfaceRules
* @see INetd#firewallRemoveUidInterfaceRules
*/
private boolean requiresVpnIsolation(@NonNull NetworkAgentInfo nai, NetworkCapabilities nc,
@Nullable
private String getVpnIsolationInterface(@NonNull NetworkAgentInfo nai, NetworkCapabilities nc,
LinkProperties lp) {
if (nc == null || lp == null) return false;
return nai.isVPN()
if (nc == null || lp == null) return null;
if (nai.isVPN()
&& !nai.networkAgentConfig.allowBypass
&& nc.getOwnerUid() != Process.SYSTEM_UID
&& lp.getInterfaceName() != null
&& (lp.hasIpv4DefaultRoute() || lp.hasIpv4UnreachableDefaultRoute())
&& (lp.hasIpv6DefaultRoute() || lp.hasIpv6UnreachableDefaultRoute())
&& !lp.hasExcludeRoute();
&& !lp.hasExcludeRoute()) {
return lp.getInterfaceName();
}
return null;
}
/**
* Returns whether we need to set interface filtering rule or not
*/
private boolean requiresVpnAllowRule(NetworkAgentInfo nai, LinkProperties lp,
String filterIface) {
// Only filter if lp has an interface.
if (lp == null || lp.getInterfaceName() == null) return false;
// Before T, allow rules are only needed if VPN isolation is enabled.
// T and After T, allow rules are needed for all VPNs.
return filterIface != null || (nai.isVPN() && SdkLevel.isAtLeastT());
}
private static UidRangeParcel[] toUidRangeStableParcels(final @NonNull Set<UidRange> ranges) {
@@ -8195,9 +8222,10 @@ public class ConnectivityService extends IConnectivityManager.Stub
if (!prevRanges.isEmpty()) {
updateVpnUidRanges(false, nai, prevRanges);
}
final boolean wasFiltering = requiresVpnIsolation(nai, prevNc, nai.linkProperties);
final boolean shouldFilter = requiresVpnIsolation(nai, newNc, nai.linkProperties);
final String iface = nai.linkProperties.getInterfaceName();
final String oldIface = getVpnIsolationInterface(nai, prevNc, nai.linkProperties);
final String newIface = getVpnIsolationInterface(nai, newNc, nai.linkProperties);
final boolean wasFiltering = requiresVpnAllowRule(nai, nai.linkProperties, oldIface);
final boolean shouldFilter = requiresVpnAllowRule(nai, nai.linkProperties, newIface);
// For VPN uid interface filtering, old ranges need to be removed before new ranges can
// be added, due to the range being expanded and stored as individual UIDs. For example
// the UIDs might be updated from [0, 99999] to ([0, 10012], [10014, 99999]) which means
@@ -8209,11 +8237,16 @@ public class ConnectivityService extends IConnectivityManager.Stub
// above, where the addition of new ranges happens before the removal of old ranges.
// TODO Fix this window by computing an accurate diff on Set<UidRange>, so the old range
// to be removed will never overlap with the new range to be added.
// Null iface given to onVpnUidRangesAdded/Removed is a wildcard to allow apps to
// receive packets on all interfaces. This is required to accept incoming traffic in
// Lockdown mode by overriding the Lockdown blocking rule.
if (wasFiltering && !prevRanges.isEmpty()) {
mPermissionMonitor.onVpnUidRangesRemoved(iface, prevRanges, prevNc.getOwnerUid());
mPermissionMonitor.onVpnUidRangesRemoved(oldIface, prevRanges,
prevNc.getOwnerUid());
}
if (shouldFilter && !newRanges.isEmpty()) {
mPermissionMonitor.onVpnUidRangesAdded(iface, newRanges, newNc.getOwnerUid());
mPermissionMonitor.onVpnUidRangesAdded(newIface, newRanges, newNc.getOwnerUid());
}
} catch (Exception e) {
// Never crash!

View File

@@ -23,6 +23,9 @@ import static android.Manifest.permission.NETWORK_STACK;
import static android.Manifest.permission.UPDATE_DEVICE_STATS;
import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
import static android.content.pm.PackageManager.GET_PERMISSIONS;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOCKDOWN_VPN;
import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
import static android.net.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS;
import static android.net.INetd.PERMISSION_INTERNET;
import static android.net.INetd.PERMISSION_NETWORK;
@@ -37,6 +40,7 @@ import static android.os.Process.SYSTEM_UID;
import static com.android.net.module.util.CollectionUtils.toIntArray;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -74,7 +78,6 @@ import com.android.networkstack.apishim.common.ProcessShim;
import com.android.server.BpfNetMaps;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -108,10 +111,19 @@ public class PermissionMonitor {
@GuardedBy("this")
private final SparseIntArray mUidToNetworkPerm = new SparseIntArray();
// Keys are active non-bypassable and fully-routed VPN's interface name, Values are uid ranges
// for apps under the VPN
// NonNull keys are active non-bypassable and fully-routed VPN's interface name, Values are uid
// ranges for apps under the VPNs which enable interface filtering.
// If key is null, Values are uid ranges for apps under the VPNs which are connected but do not
// enable interface filtering.
@GuardedBy("this")
private final Map<String, Set<UidRange>> mVpnUidRanges = new HashMap<>();
private final Map<String, Set<UidRange>> mVpnInterfaceUidRanges = new ArrayMap<>();
// Items are uid ranges for apps under the VPN Lockdown
// Ranges were given through ConnectivityManager#setRequireVpnForUids, and ranges are allowed to
// have duplicates. Also, it is allowed to give ranges that are already subject to lockdown.
// So we need to maintain uid range with multiset.
@GuardedBy("this")
private final MultiSet<UidRange> mVpnLockdownUidRanges = new MultiSet<>();
// A set of appIds for apps across all users on the device. We track appIds instead of uids
// directly to reduce its size and also eliminate the need to update this set when user is
@@ -201,6 +213,38 @@ public class PermissionMonitor {
}
}
private static class MultiSet<T> {
private final Map<T, Integer> mMap = new ArrayMap<>();
/**
* Returns the number of key in the set before this addition.
*/
public int add(T key) {
final int oldCount = mMap.getOrDefault(key, 0);
mMap.put(key, oldCount + 1);
return oldCount;
}
/**
* Return the number of key in the set before this removal.
*/
public int remove(T key) {
final int oldCount = mMap.getOrDefault(key, 0);
if (oldCount == 0) {
Log.wtf(TAG, "Attempt to remove non existing key = " + key.toString());
} else if (oldCount == 1) {
mMap.remove(key);
} else {
mMap.put(key, oldCount - 1);
}
return oldCount;
}
public Set<T> getSet() {
return mMap.keySet();
}
}
public PermissionMonitor(@NonNull final Context context, @NonNull final INetd netd,
@NonNull final BpfNetMaps bpfNetMaps) {
this(context, netd, bpfNetMaps, new Dependencies());
@@ -626,16 +670,26 @@ public class PermissionMonitor {
}
private synchronized void updateVpnUid(int uid, boolean add) {
for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
// Apps that can use restricted networks can always bypass VPNs.
if (hasRestrictedNetworksPermission(uid)) {
return;
}
for (Map.Entry<String, Set<UidRange>> vpn : mVpnInterfaceUidRanges.entrySet()) {
if (UidRange.containsUid(vpn.getValue(), uid)) {
final Set<Integer> changedUids = new HashSet<>();
changedUids.add(uid);
removeBypassingUids(changedUids, -1 /* vpnAppUid */);
updateVpnUidsInterfaceRules(vpn.getKey(), changedUids, add);
}
}
}
private synchronized void updateLockdownUid(int uid, boolean add) {
if (UidRange.containsUid(mVpnLockdownUidRanges.getSet(), uid)
&& !hasRestrictedNetworksPermission(uid)) {
updateLockdownUidRule(uid, add);
}
}
/**
* This handles both network and traffic permission, because there is no overlap in actual
* values, where network permission is NETWORK or SYSTEM, and traffic permission is INTERNET
@@ -729,9 +783,10 @@ public class PermissionMonitor {
// If the newly-installed package falls within some VPN's uid range, update Netd with it.
// This needs to happen after the mUidToNetworkPerm update above, since
// removeBypassingUids() in updateVpnUid() depends on mUidToNetworkPerm to check if the
// package can bypass VPN.
// hasRestrictedNetworksPermission() in updateVpnUid() and updateLockdownUid() depends on
// mUidToNetworkPerm to check if the package can bypass VPN.
updateVpnUid(uid, true /* add */);
updateLockdownUid(uid, true /* add */);
mAllApps.add(appId);
// Log package added.
@@ -775,9 +830,10 @@ public class PermissionMonitor {
// If the newly-removed package falls within some VPN's uid range, update Netd with it.
// This needs to happen before the mUidToNetworkPerm update below, since
// removeBypassingUids() in updateVpnUid() depends on mUidToNetworkPerm to check if the
// package can bypass VPN.
// hasRestrictedNetworksPermission() in updateVpnUid() and updateLockdownUid() depends on
// mUidToNetworkPerm to check if the package can bypass VPN.
updateVpnUid(uid, false /* add */);
updateLockdownUid(uid, false /* add */);
// If the package has been removed from all users on the device, clear it form mAllApps.
if (mPackageManager.getNameForUid(uid) == null) {
mAllApps.remove(appId);
@@ -859,48 +915,100 @@ public class PermissionMonitor {
/**
* Called when a new set of UID ranges are added to an active VPN network
*
* @param iface The active VPN network's interface name
* @param iface The active VPN network's interface name. Null iface indicates that the app is
* allowed to receive packets on all interfaces.
* @param rangesToAdd The new UID ranges to be added to the network
* @param vpnAppUid The uid of the VPN app
*/
public synchronized void onVpnUidRangesAdded(@NonNull String iface, Set<UidRange> rangesToAdd,
public synchronized void onVpnUidRangesAdded(@Nullable String iface, Set<UidRange> rangesToAdd,
int vpnAppUid) {
// 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.
// 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);
updateVpnUidsInterfaceRules(iface, changedUids, true /* add */);
if (mVpnUidRanges.containsKey(iface)) {
mVpnUidRanges.get(iface).addAll(rangesToAdd);
if (mVpnInterfaceUidRanges.containsKey(iface)) {
mVpnInterfaceUidRanges.get(iface).addAll(rangesToAdd);
} else {
mVpnUidRanges.put(iface, new HashSet<UidRange>(rangesToAdd));
mVpnInterfaceUidRanges.put(iface, new HashSet<UidRange>(rangesToAdd));
}
}
/**
* Called when a set of UID ranges are removed from an active VPN network
*
* @param iface The VPN network's interface name
* @param iface The VPN network's interface name. Null iface indicates that the app is allowed
* to receive packets on all interfaces.
* @param rangesToRemove Existing UID ranges to be removed from the VPN network
* @param vpnAppUid The uid of the VPN app
*/
public synchronized void onVpnUidRangesRemoved(@NonNull String iface,
public synchronized void onVpnUidRangesRemoved(@Nullable String iface,
Set<UidRange> rangesToRemove, int vpnAppUid) {
// 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);
updateVpnUidsInterfaceRules(iface, changedUids, false /* add */);
Set<UidRange> existingRanges = mVpnUidRanges.getOrDefault(iface, null);
Set<UidRange> existingRanges = mVpnInterfaceUidRanges.getOrDefault(iface, null);
if (existingRanges == null) {
loge("Attempt to remove unknown vpn uid Range iface = " + iface);
return;
}
existingRanges.removeAll(rangesToRemove);
if (existingRanges.size() == 0) {
mVpnUidRanges.remove(iface);
mVpnInterfaceUidRanges.remove(iface);
}
}
/**
* Called when UID ranges under VPN Lockdown are updated
*
* @param add {@code true} if the uids are to be added to the Lockdown, {@code false} if they
* are to be removed from the Lockdown.
* @param ranges The updated UID ranges under VPN Lockdown. This function does not treat the VPN
* app's UID in any special way. The caller is responsible for excluding the VPN
* app UID from the passed-in ranges.
* Ranges can have duplications and/or contain the range that is already subject
* to lockdown. However, ranges can not have overlaps with other ranges including
* ranges that are currently subject to lockdown.
*/
public synchronized void updateVpnLockdownUidRanges(boolean add, UidRange[] ranges) {
final Set<UidRange> affectedUidRanges = new HashSet<>();
for (final UidRange range : ranges) {
if (add) {
// Rule will be added if mVpnLockdownUidRanges does not have this uid range entry
// currently.
if (mVpnLockdownUidRanges.add(range) == 0) {
affectedUidRanges.add(range);
}
} else {
// Rule will be removed if the number of the range in the set is 1 before the
// removal.
if (mVpnLockdownUidRanges.remove(range) == 1) {
affectedUidRanges.add(range);
}
}
}
// mAllApps only contains appIds instead of uids. So the generated uid list might contain
// apps that are installed only on some users but not others. 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> affectedUids = intersectUids(affectedUidRanges, mAllApps);
// We skip adding rule to privileged apps and allow them to bypass incoming packet
// filtering. The behaviour is consistent with how lockdown works for outgoing packets, but
// the implementation is different: while ConnectivityService#setRequireVpnForUids does not
// exclude privileged apps from the prohibit routing rules used to implement outgoing packet
// filtering, privileged apps can still bypass outgoing packet filtering because the
// prohibit rules observe the protected from VPN bit.
for (final int uid: affectedUids) {
if (!hasRestrictedNetworksPermission(uid)) {
updateLockdownUidRule(uid, add);
}
}
}
@@ -939,7 +1047,7 @@ public class PermissionMonitor {
*/
private void removeBypassingUids(Set<Integer> uids, int vpnAppUid) {
uids.remove(vpnAppUid);
uids.removeIf(uid -> mUidToNetworkPerm.get(uid, PERMISSION_NONE) == PERMISSION_SYSTEM);
uids.removeIf(this::hasRestrictedNetworksPermission);
}
/**
@@ -948,6 +1056,7 @@ public class PermissionMonitor {
*
* This is to instruct netd to set up appropriate filtering rules for these uids, such that they
* can only receive ingress packets from the VPN's tunnel interface (and loopback).
* Null iface set up a wildcard rule that allow app to receive packets on all interfaces.
*
* @param iface the interface name of the active VPN connection
* @param add {@code true} if the uids are to be added to the interface, {@code false} if they
@@ -968,6 +1077,18 @@ public class PermissionMonitor {
}
}
private void updateLockdownUidRule(int uid, boolean add) {
try {
if (add) {
mBpfNetMaps.setUidRule(FIREWALL_CHAIN_LOCKDOWN_VPN, uid, FIREWALL_RULE_DENY);
} else {
mBpfNetMaps.setUidRule(FIREWALL_CHAIN_LOCKDOWN_VPN, uid, FIREWALL_RULE_ALLOW);
}
} catch (ServiceSpecificException e) {
loge("Failed to " + (add ? "add" : "remove") + " Lockdown rule: " + e);
}
}
/**
* Send the updated permission information to netd. Called upon package install/uninstall.
*
@@ -1055,8 +1176,14 @@ public class PermissionMonitor {
/** Should only be used by unit tests */
@VisibleForTesting
public Set<UidRange> getVpnUidRanges(String iface) {
return mVpnUidRanges.get(iface);
public Set<UidRange> getVpnInterfaceUidRanges(String iface) {
return mVpnInterfaceUidRanges.get(iface);
}
/** Should only be used by unit tests */
@VisibleForTesting
public Set<UidRange> getVpnLockdownUidRanges() {
return mVpnLockdownUidRanges.getSet();
}
private synchronized void onSettingChanged() {
@@ -1121,13 +1248,21 @@ public class PermissionMonitor {
public void dump(IndentingPrintWriter pw) {
pw.println("Interface filtering rules:");
pw.increaseIndent();
for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
for (Map.Entry<String, Set<UidRange>> vpn : mVpnInterfaceUidRanges.entrySet()) {
pw.println("Interface: " + vpn.getKey());
pw.println("UIDs: " + vpn.getValue().toString());
pw.println();
}
pw.decreaseIndent();
pw.println();
pw.println("Lockdown filtering rules:");
pw.increaseIndent();
for (final UidRange range : mVpnLockdownUidRanges.getSet()) {
pw.println("UIDs: " + range.toString());
}
pw.decreaseIndent();
pw.println();
pw.println("Update logs:");
pw.increaseIndent();

View File

@@ -51,6 +51,9 @@ import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO;
import static android.net.ConnectivityManager.EXTRA_NETWORK_TYPE;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOCKDOWN_VPN;
import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT;
import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE;
import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK;
@@ -9502,6 +9505,46 @@ public class ConnectivityServiceTest {
b2.expectBroadcast();
}
@Test
public void testLockdownSetFirewallUidRule() throws Exception {
// For ConnectivityService#setAlwaysOnVpnPackage.
mServiceContext.setPermission(
Manifest.permission.CONTROL_ALWAYS_ON_VPN, PERMISSION_GRANTED);
// Needed to call Vpn#setAlwaysOnPackage.
mServiceContext.setPermission(Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED);
// Needed to call Vpn#isAlwaysOnPackageSupported.
mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
// Enable Lockdown
final ArrayList<String> allowList = new ArrayList<>();
mVpnManagerService.setAlwaysOnVpnPackage(PRIMARY_USER, ALWAYS_ON_PACKAGE,
true /* lockdown */, allowList);
waitForIdle();
// Lockdown rule is set to apps uids
verify(mBpfNetMaps).setUidRule(
eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP1_UID), eq(FIREWALL_RULE_DENY));
verify(mBpfNetMaps).setUidRule(
eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP2_UID), eq(FIREWALL_RULE_DENY));
reset(mBpfNetMaps);
// Disable lockdown
mVpnManagerService.setAlwaysOnVpnPackage(PRIMARY_USER, null, false /* lockdown */,
allowList);
waitForIdle();
// Lockdown rule is removed from apps uids
verify(mBpfNetMaps).setUidRule(
eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP1_UID), eq(FIREWALL_RULE_ALLOW));
verify(mBpfNetMaps).setUidRule(
eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP2_UID), eq(FIREWALL_RULE_ALLOW));
// Interface rules are not changed by Lockdown mode enable/disable
verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
verify(mBpfNetMaps, never()).removeUidInterfaceRules(any());
}
/**
* Test mutable and requestable network capabilities such as
* {@link NetworkCapabilities#NET_CAPABILITY_TRUSTED} and
@@ -10373,7 +10416,7 @@ public class ConnectivityServiceTest {
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);
assertTrue(mService.mPermissionMonitor.getVpnUidRanges("tun0").equals(vpnRange));
assertTrue(mService.mPermissionMonitor.getVpnInterfaceUidRanges("tun0").equals(vpnRange));
mMockVpn.disconnect();
waitForIdle();
@@ -10381,11 +10424,11 @@ public class ConnectivityServiceTest {
// Disconnected VPN should have interface rules removed
verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID);
assertNull(mService.mPermissionMonitor.getVpnUidRanges("tun0"));
assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges("tun0"));
}
@Test
public void testLegacyVpnDoesNotResultInInterfaceFilteringRule() throws Exception {
public void testLegacyVpnSetInterfaceFilteringRuleWithWildcard() throws Exception {
LinkProperties lp = new LinkProperties();
lp.setInterfaceName("tun0");
lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null));
@@ -10395,13 +10438,29 @@ public class ConnectivityServiceTest {
mMockVpn.establish(lp, Process.SYSTEM_UID, vpnRange);
assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID);
// Legacy VPN should not have interface rules set up
verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
// A connected Legacy VPN should have interface rules with null interface.
// Null Interface is a wildcard and this accepts traffic from all the interfaces.
// There are two expected invocations, one during the VPN initial connection,
// one during the VPN LinkProperties update.
ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class);
verify(mBpfNetMaps, times(2)).addUidInterfaceRules(
eq(null) /* iface */, uidCaptor.capture());
assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID, VPN_UID);
assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID, VPN_UID);
assertEquals(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */),
vpnRange);
mMockVpn.disconnect();
waitForIdle();
// Disconnected VPN should have interface rules removed
verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID, VPN_UID);
assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */));
}
@Test
public void testLocalIpv4OnlyVpnDoesNotResultInInterfaceFilteringRule()
throws Exception {
public void testLocalIpv4OnlyVpnSetInterfaceFilteringRuleWithWildcard() throws Exception {
LinkProperties lp = new LinkProperties();
lp.setInterfaceName("tun0");
lp.addRoute(new RouteInfo(new IpPrefix("192.0.2.0/24"), null, "tun0"));
@@ -10412,7 +10471,25 @@ public class ConnectivityServiceTest {
assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID);
// IPv6 unreachable route should not be misinterpreted as a default route
verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
// A connected VPN should have interface rules with null interface.
// Null Interface is a wildcard and this accepts traffic from all the interfaces.
// There are two expected invocations, one during the VPN initial connection,
// one during the VPN LinkProperties update.
ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class);
verify(mBpfNetMaps, times(2)).addUidInterfaceRules(
eq(null) /* iface */, uidCaptor.capture());
assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID, VPN_UID);
assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID, VPN_UID);
assertEquals(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */),
vpnRange);
mMockVpn.disconnect();
waitForIdle();
// Disconnected VPN should have interface rules removed
verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID, VPN_UID);
assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */));
}
@Test

View File

@@ -30,6 +30,9 @@ import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_REQUIRED;
import static android.content.pm.PackageManager.GET_PERMISSIONS;
import static android.content.pm.PackageManager.MATCH_ANY_USER;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOCKDOWN_VPN;
import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
import static android.net.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS;
import static android.net.INetd.PERMISSION_INTERNET;
import static android.net.INetd.PERMISSION_NETWORK;
@@ -761,8 +764,8 @@ public class PermissionMonitorTest {
MOCK_APPID1);
}
@Test
public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdates() throws Exception {
private void doTestuidFilteringDuringVpnConnectDisconnectAndUidUpdates(@Nullable String ifName)
throws Exception {
doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
CONNECTIVITY_USE_RESTRICTED_NETWORKS),
buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
@@ -778,8 +781,8 @@ 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("tun0", vpnRange1, VPN_UID);
verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11}));
mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange1, VPN_UID);
verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
reset(mBpfNetMaps);
@@ -787,27 +790,38 @@ public class PermissionMonitorTest {
mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[]{MOCK_UID11}));
mPermissionMonitor.onPackageAdded(MOCK_PACKAGE1, MOCK_UID11);
verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11}));
verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
reset(mBpfNetMaps);
// 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("tun0", vpnRange1, VPN_UID);
mPermissionMonitor.onVpnUidRangesRemoved(ifName, vpnRange1, VPN_UID);
verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[] {MOCK_UID11}));
mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange2, VPN_UID);
verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID12}));
mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange2, VPN_UID);
verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID12}));
reset(mBpfNetMaps);
// When VPN is disconnected, expect rules to be torn down
mPermissionMonitor.onVpnUidRangesRemoved("tun0", vpnRange2, VPN_UID);
mPermissionMonitor.onVpnUidRangesRemoved(ifName, vpnRange2, VPN_UID);
verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[] {MOCK_UID12}));
assertNull(mPermissionMonitor.getVpnUidRanges("tun0"));
assertNull(mPermissionMonitor.getVpnInterfaceUidRanges(ifName));
}
@Test
public void testUidFilteringDuringPackageInstallAndUninstall() throws Exception {
public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdates() throws Exception {
doTestuidFilteringDuringVpnConnectDisconnectAndUidUpdates("tun0");
}
@Test
public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdatesWithWildcard()
throws Exception {
doTestuidFilteringDuringVpnConnectDisconnectAndUidUpdates(null /* ifName */);
}
private void doTestUidFilteringDuringPackageInstallAndUninstall(@Nullable String ifName) throws
Exception {
doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
NETWORK_STACK, CONNECTIVITY_USE_RESTRICTED_NETWORKS),
buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
@@ -818,12 +832,12 @@ public class PermissionMonitorTest {
mPermissionMonitor.startMonitoring();
final Set<UidRange> vpnRange = Set.of(UidRange.createForUser(MOCK_USER1),
UidRange.createForUser(MOCK_USER2));
mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange, VPN_UID);
mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange, VPN_UID);
// Newly-installed package should have uid rules added
addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_APPID1);
verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11}));
verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID21}));
verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID21}));
// Removed package should have its uid rules removed
mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
@@ -831,6 +845,168 @@ public class PermissionMonitorTest {
verify(mBpfNetMaps, never()).removeUidInterfaceRules(aryEq(new int[]{MOCK_UID21}));
}
@Test
public void testUidFilteringDuringPackageInstallAndUninstall() throws Exception {
doTestUidFilteringDuringPackageInstallAndUninstall("tun0");
}
@Test
public void testUidFilteringDuringPackageInstallAndUninstallWithWildcard() throws Exception {
doTestUidFilteringDuringPackageInstallAndUninstall(null /* ifName */);
}
@Test
public void testLockdownUidFilteringWithLockdownEnableDisable() {
doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
CONNECTIVITY_USE_RESTRICTED_NETWORKS),
buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
buildPackageInfo(MOCK_PACKAGE2, MOCK_UID12),
buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
.when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
mPermissionMonitor.startMonitoring();
// Every app on user 0 except MOCK_UID12 are under VPN.
final UidRange[] vpnRange1 = {
new UidRange(0, MOCK_UID12 - 1),
new UidRange(MOCK_UID12 + 1, UserHandle.PER_USER_RANGE - 1)
};
// Add Lockdown uid range, expect a rule to be set up for user app MOCK_UID11
mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange1);
verify(mBpfNetMaps)
.setUidRule(
eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
eq(FIREWALL_RULE_DENY));
assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange1));
reset(mBpfNetMaps);
// Remove Lockdown uid range, expect rules to be torn down
mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange1);
verify(mBpfNetMaps)
.setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
eq(FIREWALL_RULE_ALLOW));
assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
}
@Test
public void testLockdownUidFilteringWithLockdownEnableDisableWithMultiAdd() {
doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
CONNECTIVITY_USE_RESTRICTED_NETWORKS),
buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
.when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
mPermissionMonitor.startMonitoring();
// MOCK_UID11 is under VPN.
final UidRange range = new UidRange(MOCK_UID11, MOCK_UID11);
final UidRange[] vpnRange = {range};
// Add Lockdown uid range at 1st time, expect a rule to be set up
mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange);
verify(mBpfNetMaps)
.setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
eq(FIREWALL_RULE_DENY));
assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
reset(mBpfNetMaps);
// Add Lockdown uid range at 2nd time, expect a rule not to be set up because the uid
// already has the rule
mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange);
verify(mBpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt());
assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
reset(mBpfNetMaps);
// Remove Lockdown uid range at 1st time, expect a rule not to be torn down because we added
// the range 2 times.
mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
verify(mBpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt());
assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
reset(mBpfNetMaps);
// Remove Lockdown uid range at 2nd time, expect a rule to be torn down because we added
// twice and we removed twice.
mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
verify(mBpfNetMaps)
.setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
eq(FIREWALL_RULE_ALLOW));
assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
}
@Test
public void testLockdownUidFilteringWithLockdownEnableDisableWithDuplicates() {
doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
CONNECTIVITY_USE_RESTRICTED_NETWORKS),
buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
.when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
mPermissionMonitor.startMonitoring();
// MOCK_UID11 is under VPN.
final UidRange range = new UidRange(MOCK_UID11, MOCK_UID11);
final UidRange[] vpnRangeDuplicates = {range, range};
final UidRange[] vpnRange = {range};
// Add Lockdown uid ranges which contains duplicated uid ranges
mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRangeDuplicates);
verify(mBpfNetMaps)
.setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
eq(FIREWALL_RULE_DENY));
assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
reset(mBpfNetMaps);
// Remove Lockdown uid range at 1st time, expect a rule not to be torn down because uid
// ranges we added contains duplicated uid ranges.
mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
verify(mBpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt());
assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
reset(mBpfNetMaps);
// Remove Lockdown uid range at 2nd time, expect a rule to be torn down.
mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
verify(mBpfNetMaps)
.setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
eq(FIREWALL_RULE_ALLOW));
assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
}
@Test
public void testLockdownUidFilteringWithInstallAndUnInstall() {
doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
NETWORK_STACK, CONNECTIVITY_USE_RESTRICTED_NETWORKS),
buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
.when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
doReturn(List.of(MOCK_USER1, MOCK_USER2)).when(mUserManager).getUserHandles(eq(true));
mPermissionMonitor.startMonitoring();
final UidRange[] vpnRange = {
UidRange.createForUser(MOCK_USER1),
UidRange.createForUser(MOCK_USER2)
};
mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange);
// Installing package should add Lockdown rules
addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_APPID1);
verify(mBpfNetMaps)
.setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
eq(FIREWALL_RULE_DENY));
verify(mBpfNetMaps)
.setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID21),
eq(FIREWALL_RULE_DENY));
reset(mBpfNetMaps);
// Uninstalling package should remove Lockdown rules
mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
verify(mBpfNetMaps)
.setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
eq(FIREWALL_RULE_ALLOW));
verify(mBpfNetMaps, never())
.setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID21),
eq(FIREWALL_RULE_ALLOW));
}
// Normal package add/remove operations will trigger multiple intent for uids corresponding to
// each user. To simulate generic package operations, the onPackageAdded/Removed will need to be