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