Merge changes If52ece61,Iedf344f6 am: f6131c483b
Original change: https://android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/2095365 Change-Id: Ib621c73e18a33bcf2464588f07123ed969784898 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -98,6 +98,7 @@ const std::string uidMatchTypeToString(uint32_t match) {
|
||||
FLAG_MSG_TRANS(matchType, RESTRICTED_MATCH, match);
|
||||
FLAG_MSG_TRANS(matchType, LOW_POWER_STANDBY_MATCH, match);
|
||||
FLAG_MSG_TRANS(matchType, IIF_MATCH, match);
|
||||
FLAG_MSG_TRANS(matchType, LOCKDOWN_VPN_MATCH, match);
|
||||
if (match) {
|
||||
return StringPrintf("Unknown match: %u", match);
|
||||
}
|
||||
@@ -286,16 +287,13 @@ Status TrafficController::removeRule(uint32_t uid, UidOwnerMatchType match) {
|
||||
}
|
||||
|
||||
Status TrafficController::addRule(uint32_t uid, UidOwnerMatchType match, uint32_t iif) {
|
||||
// iif should be non-zero if and only if match == MATCH_IIF
|
||||
if (match == IIF_MATCH && iif == 0) {
|
||||
return statusFromErrno(EINVAL, "Interface match must have nonzero interface index");
|
||||
} else if (match != IIF_MATCH && iif != 0) {
|
||||
if (match != IIF_MATCH && iif != 0) {
|
||||
return statusFromErrno(EINVAL, "Non-interface match must have zero interface index");
|
||||
}
|
||||
auto oldMatch = mUidOwnerMap.readValue(uid);
|
||||
if (oldMatch.ok()) {
|
||||
UidOwnerValue newMatch = {
|
||||
.iif = iif ? iif : oldMatch.value().iif,
|
||||
.iif = (match == IIF_MATCH) ? iif : oldMatch.value().iif,
|
||||
.rule = oldMatch.value().rule | match,
|
||||
};
|
||||
RETURN_IF_NOT_OK(mUidOwnerMap.writeValue(uid, newMatch, BPF_ANY));
|
||||
@@ -335,6 +333,8 @@ FirewallType TrafficController::getFirewallType(ChildChain chain) {
|
||||
return ALLOWLIST;
|
||||
case LOW_POWER_STANDBY:
|
||||
return ALLOWLIST;
|
||||
case LOCKDOWN:
|
||||
return DENYLIST;
|
||||
case NONE:
|
||||
default:
|
||||
return DENYLIST;
|
||||
@@ -360,6 +360,9 @@ int TrafficController::changeUidOwnerRule(ChildChain chain, uid_t uid, FirewallR
|
||||
case LOW_POWER_STANDBY:
|
||||
res = updateOwnerMapEntry(LOW_POWER_STANDBY_MATCH, uid, rule, type);
|
||||
break;
|
||||
case LOCKDOWN:
|
||||
res = updateOwnerMapEntry(LOCKDOWN_VPN_MATCH, uid, rule, type);
|
||||
break;
|
||||
case NONE:
|
||||
default:
|
||||
ALOGW("Unknown child chain: %d", chain);
|
||||
@@ -399,9 +402,6 @@ Status TrafficController::replaceRulesInMap(const UidOwnerMatchType match,
|
||||
|
||||
Status TrafficController::addUidInterfaceRules(const int iif,
|
||||
const std::vector<int32_t>& uidsToAdd) {
|
||||
if (!iif) {
|
||||
return statusFromErrno(EINVAL, "Interface rule must specify interface");
|
||||
}
|
||||
std::lock_guard guard(mMutex);
|
||||
|
||||
for (auto uid : uidsToAdd) {
|
||||
|
||||
@@ -307,6 +307,7 @@ TEST_F(TrafficControllerTest, TestChangeUidOwnerRule) {
|
||||
checkUidOwnerRuleForChain(POWERSAVE, POWERSAVE_MATCH);
|
||||
checkUidOwnerRuleForChain(RESTRICTED, RESTRICTED_MATCH);
|
||||
checkUidOwnerRuleForChain(LOW_POWER_STANDBY, LOW_POWER_STANDBY_MATCH);
|
||||
checkUidOwnerRuleForChain(LOCKDOWN, LOCKDOWN_VPN_MATCH);
|
||||
ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(NONE, TEST_UID, ALLOW, ALLOWLIST));
|
||||
ASSERT_EQ(-EINVAL, mTc.changeUidOwnerRule(INVALID_CHAIN, TEST_UID, ALLOW, ALLOWLIST));
|
||||
}
|
||||
@@ -491,6 +492,70 @@ TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesCoexistWithNewMatche
|
||||
checkEachUidValue({10001, 10002}, IIF_MATCH);
|
||||
}
|
||||
|
||||
TEST_F(TrafficControllerTest, TestAddUidInterfaceFilteringRulesWithWildcard) {
|
||||
// iif=0 is a wildcard
|
||||
int iif = 0;
|
||||
// Add interface rule with wildcard to uids
|
||||
ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000, 1001})));
|
||||
expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif);
|
||||
}
|
||||
|
||||
TEST_F(TrafficControllerTest, TestRemoveUidInterfaceFilteringRulesWithWildcard) {
|
||||
// iif=0 is a wildcard
|
||||
int iif = 0;
|
||||
// Add interface rule with wildcard to two uids
|
||||
ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000, 1001})));
|
||||
expectUidOwnerMapValues({1000, 1001}, IIF_MATCH, iif);
|
||||
|
||||
// Remove interface rule from one of the uids
|
||||
ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
|
||||
expectUidOwnerMapValues({1001}, IIF_MATCH, iif);
|
||||
checkEachUidValue({1001}, IIF_MATCH);
|
||||
|
||||
// Remove interface rule from the remaining uid
|
||||
ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1001})));
|
||||
expectMapEmpty(mFakeUidOwnerMap);
|
||||
}
|
||||
|
||||
TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesWithWildcardAndExistingMatches) {
|
||||
// Set up existing DOZABLE_MATCH and POWERSAVE_MATCH rule
|
||||
ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
|
||||
TrafficController::IptOpInsert)));
|
||||
ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
|
||||
TrafficController::IptOpInsert)));
|
||||
|
||||
// iif=0 is a wildcard
|
||||
int iif = 0;
|
||||
// Add interface rule with wildcard to the existing uid
|
||||
ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000})));
|
||||
expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif);
|
||||
|
||||
// Remove interface rule with wildcard from the existing uid
|
||||
ASSERT_TRUE(isOk(mTc.removeUidInterfaceRules({1000})));
|
||||
expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH, 0);
|
||||
}
|
||||
|
||||
TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesWithWildcardAndNewMatches) {
|
||||
// iif=0 is a wildcard
|
||||
int iif = 0;
|
||||
// Set up existing interface rule with wildcard
|
||||
ASSERT_TRUE(isOk(mTc.addUidInterfaceRules(iif, {1000})));
|
||||
|
||||
// Add DOZABLE_MATCH and POWERSAVE_MATCH rule to the existing uid
|
||||
ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
|
||||
TrafficController::IptOpInsert)));
|
||||
ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
|
||||
TrafficController::IptOpInsert)));
|
||||
expectUidOwnerMapValues({1000}, POWERSAVE_MATCH | DOZABLE_MATCH | IIF_MATCH, iif);
|
||||
|
||||
// Remove DOZABLE_MATCH and POWERSAVE_MATCH rule from the existing uid
|
||||
ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, DOZABLE_MATCH,
|
||||
TrafficController::IptOpDelete)));
|
||||
ASSERT_TRUE(isOk(updateUidOwnerMaps({1000}, POWERSAVE_MATCH,
|
||||
TrafficController::IptOpDelete)));
|
||||
expectUidOwnerMapValues({1000}, IIF_MATCH, iif);
|
||||
}
|
||||
|
||||
TEST_F(TrafficControllerTest, TestGrantInternetPermission) {
|
||||
std::vector<uid_t> appUids = {TEST_UID, TEST_UID2, TEST_UID3};
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -5969,6 +5969,10 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
||||
+ Arrays.toString(ranges) + "): netd command failed: " + e);
|
||||
}
|
||||
|
||||
if (SdkLevel.isAtLeastT()) {
|
||||
mPermissionMonitor.updateVpnLockdownUidRanges(requireVpn, ranges);
|
||||
}
|
||||
|
||||
for (final NetworkAgentInfo nai : mNetworkAgentInfos) {
|
||||
final boolean curMetered = nai.networkCapabilities.isMetered();
|
||||
maybeNotifyNetworkBlocked(nai, curMetered, curMetered,
|
||||
@@ -7732,10 +7736,10 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
||||
|
||||
private void updateVpnFiltering(LinkProperties newLp, LinkProperties oldLp,
|
||||
NetworkAgentInfo nai) {
|
||||
final String oldIface = oldLp != null ? oldLp.getInterfaceName() : null;
|
||||
final String newIface = newLp != null ? newLp.getInterfaceName() : null;
|
||||
final boolean wasFiltering = requiresVpnIsolation(nai, nai.networkCapabilities, oldLp);
|
||||
final boolean needsFiltering = requiresVpnIsolation(nai, nai.networkCapabilities, newLp);
|
||||
final String oldIface = getVpnIsolationInterface(nai, nai.networkCapabilities, oldLp);
|
||||
final String newIface = getVpnIsolationInterface(nai, nai.networkCapabilities, newLp);
|
||||
final boolean wasFiltering = requiresVpnAllowRule(nai, oldLp, oldIface);
|
||||
final boolean needsFiltering = requiresVpnAllowRule(nai, newLp, newIface);
|
||||
|
||||
if (!wasFiltering && !needsFiltering) {
|
||||
// Nothing to do.
|
||||
@@ -7748,11 +7752,19 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
||||
}
|
||||
|
||||
final Set<UidRange> ranges = nai.networkCapabilities.getUidRanges();
|
||||
if (ranges == null || ranges.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int vpnAppUid = nai.networkCapabilities.getOwnerUid();
|
||||
// TODO: this create a window of opportunity for apps to receive traffic between the time
|
||||
// when the old rules are removed and the time when new rules are added. To fix this,
|
||||
// make eBPF support two allowlisted interfaces so here new rules can be added before the
|
||||
// old rules are being removed.
|
||||
|
||||
// Null iface given to onVpnUidRangesAdded/Removed is a wildcard to allow apps to receive
|
||||
// packets on all interfaces. This is required to accept incoming traffic in Lockdown mode
|
||||
// by overriding the Lockdown blocking rule.
|
||||
if (wasFiltering) {
|
||||
mPermissionMonitor.onVpnUidRangesRemoved(oldIface, ranges, vpnAppUid);
|
||||
}
|
||||
@@ -8041,15 +8053,14 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether VPN isolation (ingress interface filtering) should be applied on the given
|
||||
* network.
|
||||
* Returns the interface which requires VPN isolation (ingress interface filtering).
|
||||
*
|
||||
* Ingress interface filtering enforces that all apps under the given network can only receive
|
||||
* packets from the network's interface (and loopback). This is important for VPNs because
|
||||
* apps that cannot bypass a fully-routed VPN shouldn't be able to receive packets from any
|
||||
* non-VPN interfaces.
|
||||
*
|
||||
* As a result, this method should return true iff
|
||||
* As a result, this method should return Non-null interface iff
|
||||
* 1. the network is an app VPN (not legacy VPN)
|
||||
* 2. the VPN does not allow bypass
|
||||
* 3. the VPN is fully-routed
|
||||
@@ -8058,16 +8069,32 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
||||
* @see INetd#firewallAddUidInterfaceRules
|
||||
* @see INetd#firewallRemoveUidInterfaceRules
|
||||
*/
|
||||
private boolean requiresVpnIsolation(@NonNull NetworkAgentInfo nai, NetworkCapabilities nc,
|
||||
@Nullable
|
||||
private String getVpnIsolationInterface(@NonNull NetworkAgentInfo nai, NetworkCapabilities nc,
|
||||
LinkProperties lp) {
|
||||
if (nc == null || lp == null) return false;
|
||||
return nai.isVPN()
|
||||
if (nc == null || lp == null) return null;
|
||||
if (nai.isVPN()
|
||||
&& !nai.networkAgentConfig.allowBypass
|
||||
&& nc.getOwnerUid() != Process.SYSTEM_UID
|
||||
&& lp.getInterfaceName() != null
|
||||
&& (lp.hasIpv4DefaultRoute() || lp.hasIpv4UnreachableDefaultRoute())
|
||||
&& (lp.hasIpv6DefaultRoute() || lp.hasIpv6UnreachableDefaultRoute())
|
||||
&& !lp.hasExcludeRoute();
|
||||
&& !lp.hasExcludeRoute()) {
|
||||
return lp.getInterfaceName();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether we need to set interface filtering rule or not
|
||||
*/
|
||||
private boolean requiresVpnAllowRule(NetworkAgentInfo nai, LinkProperties lp,
|
||||
String filterIface) {
|
||||
// Only filter if lp has an interface.
|
||||
if (lp == null || lp.getInterfaceName() == null) return false;
|
||||
// Before T, allow rules are only needed if VPN isolation is enabled.
|
||||
// T and After T, allow rules are needed for all VPNs.
|
||||
return filterIface != null || (nai.isVPN() && SdkLevel.isAtLeastT());
|
||||
}
|
||||
|
||||
private static UidRangeParcel[] toUidRangeStableParcels(final @NonNull Set<UidRange> ranges) {
|
||||
@@ -8195,9 +8222,10 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
||||
if (!prevRanges.isEmpty()) {
|
||||
updateVpnUidRanges(false, nai, prevRanges);
|
||||
}
|
||||
final boolean wasFiltering = requiresVpnIsolation(nai, prevNc, nai.linkProperties);
|
||||
final boolean shouldFilter = requiresVpnIsolation(nai, newNc, nai.linkProperties);
|
||||
final String iface = nai.linkProperties.getInterfaceName();
|
||||
final String oldIface = getVpnIsolationInterface(nai, prevNc, nai.linkProperties);
|
||||
final String newIface = getVpnIsolationInterface(nai, newNc, nai.linkProperties);
|
||||
final boolean wasFiltering = requiresVpnAllowRule(nai, nai.linkProperties, oldIface);
|
||||
final boolean shouldFilter = requiresVpnAllowRule(nai, nai.linkProperties, newIface);
|
||||
// For VPN uid interface filtering, old ranges need to be removed before new ranges can
|
||||
// be added, due to the range being expanded and stored as individual UIDs. For example
|
||||
// the UIDs might be updated from [0, 99999] to ([0, 10012], [10014, 99999]) which means
|
||||
@@ -8209,11 +8237,16 @@ public class ConnectivityService extends IConnectivityManager.Stub
|
||||
// above, where the addition of new ranges happens before the removal of old ranges.
|
||||
// TODO Fix this window by computing an accurate diff on Set<UidRange>, so the old range
|
||||
// to be removed will never overlap with the new range to be added.
|
||||
|
||||
// Null iface given to onVpnUidRangesAdded/Removed is a wildcard to allow apps to
|
||||
// receive packets on all interfaces. This is required to accept incoming traffic in
|
||||
// Lockdown mode by overriding the Lockdown blocking rule.
|
||||
if (wasFiltering && !prevRanges.isEmpty()) {
|
||||
mPermissionMonitor.onVpnUidRangesRemoved(iface, prevRanges, prevNc.getOwnerUid());
|
||||
mPermissionMonitor.onVpnUidRangesRemoved(oldIface, prevRanges,
|
||||
prevNc.getOwnerUid());
|
||||
}
|
||||
if (shouldFilter && !newRanges.isEmpty()) {
|
||||
mPermissionMonitor.onVpnUidRangesAdded(iface, newRanges, newNc.getOwnerUid());
|
||||
mPermissionMonitor.onVpnUidRangesAdded(newIface, newRanges, newNc.getOwnerUid());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Never crash!
|
||||
|
||||
@@ -23,6 +23,9 @@ import static android.Manifest.permission.NETWORK_STACK;
|
||||
import static android.Manifest.permission.UPDATE_DEVICE_STATS;
|
||||
import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
|
||||
import static android.content.pm.PackageManager.GET_PERMISSIONS;
|
||||
import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOCKDOWN_VPN;
|
||||
import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
|
||||
import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
|
||||
import static android.net.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS;
|
||||
import static android.net.INetd.PERMISSION_INTERNET;
|
||||
import static android.net.INetd.PERMISSION_NETWORK;
|
||||
@@ -37,6 +40,7 @@ import static android.os.Process.SYSTEM_UID;
|
||||
import static com.android.net.module.util.CollectionUtils.toIntArray;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@@ -74,7 +78,6 @@ import com.android.networkstack.apishim.common.ProcessShim;
|
||||
import com.android.server.BpfNetMaps;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -108,10 +111,19 @@ public class PermissionMonitor {
|
||||
@GuardedBy("this")
|
||||
private final SparseIntArray mUidToNetworkPerm = new SparseIntArray();
|
||||
|
||||
// Keys are active non-bypassable and fully-routed VPN's interface name, Values are uid ranges
|
||||
// for apps under the VPN
|
||||
// NonNull keys are active non-bypassable and fully-routed VPN's interface name, Values are uid
|
||||
// ranges for apps under the VPNs which enable interface filtering.
|
||||
// If key is null, Values are uid ranges for apps under the VPNs which are connected but do not
|
||||
// enable interface filtering.
|
||||
@GuardedBy("this")
|
||||
private final Map<String, Set<UidRange>> mVpnUidRanges = new HashMap<>();
|
||||
private final Map<String, Set<UidRange>> mVpnInterfaceUidRanges = new ArrayMap<>();
|
||||
|
||||
// Items are uid ranges for apps under the VPN Lockdown
|
||||
// Ranges were given through ConnectivityManager#setRequireVpnForUids, and ranges are allowed to
|
||||
// have duplicates. Also, it is allowed to give ranges that are already subject to lockdown.
|
||||
// So we need to maintain uid range with multiset.
|
||||
@GuardedBy("this")
|
||||
private final MultiSet<UidRange> mVpnLockdownUidRanges = new MultiSet<>();
|
||||
|
||||
// A set of appIds for apps across all users on the device. We track appIds instead of uids
|
||||
// directly to reduce its size and also eliminate the need to update this set when user is
|
||||
@@ -201,6 +213,38 @@ public class PermissionMonitor {
|
||||
}
|
||||
}
|
||||
|
||||
private static class MultiSet<T> {
|
||||
private final Map<T, Integer> mMap = new ArrayMap<>();
|
||||
|
||||
/**
|
||||
* Returns the number of key in the set before this addition.
|
||||
*/
|
||||
public int add(T key) {
|
||||
final int oldCount = mMap.getOrDefault(key, 0);
|
||||
mMap.put(key, oldCount + 1);
|
||||
return oldCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of key in the set before this removal.
|
||||
*/
|
||||
public int remove(T key) {
|
||||
final int oldCount = mMap.getOrDefault(key, 0);
|
||||
if (oldCount == 0) {
|
||||
Log.wtf(TAG, "Attempt to remove non existing key = " + key.toString());
|
||||
} else if (oldCount == 1) {
|
||||
mMap.remove(key);
|
||||
} else {
|
||||
mMap.put(key, oldCount - 1);
|
||||
}
|
||||
return oldCount;
|
||||
}
|
||||
|
||||
public Set<T> getSet() {
|
||||
return mMap.keySet();
|
||||
}
|
||||
}
|
||||
|
||||
public PermissionMonitor(@NonNull final Context context, @NonNull final INetd netd,
|
||||
@NonNull final BpfNetMaps bpfNetMaps) {
|
||||
this(context, netd, bpfNetMaps, new Dependencies());
|
||||
@@ -626,16 +670,26 @@ public class PermissionMonitor {
|
||||
}
|
||||
|
||||
private synchronized void updateVpnUid(int uid, boolean add) {
|
||||
for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
|
||||
// Apps that can use restricted networks can always bypass VPNs.
|
||||
if (hasRestrictedNetworksPermission(uid)) {
|
||||
return;
|
||||
}
|
||||
for (Map.Entry<String, Set<UidRange>> vpn : mVpnInterfaceUidRanges.entrySet()) {
|
||||
if (UidRange.containsUid(vpn.getValue(), uid)) {
|
||||
final Set<Integer> changedUids = new HashSet<>();
|
||||
changedUids.add(uid);
|
||||
removeBypassingUids(changedUids, -1 /* vpnAppUid */);
|
||||
updateVpnUidsInterfaceRules(vpn.getKey(), changedUids, add);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void updateLockdownUid(int uid, boolean add) {
|
||||
if (UidRange.containsUid(mVpnLockdownUidRanges.getSet(), uid)
|
||||
&& !hasRestrictedNetworksPermission(uid)) {
|
||||
updateLockdownUidRule(uid, add);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This handles both network and traffic permission, because there is no overlap in actual
|
||||
* values, where network permission is NETWORK or SYSTEM, and traffic permission is INTERNET
|
||||
@@ -729,9 +783,10 @@ public class PermissionMonitor {
|
||||
|
||||
// If the newly-installed package falls within some VPN's uid range, update Netd with it.
|
||||
// This needs to happen after the mUidToNetworkPerm update above, since
|
||||
// removeBypassingUids() in updateVpnUid() depends on mUidToNetworkPerm to check if the
|
||||
// package can bypass VPN.
|
||||
// hasRestrictedNetworksPermission() in updateVpnUid() and updateLockdownUid() depends on
|
||||
// mUidToNetworkPerm to check if the package can bypass VPN.
|
||||
updateVpnUid(uid, true /* add */);
|
||||
updateLockdownUid(uid, true /* add */);
|
||||
mAllApps.add(appId);
|
||||
|
||||
// Log package added.
|
||||
@@ -775,9 +830,10 @@ public class PermissionMonitor {
|
||||
|
||||
// If the newly-removed package falls within some VPN's uid range, update Netd with it.
|
||||
// This needs to happen before the mUidToNetworkPerm update below, since
|
||||
// removeBypassingUids() in updateVpnUid() depends on mUidToNetworkPerm to check if the
|
||||
// package can bypass VPN.
|
||||
// hasRestrictedNetworksPermission() in updateVpnUid() and updateLockdownUid() depends on
|
||||
// mUidToNetworkPerm to check if the package can bypass VPN.
|
||||
updateVpnUid(uid, false /* add */);
|
||||
updateLockdownUid(uid, false /* add */);
|
||||
// If the package has been removed from all users on the device, clear it form mAllApps.
|
||||
if (mPackageManager.getNameForUid(uid) == null) {
|
||||
mAllApps.remove(appId);
|
||||
@@ -859,48 +915,100 @@ public class PermissionMonitor {
|
||||
/**
|
||||
* Called when a new set of UID ranges are added to an active VPN network
|
||||
*
|
||||
* @param iface The active VPN network's interface name
|
||||
* @param iface The active VPN network's interface name. Null iface indicates that the app is
|
||||
* allowed to receive packets on all interfaces.
|
||||
* @param rangesToAdd The new UID ranges to be added to the network
|
||||
* @param vpnAppUid The uid of the VPN app
|
||||
*/
|
||||
public synchronized void onVpnUidRangesAdded(@NonNull String iface, Set<UidRange> rangesToAdd,
|
||||
public synchronized void onVpnUidRangesAdded(@Nullable String iface, Set<UidRange> rangesToAdd,
|
||||
int vpnAppUid) {
|
||||
// Calculate the list of new app uids under the VPN due to the new UID ranges and update
|
||||
// Netd about them. Because mAllApps only contains appIds instead of uids, the result might
|
||||
// be an overestimation if an app is not installed on the user on which the VPN is running,
|
||||
// but that's safe.
|
||||
// but that's safe: if an app is not installed, it cannot receive any packets, so dropping
|
||||
// packets to that UID is fine.
|
||||
final Set<Integer> changedUids = intersectUids(rangesToAdd, mAllApps);
|
||||
removeBypassingUids(changedUids, vpnAppUid);
|
||||
updateVpnUidsInterfaceRules(iface, changedUids, true /* add */);
|
||||
if (mVpnUidRanges.containsKey(iface)) {
|
||||
mVpnUidRanges.get(iface).addAll(rangesToAdd);
|
||||
if (mVpnInterfaceUidRanges.containsKey(iface)) {
|
||||
mVpnInterfaceUidRanges.get(iface).addAll(rangesToAdd);
|
||||
} else {
|
||||
mVpnUidRanges.put(iface, new HashSet<UidRange>(rangesToAdd));
|
||||
mVpnInterfaceUidRanges.put(iface, new HashSet<UidRange>(rangesToAdd));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a set of UID ranges are removed from an active VPN network
|
||||
*
|
||||
* @param iface The VPN network's interface name
|
||||
* @param iface The VPN network's interface name. Null iface indicates that the app is allowed
|
||||
* to receive packets on all interfaces.
|
||||
* @param rangesToRemove Existing UID ranges to be removed from the VPN network
|
||||
* @param vpnAppUid The uid of the VPN app
|
||||
*/
|
||||
public synchronized void onVpnUidRangesRemoved(@NonNull String iface,
|
||||
public synchronized void onVpnUidRangesRemoved(@Nullable String iface,
|
||||
Set<UidRange> rangesToRemove, int vpnAppUid) {
|
||||
// Calculate the list of app uids that are no longer under the VPN due to the removed UID
|
||||
// ranges and update Netd about them.
|
||||
final Set<Integer> changedUids = intersectUids(rangesToRemove, mAllApps);
|
||||
removeBypassingUids(changedUids, vpnAppUid);
|
||||
updateVpnUidsInterfaceRules(iface, changedUids, false /* add */);
|
||||
Set<UidRange> existingRanges = mVpnUidRanges.getOrDefault(iface, null);
|
||||
Set<UidRange> existingRanges = mVpnInterfaceUidRanges.getOrDefault(iface, null);
|
||||
if (existingRanges == null) {
|
||||
loge("Attempt to remove unknown vpn uid Range iface = " + iface);
|
||||
return;
|
||||
}
|
||||
existingRanges.removeAll(rangesToRemove);
|
||||
if (existingRanges.size() == 0) {
|
||||
mVpnUidRanges.remove(iface);
|
||||
mVpnInterfaceUidRanges.remove(iface);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when UID ranges under VPN Lockdown are updated
|
||||
*
|
||||
* @param add {@code true} if the uids are to be added to the Lockdown, {@code false} if they
|
||||
* are to be removed from the Lockdown.
|
||||
* @param ranges The updated UID ranges under VPN Lockdown. This function does not treat the VPN
|
||||
* app's UID in any special way. The caller is responsible for excluding the VPN
|
||||
* app UID from the passed-in ranges.
|
||||
* Ranges can have duplications and/or contain the range that is already subject
|
||||
* to lockdown. However, ranges can not have overlaps with other ranges including
|
||||
* ranges that are currently subject to lockdown.
|
||||
*/
|
||||
public synchronized void updateVpnLockdownUidRanges(boolean add, UidRange[] ranges) {
|
||||
final Set<UidRange> affectedUidRanges = new HashSet<>();
|
||||
|
||||
for (final UidRange range : ranges) {
|
||||
if (add) {
|
||||
// Rule will be added if mVpnLockdownUidRanges does not have this uid range entry
|
||||
// currently.
|
||||
if (mVpnLockdownUidRanges.add(range) == 0) {
|
||||
affectedUidRanges.add(range);
|
||||
}
|
||||
} else {
|
||||
// Rule will be removed if the number of the range in the set is 1 before the
|
||||
// removal.
|
||||
if (mVpnLockdownUidRanges.remove(range) == 1) {
|
||||
affectedUidRanges.add(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mAllApps only contains appIds instead of uids. So the generated uid list might contain
|
||||
// apps that are installed only on some users but not others. But that's safe: if an app is
|
||||
// not installed, it cannot receive any packets, so dropping packets to that UID is fine.
|
||||
final Set<Integer> affectedUids = intersectUids(affectedUidRanges, mAllApps);
|
||||
|
||||
// We skip adding rule to privileged apps and allow them to bypass incoming packet
|
||||
// filtering. The behaviour is consistent with how lockdown works for outgoing packets, but
|
||||
// the implementation is different: while ConnectivityService#setRequireVpnForUids does not
|
||||
// exclude privileged apps from the prohibit routing rules used to implement outgoing packet
|
||||
// filtering, privileged apps can still bypass outgoing packet filtering because the
|
||||
// prohibit rules observe the protected from VPN bit.
|
||||
for (final int uid: affectedUids) {
|
||||
if (!hasRestrictedNetworksPermission(uid)) {
|
||||
updateLockdownUidRule(uid, add);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -939,7 +1047,7 @@ public class PermissionMonitor {
|
||||
*/
|
||||
private void removeBypassingUids(Set<Integer> uids, int vpnAppUid) {
|
||||
uids.remove(vpnAppUid);
|
||||
uids.removeIf(uid -> mUidToNetworkPerm.get(uid, PERMISSION_NONE) == PERMISSION_SYSTEM);
|
||||
uids.removeIf(this::hasRestrictedNetworksPermission);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -948,6 +1056,7 @@ public class PermissionMonitor {
|
||||
*
|
||||
* This is to instruct netd to set up appropriate filtering rules for these uids, such that they
|
||||
* can only receive ingress packets from the VPN's tunnel interface (and loopback).
|
||||
* Null iface set up a wildcard rule that allow app to receive packets on all interfaces.
|
||||
*
|
||||
* @param iface the interface name of the active VPN connection
|
||||
* @param add {@code true} if the uids are to be added to the interface, {@code false} if they
|
||||
@@ -968,6 +1077,18 @@ public class PermissionMonitor {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLockdownUidRule(int uid, boolean add) {
|
||||
try {
|
||||
if (add) {
|
||||
mBpfNetMaps.setUidRule(FIREWALL_CHAIN_LOCKDOWN_VPN, uid, FIREWALL_RULE_DENY);
|
||||
} else {
|
||||
mBpfNetMaps.setUidRule(FIREWALL_CHAIN_LOCKDOWN_VPN, uid, FIREWALL_RULE_ALLOW);
|
||||
}
|
||||
} catch (ServiceSpecificException e) {
|
||||
loge("Failed to " + (add ? "add" : "remove") + " Lockdown rule: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the updated permission information to netd. Called upon package install/uninstall.
|
||||
*
|
||||
@@ -1055,8 +1176,14 @@ public class PermissionMonitor {
|
||||
|
||||
/** Should only be used by unit tests */
|
||||
@VisibleForTesting
|
||||
public Set<UidRange> getVpnUidRanges(String iface) {
|
||||
return mVpnUidRanges.get(iface);
|
||||
public Set<UidRange> getVpnInterfaceUidRanges(String iface) {
|
||||
return mVpnInterfaceUidRanges.get(iface);
|
||||
}
|
||||
|
||||
/** Should only be used by unit tests */
|
||||
@VisibleForTesting
|
||||
public Set<UidRange> getVpnLockdownUidRanges() {
|
||||
return mVpnLockdownUidRanges.getSet();
|
||||
}
|
||||
|
||||
private synchronized void onSettingChanged() {
|
||||
@@ -1121,13 +1248,21 @@ public class PermissionMonitor {
|
||||
public void dump(IndentingPrintWriter pw) {
|
||||
pw.println("Interface filtering rules:");
|
||||
pw.increaseIndent();
|
||||
for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
|
||||
for (Map.Entry<String, Set<UidRange>> vpn : mVpnInterfaceUidRanges.entrySet()) {
|
||||
pw.println("Interface: " + vpn.getKey());
|
||||
pw.println("UIDs: " + vpn.getValue().toString());
|
||||
pw.println();
|
||||
}
|
||||
pw.decreaseIndent();
|
||||
|
||||
pw.println();
|
||||
pw.println("Lockdown filtering rules:");
|
||||
pw.increaseIndent();
|
||||
for (final UidRange range : mVpnLockdownUidRanges.getSet()) {
|
||||
pw.println("UIDs: " + range.toString());
|
||||
}
|
||||
pw.decreaseIndent();
|
||||
|
||||
pw.println();
|
||||
pw.println("Update logs:");
|
||||
pw.increaseIndent();
|
||||
|
||||
@@ -51,6 +51,9 @@ import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
|
||||
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
|
||||
import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO;
|
||||
import static android.net.ConnectivityManager.EXTRA_NETWORK_TYPE;
|
||||
import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOCKDOWN_VPN;
|
||||
import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
|
||||
import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
|
||||
import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_DEFAULT;
|
||||
import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE;
|
||||
import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK;
|
||||
@@ -9502,6 +9505,46 @@ public class ConnectivityServiceTest {
|
||||
b2.expectBroadcast();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLockdownSetFirewallUidRule() throws Exception {
|
||||
// For ConnectivityService#setAlwaysOnVpnPackage.
|
||||
mServiceContext.setPermission(
|
||||
Manifest.permission.CONTROL_ALWAYS_ON_VPN, PERMISSION_GRANTED);
|
||||
// Needed to call Vpn#setAlwaysOnPackage.
|
||||
mServiceContext.setPermission(Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED);
|
||||
// Needed to call Vpn#isAlwaysOnPackageSupported.
|
||||
mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
|
||||
|
||||
// Enable Lockdown
|
||||
final ArrayList<String> allowList = new ArrayList<>();
|
||||
mVpnManagerService.setAlwaysOnVpnPackage(PRIMARY_USER, ALWAYS_ON_PACKAGE,
|
||||
true /* lockdown */, allowList);
|
||||
waitForIdle();
|
||||
|
||||
// Lockdown rule is set to apps uids
|
||||
verify(mBpfNetMaps).setUidRule(
|
||||
eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP1_UID), eq(FIREWALL_RULE_DENY));
|
||||
verify(mBpfNetMaps).setUidRule(
|
||||
eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP2_UID), eq(FIREWALL_RULE_DENY));
|
||||
|
||||
reset(mBpfNetMaps);
|
||||
|
||||
// Disable lockdown
|
||||
mVpnManagerService.setAlwaysOnVpnPackage(PRIMARY_USER, null, false /* lockdown */,
|
||||
allowList);
|
||||
waitForIdle();
|
||||
|
||||
// Lockdown rule is removed from apps uids
|
||||
verify(mBpfNetMaps).setUidRule(
|
||||
eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP1_UID), eq(FIREWALL_RULE_ALLOW));
|
||||
verify(mBpfNetMaps).setUidRule(
|
||||
eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP2_UID), eq(FIREWALL_RULE_ALLOW));
|
||||
|
||||
// Interface rules are not changed by Lockdown mode enable/disable
|
||||
verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
|
||||
verify(mBpfNetMaps, never()).removeUidInterfaceRules(any());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test mutable and requestable network capabilities such as
|
||||
* {@link NetworkCapabilities#NET_CAPABILITY_TRUSTED} and
|
||||
@@ -10373,7 +10416,7 @@ public class ConnectivityServiceTest {
|
||||
verify(mBpfNetMaps, times(2)).addUidInterfaceRules(eq("tun0"), uidCaptor.capture());
|
||||
assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID);
|
||||
assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID);
|
||||
assertTrue(mService.mPermissionMonitor.getVpnUidRanges("tun0").equals(vpnRange));
|
||||
assertTrue(mService.mPermissionMonitor.getVpnInterfaceUidRanges("tun0").equals(vpnRange));
|
||||
|
||||
mMockVpn.disconnect();
|
||||
waitForIdle();
|
||||
@@ -10381,11 +10424,11 @@ public class ConnectivityServiceTest {
|
||||
// Disconnected VPN should have interface rules removed
|
||||
verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
|
||||
assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID);
|
||||
assertNull(mService.mPermissionMonitor.getVpnUidRanges("tun0"));
|
||||
assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges("tun0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLegacyVpnDoesNotResultInInterfaceFilteringRule() throws Exception {
|
||||
public void testLegacyVpnSetInterfaceFilteringRuleWithWildcard() throws Exception {
|
||||
LinkProperties lp = new LinkProperties();
|
||||
lp.setInterfaceName("tun0");
|
||||
lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null));
|
||||
@@ -10395,13 +10438,29 @@ public class ConnectivityServiceTest {
|
||||
mMockVpn.establish(lp, Process.SYSTEM_UID, vpnRange);
|
||||
assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID);
|
||||
|
||||
// Legacy VPN should not have interface rules set up
|
||||
verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
|
||||
// A connected Legacy VPN should have interface rules with null interface.
|
||||
// Null Interface is a wildcard and this accepts traffic from all the interfaces.
|
||||
// There are two expected invocations, one during the VPN initial connection,
|
||||
// one during the VPN LinkProperties update.
|
||||
ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class);
|
||||
verify(mBpfNetMaps, times(2)).addUidInterfaceRules(
|
||||
eq(null) /* iface */, uidCaptor.capture());
|
||||
assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID, VPN_UID);
|
||||
assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID, VPN_UID);
|
||||
assertEquals(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */),
|
||||
vpnRange);
|
||||
|
||||
mMockVpn.disconnect();
|
||||
waitForIdle();
|
||||
|
||||
// Disconnected VPN should have interface rules removed
|
||||
verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
|
||||
assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID, VPN_UID);
|
||||
assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLocalIpv4OnlyVpnDoesNotResultInInterfaceFilteringRule()
|
||||
throws Exception {
|
||||
public void testLocalIpv4OnlyVpnSetInterfaceFilteringRuleWithWildcard() throws Exception {
|
||||
LinkProperties lp = new LinkProperties();
|
||||
lp.setInterfaceName("tun0");
|
||||
lp.addRoute(new RouteInfo(new IpPrefix("192.0.2.0/24"), null, "tun0"));
|
||||
@@ -10412,7 +10471,25 @@ public class ConnectivityServiceTest {
|
||||
assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID);
|
||||
|
||||
// IPv6 unreachable route should not be misinterpreted as a default route
|
||||
verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
|
||||
// A connected VPN should have interface rules with null interface.
|
||||
// Null Interface is a wildcard and this accepts traffic from all the interfaces.
|
||||
// There are two expected invocations, one during the VPN initial connection,
|
||||
// one during the VPN LinkProperties update.
|
||||
ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class);
|
||||
verify(mBpfNetMaps, times(2)).addUidInterfaceRules(
|
||||
eq(null) /* iface */, uidCaptor.capture());
|
||||
assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID, VPN_UID);
|
||||
assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID, VPN_UID);
|
||||
assertEquals(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */),
|
||||
vpnRange);
|
||||
|
||||
mMockVpn.disconnect();
|
||||
waitForIdle();
|
||||
|
||||
// Disconnected VPN should have interface rules removed
|
||||
verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
|
||||
assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID, VPN_UID);
|
||||
assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -30,6 +30,9 @@ import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
|
||||
import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_REQUIRED;
|
||||
import static android.content.pm.PackageManager.GET_PERMISSIONS;
|
||||
import static android.content.pm.PackageManager.MATCH_ANY_USER;
|
||||
import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOCKDOWN_VPN;
|
||||
import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
|
||||
import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
|
||||
import static android.net.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS;
|
||||
import static android.net.INetd.PERMISSION_INTERNET;
|
||||
import static android.net.INetd.PERMISSION_NETWORK;
|
||||
@@ -761,8 +764,8 @@ public class PermissionMonitorTest {
|
||||
MOCK_APPID1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdates() throws Exception {
|
||||
private void doTestuidFilteringDuringVpnConnectDisconnectAndUidUpdates(@Nullable String ifName)
|
||||
throws Exception {
|
||||
doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
|
||||
CONNECTIVITY_USE_RESTRICTED_NETWORKS),
|
||||
buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
|
||||
@@ -778,8 +781,8 @@ public class PermissionMonitorTest {
|
||||
final Set<UidRange> vpnRange2 = Set.of(new UidRange(MOCK_UID12, MOCK_UID12));
|
||||
|
||||
// When VPN is connected, expect a rule to be set up for user app MOCK_UID11
|
||||
mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange1, VPN_UID);
|
||||
verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11}));
|
||||
mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange1, VPN_UID);
|
||||
verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
|
||||
|
||||
reset(mBpfNetMaps);
|
||||
|
||||
@@ -787,27 +790,38 @@ public class PermissionMonitorTest {
|
||||
mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
|
||||
verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[]{MOCK_UID11}));
|
||||
mPermissionMonitor.onPackageAdded(MOCK_PACKAGE1, MOCK_UID11);
|
||||
verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11}));
|
||||
verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
|
||||
|
||||
reset(mBpfNetMaps);
|
||||
|
||||
// During VPN uid update (vpnRange1 -> vpnRange2), ConnectivityService first deletes the
|
||||
// old UID rules then adds the new ones. Expect netd to be updated
|
||||
mPermissionMonitor.onVpnUidRangesRemoved("tun0", vpnRange1, VPN_UID);
|
||||
mPermissionMonitor.onVpnUidRangesRemoved(ifName, vpnRange1, VPN_UID);
|
||||
verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[] {MOCK_UID11}));
|
||||
mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange2, VPN_UID);
|
||||
verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID12}));
|
||||
mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange2, VPN_UID);
|
||||
verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID12}));
|
||||
|
||||
reset(mBpfNetMaps);
|
||||
|
||||
// When VPN is disconnected, expect rules to be torn down
|
||||
mPermissionMonitor.onVpnUidRangesRemoved("tun0", vpnRange2, VPN_UID);
|
||||
mPermissionMonitor.onVpnUidRangesRemoved(ifName, vpnRange2, VPN_UID);
|
||||
verify(mBpfNetMaps).removeUidInterfaceRules(aryEq(new int[] {MOCK_UID12}));
|
||||
assertNull(mPermissionMonitor.getVpnUidRanges("tun0"));
|
||||
assertNull(mPermissionMonitor.getVpnInterfaceUidRanges(ifName));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUidFilteringDuringPackageInstallAndUninstall() throws Exception {
|
||||
public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdates() throws Exception {
|
||||
doTestuidFilteringDuringVpnConnectDisconnectAndUidUpdates("tun0");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdatesWithWildcard()
|
||||
throws Exception {
|
||||
doTestuidFilteringDuringVpnConnectDisconnectAndUidUpdates(null /* ifName */);
|
||||
}
|
||||
|
||||
private void doTestUidFilteringDuringPackageInstallAndUninstall(@Nullable String ifName) throws
|
||||
Exception {
|
||||
doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
|
||||
NETWORK_STACK, CONNECTIVITY_USE_RESTRICTED_NETWORKS),
|
||||
buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
|
||||
@@ -818,12 +832,12 @@ public class PermissionMonitorTest {
|
||||
mPermissionMonitor.startMonitoring();
|
||||
final Set<UidRange> vpnRange = Set.of(UidRange.createForUser(MOCK_USER1),
|
||||
UidRange.createForUser(MOCK_USER2));
|
||||
mPermissionMonitor.onVpnUidRangesAdded("tun0", vpnRange, VPN_UID);
|
||||
mPermissionMonitor.onVpnUidRangesAdded(ifName, vpnRange, VPN_UID);
|
||||
|
||||
// Newly-installed package should have uid rules added
|
||||
addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_APPID1);
|
||||
verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID11}));
|
||||
verify(mBpfNetMaps).addUidInterfaceRules(eq("tun0"), aryEq(new int[]{MOCK_UID21}));
|
||||
verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID11}));
|
||||
verify(mBpfNetMaps).addUidInterfaceRules(eq(ifName), aryEq(new int[]{MOCK_UID21}));
|
||||
|
||||
// Removed package should have its uid rules removed
|
||||
mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
|
||||
@@ -831,6 +845,168 @@ public class PermissionMonitorTest {
|
||||
verify(mBpfNetMaps, never()).removeUidInterfaceRules(aryEq(new int[]{MOCK_UID21}));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUidFilteringDuringPackageInstallAndUninstall() throws Exception {
|
||||
doTestUidFilteringDuringPackageInstallAndUninstall("tun0");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUidFilteringDuringPackageInstallAndUninstallWithWildcard() throws Exception {
|
||||
doTestUidFilteringDuringPackageInstallAndUninstall(null /* ifName */);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLockdownUidFilteringWithLockdownEnableDisable() {
|
||||
doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
|
||||
CONNECTIVITY_USE_RESTRICTED_NETWORKS),
|
||||
buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
|
||||
buildPackageInfo(MOCK_PACKAGE2, MOCK_UID12),
|
||||
buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
|
||||
.when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
|
||||
mPermissionMonitor.startMonitoring();
|
||||
// Every app on user 0 except MOCK_UID12 are under VPN.
|
||||
final UidRange[] vpnRange1 = {
|
||||
new UidRange(0, MOCK_UID12 - 1),
|
||||
new UidRange(MOCK_UID12 + 1, UserHandle.PER_USER_RANGE - 1)
|
||||
};
|
||||
|
||||
// Add Lockdown uid range, expect a rule to be set up for user app MOCK_UID11
|
||||
mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange1);
|
||||
verify(mBpfNetMaps)
|
||||
.setUidRule(
|
||||
eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
|
||||
eq(FIREWALL_RULE_DENY));
|
||||
assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange1));
|
||||
|
||||
reset(mBpfNetMaps);
|
||||
|
||||
// Remove Lockdown uid range, expect rules to be torn down
|
||||
mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange1);
|
||||
verify(mBpfNetMaps)
|
||||
.setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
|
||||
eq(FIREWALL_RULE_ALLOW));
|
||||
assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLockdownUidFilteringWithLockdownEnableDisableWithMultiAdd() {
|
||||
doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
|
||||
CONNECTIVITY_USE_RESTRICTED_NETWORKS),
|
||||
buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
|
||||
buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
|
||||
.when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
|
||||
mPermissionMonitor.startMonitoring();
|
||||
// MOCK_UID11 is under VPN.
|
||||
final UidRange range = new UidRange(MOCK_UID11, MOCK_UID11);
|
||||
final UidRange[] vpnRange = {range};
|
||||
|
||||
// Add Lockdown uid range at 1st time, expect a rule to be set up
|
||||
mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange);
|
||||
verify(mBpfNetMaps)
|
||||
.setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
|
||||
eq(FIREWALL_RULE_DENY));
|
||||
assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
|
||||
|
||||
reset(mBpfNetMaps);
|
||||
|
||||
// Add Lockdown uid range at 2nd time, expect a rule not to be set up because the uid
|
||||
// already has the rule
|
||||
mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange);
|
||||
verify(mBpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt());
|
||||
assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
|
||||
|
||||
reset(mBpfNetMaps);
|
||||
|
||||
// Remove Lockdown uid range at 1st time, expect a rule not to be torn down because we added
|
||||
// the range 2 times.
|
||||
mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
|
||||
verify(mBpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt());
|
||||
assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
|
||||
|
||||
reset(mBpfNetMaps);
|
||||
|
||||
// Remove Lockdown uid range at 2nd time, expect a rule to be torn down because we added
|
||||
// twice and we removed twice.
|
||||
mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
|
||||
verify(mBpfNetMaps)
|
||||
.setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
|
||||
eq(FIREWALL_RULE_ALLOW));
|
||||
assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLockdownUidFilteringWithLockdownEnableDisableWithDuplicates() {
|
||||
doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
|
||||
CONNECTIVITY_USE_RESTRICTED_NETWORKS),
|
||||
buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
|
||||
buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
|
||||
.when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
|
||||
mPermissionMonitor.startMonitoring();
|
||||
// MOCK_UID11 is under VPN.
|
||||
final UidRange range = new UidRange(MOCK_UID11, MOCK_UID11);
|
||||
final UidRange[] vpnRangeDuplicates = {range, range};
|
||||
final UidRange[] vpnRange = {range};
|
||||
|
||||
// Add Lockdown uid ranges which contains duplicated uid ranges
|
||||
mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRangeDuplicates);
|
||||
verify(mBpfNetMaps)
|
||||
.setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
|
||||
eq(FIREWALL_RULE_DENY));
|
||||
assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
|
||||
|
||||
reset(mBpfNetMaps);
|
||||
|
||||
// Remove Lockdown uid range at 1st time, expect a rule not to be torn down because uid
|
||||
// ranges we added contains duplicated uid ranges.
|
||||
mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
|
||||
verify(mBpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt());
|
||||
assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
|
||||
|
||||
reset(mBpfNetMaps);
|
||||
|
||||
// Remove Lockdown uid range at 2nd time, expect a rule to be torn down.
|
||||
mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
|
||||
verify(mBpfNetMaps)
|
||||
.setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
|
||||
eq(FIREWALL_RULE_ALLOW));
|
||||
assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLockdownUidFilteringWithInstallAndUnInstall() {
|
||||
doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
|
||||
NETWORK_STACK, CONNECTIVITY_USE_RESTRICTED_NETWORKS),
|
||||
buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
|
||||
.when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
|
||||
doReturn(List.of(MOCK_USER1, MOCK_USER2)).when(mUserManager).getUserHandles(eq(true));
|
||||
|
||||
mPermissionMonitor.startMonitoring();
|
||||
final UidRange[] vpnRange = {
|
||||
UidRange.createForUser(MOCK_USER1),
|
||||
UidRange.createForUser(MOCK_USER2)
|
||||
};
|
||||
mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange);
|
||||
|
||||
// Installing package should add Lockdown rules
|
||||
addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_APPID1);
|
||||
verify(mBpfNetMaps)
|
||||
.setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
|
||||
eq(FIREWALL_RULE_DENY));
|
||||
verify(mBpfNetMaps)
|
||||
.setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID21),
|
||||
eq(FIREWALL_RULE_DENY));
|
||||
|
||||
reset(mBpfNetMaps);
|
||||
|
||||
// Uninstalling package should remove Lockdown rules
|
||||
mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
|
||||
verify(mBpfNetMaps)
|
||||
.setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
|
||||
eq(FIREWALL_RULE_ALLOW));
|
||||
verify(mBpfNetMaps, never())
|
||||
.setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID21),
|
||||
eq(FIREWALL_RULE_ALLOW));
|
||||
}
|
||||
|
||||
// Normal package add/remove operations will trigger multiple intent for uids corresponding to
|
||||
// each user. To simulate generic package operations, the onPackageAdded/Removed will need to be
|
||||
|
||||
Reference in New Issue
Block a user