Merge "Explicitly enabled Proguard compat for NearbyMultiDevicesClientsSnippets" into tm-dev
diff --git a/bpf_progs/bpf_shared.h b/bpf_progs/bpf_shared.h
index a6e78b6..9a246a6 100644
--- a/bpf_progs/bpf_shared.h
+++ b/bpf_progs/bpf_shared.h
@@ -132,6 +132,7 @@
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 fe9a871..76911f4 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -194,7 +194,7 @@
BpfConfig enabledRules = getConfig(UID_RULES_CONFIGURATION_KEY);
UidOwnerValue* uidEntry = bpf_uid_owner_map_lookup_elem(&uid);
- uint8_t uidRules = uidEntry ? uidEntry->rule : 0;
+ uint32_t uidRules = uidEntry ? uidEntry->rule : 0;
uint32_t allowed_iif = uidEntry ? uidEntry->iif : 0;
if (enabledRules) {
@@ -214,9 +214,16 @@
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 a174fe3..4ecc8a1 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -982,6 +982,16 @@
@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 @@
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 f13c68d..7b1f59c 100644
--- a/service/jni/com_android_server_BpfNetMaps.cpp
+++ b/service/jni/com_android_server_BpfNetMaps.cpp
@@ -133,12 +133,16 @@
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 3e98edb..5581c40 100644
--- a/service/native/TrafficController.cpp
+++ b/service/native/TrafficController.cpp
@@ -88,7 +88,7 @@
} \
} while (0)
-const std::string uidMatchTypeToString(uint8_t match) {
+const std::string uidMatchTypeToString(uint32_t match) {
std::string matchType;
FLAG_MSG_TRANS(matchType, HAPPY_BOX_MATCH, match);
FLAG_MSG_TRANS(matchType, PENALTY_BOX_MATCH, match);
@@ -98,6 +98,7 @@
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);
}
@@ -272,7 +273,7 @@
if (oldMatch.ok()) {
UidOwnerValue newMatch = {
.iif = (match == IIF_MATCH) ? 0 : oldMatch.value().iif,
- .rule = static_cast<uint8_t>(oldMatch.value().rule & ~match),
+ .rule = oldMatch.value().rule & ~match,
};
if (newMatch.rule == 0) {
RETURN_IF_NOT_OK(mUidOwnerMap.deleteValue(uid));
@@ -286,23 +287,20 @@
}
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,
- .rule = static_cast<uint8_t>(oldMatch.value().rule | match),
+ .iif = (match == IIF_MATCH) ? iif : oldMatch.value().iif,
+ .rule = oldMatch.value().rule | match,
};
RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
} else {
UidOwnerValue newMatch = {
.iif = iif,
- .rule = static_cast<uint8_t>(match),
+ .rule = match,
};
RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
}
@@ -335,6 +333,8 @@
return ALLOWLIST;
case LOW_POWER_STANDBY:
return ALLOWLIST;
+ case LOCKDOWN:
+ return DENYLIST;
case NONE:
default:
return DENYLIST;
@@ -360,6 +360,9 @@
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::addUidInterfaceRules(const int iif,
const std::vector<int32_t>& uidsToAdd) {
- if (!iif) {
- return statusFromErrno(EINVAL, "Interface rule must specify interface");
- }
std::lock_guard guard(mMutex);
for (auto uid : uidsToAdd) {
diff --git a/service/native/TrafficControllerTest.cpp b/service/native/TrafficControllerTest.cpp
index 9529cae..ad53cb8 100644
--- a/service/native/TrafficControllerTest.cpp
+++ b/service/native/TrafficControllerTest.cpp
@@ -307,6 +307,7 @@
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 @@
checkEachUidValue({10001, 10002}, IIF_MATCH);
}
+TEST_F(TrafficControllerTest, TestAddUidInterfaceFilteringRulesWithWildcard) {
+ // iif=0 is a wildcard
+ int iif = 0;
+ // Add interface rule with wildcard to uids
+ ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000, 1001})));
+ expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif);
+}
+
+TEST_F(TrafficControllerTest, TestRemoveUidInterfaceFilteringRulesWithWildcard) {
+ // iif=0 is a wildcard
+ int iif = 0;
+ // Add interface rule with wildcard to two uids
+ ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000, 1001})));
+ expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif);
+
+ // Remove interface rule from one of the uids
+ ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
+ expectUidOwnerMapValues({1001}, IIF_MATCH, iif);
+ checkEachUidValue({1001}, IIF_MATCH);
+
+ // Remove interface rule from the remaining uid
+ ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1001})));
+ expectMapEmpty(mFakeUidOwnerMap);
+}
+
+TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesWithWildcardAndExistingMatches) {
+ // Set up existing DOZABLE_MATCH and POWERSAVE_MATCH rule
+ ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
+ TrafficController::IptOpInsert)));
+ ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
+ TrafficController::IptOpInsert)));
+
+ // iif=0 is a wildcard
+ int iif = 0;
+ // Add interface rule with wildcard to the existing uid
+ ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000})));
+ expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif);
+
+ // Remove interface rule with wildcard from the existing uid
+ ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
+ expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH, 0);
+}
+
+TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesWithWildcardAndNewMatches) {
+ // iif=0 is a wildcard
+ int iif = 0;
+ // Set up existing interface rule with wildcard
+ ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000})));
+
+ // Add DOZABLE_MATCH and POWERSAVE_MATCH rule to the existing uid
+ ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
+ TrafficController::IptOpInsert)));
+ ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
+ TrafficController::IptOpInsert)));
+ expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif);
+
+ // Remove DOZABLE_MATCH and POWERSAVE_MATCH rule from the existing uid
+ ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
+ TrafficController::IptOpDelete)));
+ ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
+ TrafficController::IptOpDelete)));
+ expectUidOwnerMapValues({1000}, IIF_MATCH, iif);
+}
+
TEST_F(TrafficControllerTest, TestGrantInternetPermission) {
std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
diff --git a/service/native/include/Common.h b/service/native/include/Common.h
index dc44845..847acec 100644
--- a/service/native/include/Common.h
+++ b/service/native/include/Common.h
@@ -35,6 +35,7 @@
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 d3a267a..fb0a48d 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -5975,6 +5975,10 @@
+ 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,
@@ -7738,10 +7742,10 @@
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.
@@ -7754,11 +7758,19 @@
}
final Set<UidRange> ranges = nai.networkCapabilities.getUidRanges();
+ if (ranges == null || ranges.isEmpty()) {
+ return;
+ }
+
final int vpnAppUid = nai.networkCapabilities.getOwnerUid();
// TODO: this create a window of opportunity for apps to receive traffic between the time
// when the old rules are removed and the time when new rules are added. To fix this,
// make eBPF support two allowlisted interfaces so here new rules can be added before the
// old rules are being removed.
+
+ // Null iface given to onVpnUidRangesAdded/Removed is a wildcard to allow apps to receive
+ // packets on all interfaces. This is required to accept incoming traffic in Lockdown mode
+ // by overriding the Lockdown blocking rule.
if (wasFiltering) {
mPermissionMonitor.onVpnUidRangesRemoved(oldIface, ranges, vpnAppUid);
}
@@ -8047,15 +8059,14 @@
}
/**
- * 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
@@ -8064,16 +8075,32 @@
* @see INetd#firewallAddUidInterfaceRules
* @see INetd#firewallRemoveUidInterfaceRules
*/
- private boolean requiresVpnIsolation(@NonNull NetworkAgentInfo nai, NetworkCapabilities nc,
+ @Nullable
+ private String getVpnIsolationInterface(@NonNull NetworkAgentInfo nai, NetworkCapabilities nc,
LinkProperties lp) {
- if (nc == null || lp == null) return false;
- return nai.isVPN()
+ if (nc == null || lp == null) return null;
+ if (nai.isVPN()
&& !nai.networkAgentConfig.allowBypass
&& nc.getOwnerUid() != Process.SYSTEM_UID
&& lp.getInterfaceName() != null
&& (lp.hasIpv4DefaultRoute() || lp.hasIpv4UnreachableDefaultRoute())
&& (lp.hasIpv6DefaultRoute() || lp.hasIpv6UnreachableDefaultRoute())
- && !lp.hasExcludeRoute();
+ && !lp.hasExcludeRoute()) {
+ return lp.getInterfaceName();
+ }
+ return null;
+ }
+
+ /**
+ * Returns whether we need to set interface filtering rule or not
+ */
+ private boolean requiresVpnAllowRule(NetworkAgentInfo nai, LinkProperties lp,
+ String filterIface) {
+ // Only filter if lp has an interface.
+ if (lp == null || lp.getInterfaceName() == null) return false;
+ // Before T, allow rules are only needed if VPN isolation is enabled.
+ // T and After T, allow rules are needed for all VPNs.
+ return filterIface != null || (nai.isVPN() && SdkLevel.isAtLeastT());
}
private static UidRangeParcel[] toUidRangeStableParcels(final @NonNull Set<UidRange> ranges) {
@@ -8201,9 +8228,10 @@
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
@@ -8215,11 +8243,16 @@
// above, where the addition of new ranges happens before the removal of old ranges.
// TODO Fix this window by computing an accurate diff on Set<UidRange>, so the old range
// to be removed will never overlap with the new range to be added.
+
+ // Null iface given to onVpnUidRangesAdded/Removed is a wildcard to allow apps to
+ // receive packets on all interfaces. This is required to accept incoming traffic in
+ // Lockdown mode by overriding the Lockdown blocking rule.
if (wasFiltering && !prevRanges.isEmpty()) {
- mPermissionMonitor.onVpnUidRangesRemoved(iface, prevRanges, prevNc.getOwnerUid());
+ mPermissionMonitor.onVpnUidRangesRemoved(oldIface, prevRanges,
+ prevNc.getOwnerUid());
}
if (shouldFilter && !newRanges.isEmpty()) {
- mPermissionMonitor.onVpnUidRangesAdded(iface, newRanges, newNc.getOwnerUid());
+ mPermissionMonitor.onVpnUidRangesAdded(newIface, newRanges, newNc.getOwnerUid());
}
} catch (Exception e) {
// Never crash!
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java
index 8d99cb4..e4a2c20 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.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 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.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 @@
@GuardedBy("this")
private final SparseIntArray mUidToNetworkPerm = new SparseIntArray();
- // Keys are active non-bypassable and fully-routed VPN's interface name, Values are uid ranges
- // for apps under the VPN
+ // NonNull keys are active non-bypassable and fully-routed VPN's interface name, Values are uid
+ // ranges for apps under the VPNs which enable interface filtering.
+ // If key is null, Values are uid ranges for apps under the VPNs which are connected but do not
+ // enable interface filtering.
@GuardedBy("this")
- private final Map<String, Set<UidRange>> mVpnUidRanges = new HashMap<>();
+ private final Map<String, Set<UidRange>> mVpnInterfaceUidRanges = new ArrayMap<>();
+
+ // Items are uid ranges for apps under the VPN Lockdown
+ // Ranges were given through ConnectivityManager#setRequireVpnForUids, and ranges are allowed to
+ // have duplicates. Also, it is allowed to give ranges that are already subject to lockdown.
+ // So we need to maintain uid range with multiset.
+ @GuardedBy("this")
+ private final MultiSet<UidRange> mVpnLockdownUidRanges = new MultiSet<>();
// A set of appIds for apps across all users on the device. We track appIds instead of uids
// directly to reduce its size and also eliminate the need to update this set when user is
@@ -201,6 +213,38 @@
}
}
+ private static class MultiSet<T> {
+ private final Map<T, Integer> mMap = new ArrayMap<>();
+
+ /**
+ * Returns the number of key in the set before this addition.
+ */
+ public int add(T key) {
+ final int oldCount = mMap.getOrDefault(key, 0);
+ mMap.put(key, oldCount + 1);
+ return oldCount;
+ }
+
+ /**
+ * Return the number of key in the set before this removal.
+ */
+ public int remove(T key) {
+ final int oldCount = mMap.getOrDefault(key, 0);
+ if (oldCount == 0) {
+ Log.wtf(TAG, "Attempt to remove non existing key = " + key.toString());
+ } else if (oldCount == 1) {
+ mMap.remove(key);
+ } else {
+ mMap.put(key, oldCount - 1);
+ }
+ return oldCount;
+ }
+
+ public Set<T> getSet() {
+ return mMap.keySet();
+ }
+ }
+
public PermissionMonitor(@NonNull final Context context, @NonNull final INetd netd,
@NonNull final BpfNetMaps bpfNetMaps) {
this(context, netd, bpfNetMaps, new Dependencies());
@@ -626,16 +670,26 @@
}
private synchronized void updateVpnUid(int uid, boolean add) {
- for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
+ // Apps that can use restricted networks can always bypass VPNs.
+ if (hasRestrictedNetworksPermission(uid)) {
+ return;
+ }
+ for (Map.Entry<String, Set<UidRange>> vpn : mVpnInterfaceUidRanges.entrySet()) {
if (UidRange.containsUid(vpn.getValue(), uid)) {
final Set<Integer> changedUids = new HashSet<>();
changedUids.add(uid);
- removeBypassingUids(changedUids, -1 /* vpnAppUid */);
updateVpnUidsInterfaceRules(vpn.getKey(), changedUids, add);
}
}
}
+ private synchronized void updateLockdownUid(int uid, boolean add) {
+ if (UidRange.containsUid(mVpnLockdownUidRanges.getSet(), uid)
+ && !hasRestrictedNetworksPermission(uid)) {
+ updateLockdownUidRule(uid, add);
+ }
+ }
+
/**
* This handles both network and traffic permission, because there is no overlap in actual
* values, where network permission is NETWORK or SYSTEM, and traffic permission is INTERNET
@@ -729,9 +783,10 @@
// 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 @@
// 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 @@
/**
* Called when a new set of UID ranges are added to an active VPN network
*
- * @param iface The active VPN network's interface name
+ * @param iface The active VPN network's interface name. Null iface indicates that the app is
+ * allowed to receive packets on all interfaces.
* @param rangesToAdd The new UID ranges to be added to the network
* @param vpnAppUid The uid of the VPN app
*/
- public synchronized void onVpnUidRangesAdded(@NonNull String iface, Set<UidRange> rangesToAdd,
+ public synchronized void onVpnUidRangesAdded(@Nullable String iface, Set<UidRange> rangesToAdd,
int vpnAppUid) {
// Calculate the list of new app uids under the VPN due to the new UID ranges and update
// Netd about them. Because mAllApps only contains appIds instead of uids, the result might
// be an overestimation if an app is not installed on the user on which the VPN is running,
- // but that's safe.
+ // but that's safe: if an app is not installed, it cannot receive any packets, so dropping
+ // packets to that UID is fine.
final Set<Integer> changedUids = intersectUids(rangesToAdd, mAllApps);
removeBypassingUids(changedUids, vpnAppUid);
updateVpnUidsInterfaceRules(iface, changedUids, true /* add */);
- if (mVpnUidRanges.containsKey(iface)) {
- mVpnUidRanges.get(iface).addAll(rangesToAdd);
+ if (mVpnInterfaceUidRanges.containsKey(iface)) {
+ mVpnInterfaceUidRanges.get(iface).addAll(rangesToAdd);
} else {
- mVpnUidRanges.put(iface, new HashSet<UidRange>(rangesToAdd));
+ mVpnInterfaceUidRanges.put(iface, new HashSet<UidRange>(rangesToAdd));
}
}
/**
* Called when a set of UID ranges are removed from an active VPN network
*
- * @param iface The VPN network's interface name
+ * @param iface The VPN network's interface name. Null iface indicates that the app is allowed
+ * to receive packets on all interfaces.
* @param rangesToRemove Existing UID ranges to be removed from the VPN network
* @param vpnAppUid The uid of the VPN app
*/
- public synchronized void onVpnUidRangesRemoved(@NonNull String iface,
+ public synchronized void onVpnUidRangesRemoved(@Nullable String iface,
Set<UidRange> rangesToRemove, int vpnAppUid) {
// Calculate the list of app uids that are no longer under the VPN due to the removed UID
// ranges and update Netd about them.
final Set<Integer> changedUids = intersectUids(rangesToRemove, mAllApps);
removeBypassingUids(changedUids, vpnAppUid);
updateVpnUidsInterfaceRules(iface, changedUids, false /* add */);
- Set<UidRange> existingRanges = mVpnUidRanges.getOrDefault(iface, null);
+ Set<UidRange> existingRanges = mVpnInterfaceUidRanges.getOrDefault(iface, null);
if (existingRanges == null) {
loge("Attempt to remove unknown vpn uid Range iface = " + iface);
return;
}
existingRanges.removeAll(rangesToRemove);
if (existingRanges.size() == 0) {
- mVpnUidRanges.remove(iface);
+ mVpnInterfaceUidRanges.remove(iface);
+ }
+ }
+
+ /**
+ * Called when UID ranges under VPN Lockdown are updated
+ *
+ * @param add {@code true} if the uids are to be added to the Lockdown, {@code false} if they
+ * are to be removed from the Lockdown.
+ * @param ranges The updated UID ranges under VPN Lockdown. This function does not treat the VPN
+ * app's UID in any special way. The caller is responsible for excluding the VPN
+ * app UID from the passed-in ranges.
+ * Ranges can have duplications and/or contain the range that is already subject
+ * to lockdown. However, ranges can not have overlaps with other ranges including
+ * ranges that are currently subject to lockdown.
+ */
+ public synchronized void updateVpnLockdownUidRanges(boolean add, UidRange[] ranges) {
+ final Set<UidRange> affectedUidRanges = new HashSet<>();
+
+ for (final UidRange range : ranges) {
+ if (add) {
+ // Rule will be added if mVpnLockdownUidRanges does not have this uid range entry
+ // currently.
+ if (mVpnLockdownUidRanges.add(range) == 0) {
+ affectedUidRanges.add(range);
+ }
+ } else {
+ // Rule will be removed if the number of the range in the set is 1 before the
+ // removal.
+ if (mVpnLockdownUidRanges.remove(range) == 1) {
+ affectedUidRanges.add(range);
+ }
+ }
+ }
+
+ // mAllApps only contains appIds instead of uids. So the generated uid list might contain
+ // apps that are installed only on some users but not others. But that's safe: if an app is
+ // not installed, it cannot receive any packets, so dropping packets to that UID is fine.
+ final Set<Integer> affectedUids = intersectUids(affectedUidRanges, mAllApps);
+
+ // We skip adding rule to privileged apps and allow them to bypass incoming packet
+ // filtering. The behaviour is consistent with how lockdown works for outgoing packets, but
+ // the implementation is different: while ConnectivityService#setRequireVpnForUids does not
+ // exclude privileged apps from the prohibit routing rules used to implement outgoing packet
+ // filtering, privileged apps can still bypass outgoing packet filtering because the
+ // prohibit rules observe the protected from VPN bit.
+ for (final int uid: affectedUids) {
+ if (!hasRestrictedNetworksPermission(uid)) {
+ updateLockdownUidRule(uid, add);
+ }
}
}
@@ -939,7 +1047,7 @@
*/
private void removeBypassingUids(Set<Integer> uids, int vpnAppUid) {
uids.remove(vpnAppUid);
- uids.removeIf(uid -> mUidToNetworkPerm.get(uid, PERMISSION_NONE) == PERMISSION_SYSTEM);
+ uids.removeIf(this::hasRestrictedNetworksPermission);
}
/**
@@ -948,6 +1056,7 @@
*
* 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 @@
}
}
+ 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 @@
/** Should only be used by unit tests */
@VisibleForTesting
- public Set<UidRange> getVpnUidRanges(String iface) {
- return mVpnUidRanges.get(iface);
+ public Set<UidRange> getVpnInterfaceUidRanges(String iface) {
+ return mVpnInterfaceUidRanges.get(iface);
+ }
+
+ /** Should only be used by unit tests */
+ @VisibleForTesting
+ public Set<UidRange> getVpnLockdownUidRanges() {
+ return mVpnLockdownUidRanges.getSet();
}
private synchronized void onSettingChanged() {
@@ -1121,7 +1248,7 @@
public void dump(IndentingPrintWriter pw) {
pw.println("Interface filtering rules:");
pw.increaseIndent();
- for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
+ for (Map.Entry<String, Set<UidRange>> vpn : mVpnInterfaceUidRanges.entrySet()) {
pw.println("Interface: " + vpn.getKey());
pw.println("UIDs: " + vpn.getValue().toString());
pw.println();
@@ -1129,6 +1256,14 @@
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();
mPermissionUpdateLogs.reverseDump(pw);
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index b7da17b..9961978 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.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 @@
b2.expectBroadcast();
}
+ @Test
+ public void testLockdownSetFirewallUidRule() throws Exception {
+ // For ConnectivityService#setAlwaysOnVpnPackage.
+ mServiceContext.setPermission(
+ Manifest.permission.CONTROL_ALWAYS_ON_VPN, PERMISSION_GRANTED);
+ // Needed to call Vpn#setAlwaysOnPackage.
+ mServiceContext.setPermission(Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED);
+ // Needed to call Vpn#isAlwaysOnPackageSupported.
+ mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
+
+ // Enable Lockdown
+ final ArrayList<String> allowList = new ArrayList<>();
+ mVpnManagerService.setAlwaysOnVpnPackage(PRIMARY_USER, ALWAYS_ON_PACKAGE,
+ true /* lockdown */, allowList);
+ waitForIdle();
+
+ // Lockdown rule is set to apps uids
+ verify(mBpfNetMaps).setUidRule(
+ eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP1_UID), eq(FIREWALL_RULE_DENY));
+ verify(mBpfNetMaps).setUidRule(
+ eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP2_UID), eq(FIREWALL_RULE_DENY));
+
+ reset(mBpfNetMaps);
+
+ // Disable lockdown
+ mVpnManagerService.setAlwaysOnVpnPackage(PRIMARY_USER, null, false /* lockdown */,
+ allowList);
+ waitForIdle();
+
+ // Lockdown rule is removed from apps uids
+ verify(mBpfNetMaps).setUidRule(
+ eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP1_UID), eq(FIREWALL_RULE_ALLOW));
+ verify(mBpfNetMaps).setUidRule(
+ eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP2_UID), eq(FIREWALL_RULE_ALLOW));
+
+ // Interface rules are not changed by Lockdown mode enable/disable
+ verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+ verify(mBpfNetMaps, never()).removeUidInterfaceRules(any());
+ }
+
/**
* Test mutable and requestable network capabilities such as
* {@link NetworkCapabilities#NET_CAPABILITY_TRUSTED} and
@@ -10373,7 +10416,7 @@
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 @@
// 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 @@
mMockVpn.establish(lp, Process.SYSTEM_UID, vpnRange);
assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID);
- // Legacy VPN should not have interface rules set up
- verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+ // A connected Legacy VPN should have interface rules with null interface.
+ // Null Interface is a wildcard and this accepts traffic from all the interfaces.
+ // There are two expected invocations, one during the VPN initial connection,
+ // one during the VPN LinkProperties update.
+ ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class);
+ verify(mBpfNetMaps, times(2)).addUidInterfaceRules(
+ eq(null) /* iface */, uidCaptor.capture());
+ assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID, VPN_UID);
+ assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID, VPN_UID);
+ assertEquals(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */),
+ vpnRange);
+
+ mMockVpn.disconnect();
+ waitForIdle();
+
+ // Disconnected VPN should have interface rules removed
+ verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
+ assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID, VPN_UID);
+ assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */));
}
@Test
- public void testLocalIpv4OnlyVpnDoesNotResultInInterfaceFilteringRule()
- throws Exception {
+ public void testLocalIpv4OnlyVpnSetInterfaceFilteringRuleWithWildcard() throws Exception {
LinkProperties lp = new LinkProperties();
lp.setInterfaceName("tun0");
lp.addRoute(new RouteInfo(new IpPrefix("192.0.2.0/24"), null, "tun0"));
@@ -10412,7 +10471,25 @@
assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID);
// IPv6 unreachable route should not be misinterpreted as a default route
- verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
+ // A connected VPN should have interface rules with null interface.
+ // Null Interface is a wildcard and this accepts traffic from all the interfaces.
+ // There are two expected invocations, one during the VPN initial connection,
+ // one during the VPN LinkProperties update.
+ ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class);
+ verify(mBpfNetMaps, times(2)).addUidInterfaceRules(
+ eq(null) /* iface */, uidCaptor.capture());
+ assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID, VPN_UID);
+ assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID, VPN_UID);
+ assertEquals(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */),
+ vpnRange);
+
+ mMockVpn.disconnect();
+ waitForIdle();
+
+ // Disconnected VPN should have interface rules removed
+ verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
+ assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID, VPN_UID);
+ assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */));
}
@Test
diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
index fb821c3..ecd17ba 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_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 @@
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 @@
final Set<UidRange> vpnRange2 = Set.of(new UidRange(MOCK_UID12, MOCK_UID12));
// When VPN is connected, expect a rule to be set up for user app MOCK_UID11
- mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange1, VPN_UID);
- verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11}));
+ mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange1, VPN_UID);
+ verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
reset(mBpfNetMaps);
@@ -787,27 +790,38 @@
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 @@
mPermissionMonitor.startMonitoring();
final Set<UidRange> vpnRange = Set.of(UidRange.createForUser(MOCK_USER1),
UidRange.createForUser(MOCK_USER2));
- mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange, VPN_UID);
+ mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange, VPN_UID);
// Newly-installed package should have uid rules added
addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_APPID1);
- verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11}));
- verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID21}));
+ verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
+ verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID21}));
// Removed package should have its uid rules removed
mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
@@ -831,6 +845,168 @@
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