Block incoming packets in VPN Lockdown mode.

Currently, even when VPN Lockdown mode is enabled, incoming packets are
not dropped if VPN is not connected.

This commit fixed this issue.
After this commit, If VPN Lockdown mode is enabled, incoming packets
are dropped regardless of the VPN connectivity.

Bug: 206482423
Test: atest TrafficControllerTest ConnectivityServiceTest PermissionMonitorTest
Change-Id: If52ece613c8aac1073355e43b6fb9cb3fcc87d1d
(cherry picked from commit b08654ca04)
Merged-In: If52ece613c8aac1073355e43b6fb9cb3fcc87d1d
This commit is contained in:
Motomu Utsumi
2022-05-11 05:56:26 +00:00
parent 6b21340fb3
commit 966ff7f82a
11 changed files with 567 additions and 70 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) {
// 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);
if (ifNameUtf8.c_str() == nullptr) {
return -EINVAL;
}
const std::string interfaceName(ifNameUtf8.c_str());
const int ifIndex = if_nametoindex(interfaceName.c_str());
ifIndex = if_nametoindex(interfaceName.c_str());
} else {
ifIndex = 0;
}
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

@@ -5974,6 +5974,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,
@@ -7739,8 +7743,8 @@ public class ConnectivityService extends IConnectivityManager.Stub
NetworkAgentInfo nai) {
final String oldIface = getVpnIsolationInterface(nai, nai.networkCapabilities, oldLp);
final String newIface = getVpnIsolationInterface(nai, nai.networkCapabilities, newLp);
final boolean wasFiltering = requiresVpnAllowRule(oldIface);
final boolean needsFiltering = requiresVpnAllowRule(newIface);
final boolean wasFiltering = requiresVpnAllowRule(nai, oldLp, oldIface);
final boolean needsFiltering = requiresVpnAllowRule(nai, newLp, newIface);
if (!wasFiltering && !needsFiltering) {
// Nothing to do.
@@ -7753,11 +7757,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);
}
@@ -8081,9 +8093,13 @@ public class ConnectivityService extends IConnectivityManager.Stub
/**
* Returns whether we need to set interface filtering rule or not
*/
private boolean requiresVpnAllowRule(String filterIface) {
// Allow rules are only needed if VPN isolation is enabled.
return filterIface != null;
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) {
@@ -8213,8 +8229,8 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
final String oldIface = getVpnIsolationInterface(nai, prevNc, nai.linkProperties);
final String newIface = getVpnIsolationInterface(nai, newNc, nai.linkProperties);
final boolean wasFiltering = requiresVpnAllowRule(oldIface);
final boolean shouldFilter = requiresVpnAllowRule(newIface);
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
@@ -8226,6 +8242,10 @@ 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(oldIface, prevRanges,
prevNc.getOwnerUid());

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

@@ -52,6 +52,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