From 966ff7f82ae3f387ea6bd4be3c75b08ff76656f9 Mon Sep 17 00:00:00 2001 From: Motomu Utsumi Date: Wed, 11 May 2022 05:56:26 +0000 Subject: [PATCH] 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 b08654ca0450d021da709a762ab509a8d4f87d40) Merged-In: If52ece613c8aac1073355e43b6fb9cb3fcc87d1d --- bpf_progs/bpf_shared.h | 1 + bpf_progs/netd.c | 13 +- .../src/android/net/ConnectivityManager.java | 13 +- service/jni/com_android_server_BpfNetMaps.cpp | 14 +- service/native/TrafficController.cpp | 16 +- service/native/TrafficControllerTest.cpp | 65 ++++++ service/native/include/Common.h | 1 + .../android/server/ConnectivityService.java | 34 ++- .../connectivity/PermissionMonitor.java | 183 +++++++++++++--- .../server/ConnectivityServiceTest.java | 93 +++++++- .../connectivity/PermissionMonitorTest.java | 204 ++++++++++++++++-- 11 files changed, 567 insertions(+), 70 deletions(-) diff --git a/bpf_progs/bpf_shared.h b/bpf_progs/bpf_shared.h index a6e78b67d9..9a246a6a5c 100644 --- a/bpf_progs/bpf_shared.h +++ b/bpf_progs/bpf_shared.h @@ -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 { diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c index f3f675fb87..76911f4d33 100644 --- a/bpf_progs/netd.c +++ b/bpf_progs/netd.c @@ -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; } } diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java index a174fe34ce..4ecc8a127c 100644 --- a/framework/src/android/net/ConnectivityManager.java +++ b/framework/src/android/net/ConnectivityManager.java @@ -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) diff --git a/service/jni/com_android_server_BpfNetMaps.cpp b/service/jni/com_android_server_BpfNetMaps.cpp index f13c68d310..7b1f59ca22 100644 --- a/service/jni/com_android_server_BpfNetMaps.cpp +++ b/service/jni/com_android_server_BpfNetMaps.cpp @@ -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) { diff --git a/service/native/TrafficController.cpp b/service/native/TrafficController.cpp index 473c9e38e6..5581c40924 100644 --- a/service/native/TrafficController.cpp +++ b/service/native/TrafficController.cpp @@ -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& uidsToAdd) { - if (!iif) { - return statusFromErrno(EINVAL, "Interface rule must specify interface"); - } std::lock_guard guard(mMutex); for (auto uid : uidsToAdd) { diff --git a/service/native/TrafficControllerTest.cpp b/service/native/TrafficControllerTest.cpp index 9529caee48..ad53cb88bc 100644 --- a/service/native/TrafficControllerTest.cpp +++ b/service/native/TrafficControllerTest.cpp @@ -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 appUids = {TEST_UID, TEST_UID2, TEST_UID3}; diff --git a/service/native/include/Common.h b/service/native/include/Common.h index dc448450a6..847acece50 100644 --- a/service/native/include/Common.h +++ b/service/native/include/Common.h @@ -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) diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java index d17ff0fe02..5bac1a1532 100644 --- a/service/src/com/android/server/ConnectivityService.java +++ b/service/src/com/android/server/ConnectivityService.java @@ -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 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 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, 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()); diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java index 8d99cb477a..e4a2c20bd5 100755 --- a/service/src/com/android/server/connectivity/PermissionMonitor.java +++ b/service/src/com/android/server/connectivity/PermissionMonitor.java @@ -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> mVpnUidRanges = new HashMap<>(); + private final Map> 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 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 { + private final Map 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 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> vpn : mVpnUidRanges.entrySet()) { + // Apps that can use restricted networks can always bypass VPNs. + if (hasRestrictedNetworksPermission(uid)) { + return; + } + for (Map.Entry> vpn : mVpnInterfaceUidRanges.entrySet()) { if (UidRange.containsUid(vpn.getValue(), uid)) { final Set 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 rangesToAdd, + public synchronized void onVpnUidRangesAdded(@Nullable String iface, Set 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 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(rangesToAdd)); + mVpnInterfaceUidRanges.put(iface, new HashSet(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 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 changedUids = intersectUids(rangesToRemove, mAllApps); removeBypassingUids(changedUids, vpnAppUid); updateVpnUidsInterfaceRules(iface, changedUids, false /* add */); - Set existingRanges = mVpnUidRanges.getOrDefault(iface, null); + Set 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 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 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 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 getVpnUidRanges(String iface) { - return mVpnUidRanges.get(iface); + public Set getVpnInterfaceUidRanges(String iface) { + return mVpnInterfaceUidRanges.get(iface); + } + + /** Should only be used by unit tests */ + @VisibleForTesting + public Set 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> vpn : mVpnUidRanges.entrySet()) { + for (Map.Entry> 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(); diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java index b7da17befa..9961978da3 100644 --- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java +++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java @@ -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 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 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 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 diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java index fb821c3150..ecd17bab69 100644 --- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java +++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java @@ -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 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 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