Revert "Revert "Block incoming non-VPN packets to apps under fully-routed VPN""

This reverts commit bc571c7cc8.

Reason for revert: Rolling forward, will fix tests in same CL stack.

Bug: 114231106
Bug: 130397860
Test: FrameworksNetTests
Change-Id: Ia8a0c99b4e1fd5dff26c881715cd876618ca4321
This commit is contained in:
Lorenzo Colitti
2019-04-12 10:48:06 +00:00
parent 27a60a1aff
commit bad9d911b8
7 changed files with 739 additions and 104 deletions

View File

@@ -47,6 +47,7 @@ import static android.system.OsConstants.IPPROTO_UDP;
import static com.android.internal.util.Preconditions.checkNotNull;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.BroadcastOptions;
import android.app.NotificationManager;
@@ -276,7 +277,8 @@ public class ConnectivityService extends IConnectivityManager.Stub
private Tethering mTethering;
private final PermissionMonitor mPermissionMonitor;
@VisibleForTesting
protected final PermissionMonitor mPermissionMonitor;
private KeyStore mKeyStore;
@@ -829,13 +831,13 @@ public class ConnectivityService extends IConnectivityManager.Stub
public ConnectivityService(Context context, INetworkManagementService netManager,
INetworkStatsService statsService, INetworkPolicyManager policyManager) {
this(context, netManager, statsService, policyManager,
getDnsResolver(), new IpConnectivityLog());
getDnsResolver(), new IpConnectivityLog(), NetdService.getInstance());
}
@VisibleForTesting
protected ConnectivityService(Context context, INetworkManagementService netManager,
INetworkStatsService statsService, INetworkPolicyManager policyManager,
IDnsResolver dnsresolver, IpConnectivityLog logger) {
IDnsResolver dnsresolver, IpConnectivityLog logger, INetd netd) {
if (DBG) log("ConnectivityService starting up");
mSystemProperties = getSystemProperties();
@@ -875,7 +877,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
mDnsResolver = checkNotNull(dnsresolver, "missing IDnsResolver");
mProxyTracker = makeProxyTracker();
mNetd = NetdService.getInstance();
mNetd = netd;
mKeyStore = KeyStore.getInstance();
mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
@@ -961,7 +963,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
mTethering = makeTethering();
mPermissionMonitor = new PermissionMonitor(mContext, mNMS, mNetd);
mPermissionMonitor = new PermissionMonitor(mContext, mNetd);
// Set up the listener for user state for creating user VPNs.
// Should run on mHandler to avoid any races.
@@ -2441,6 +2443,13 @@ public class ConnectivityService extends IConnectivityManager.Stub
pw.println("NetworkStackClient logs:");
pw.increaseIndent();
NetworkStackClient.getInstance().dump(pw);
pw.decreaseIndent();
pw.println();
pw.println("Permission Monitor:");
pw.increaseIndent();
mPermissionMonitor.dump(pw);
pw.decreaseIndent();
}
private void dumpNetworks(IndentingPrintWriter pw) {
@@ -5465,6 +5474,11 @@ public class ConnectivityService extends IConnectivityManager.Stub
networkAgent.clatd.fixupLinkProperties(oldLp, newLp);
updateInterfaces(newLp, oldLp, netId, networkAgent.networkCapabilities);
// update filtering rules, need to happen after the interface update so netd knows about the
// new interface (the interface name -> index map becomes initialized)
updateVpnFiltering(newLp, oldLp, networkAgent);
updateMtu(newLp, oldLp);
// TODO - figure out what to do for clat
// for (LinkProperties lp : newLp.getStackedLinks()) {
@@ -5630,6 +5644,37 @@ 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);
if (!wasFiltering && !needsFiltering) {
// Nothing to do.
return;
}
if (Objects.equals(oldIface, newIface) && (wasFiltering == needsFiltering)) {
// Nothing changed.
return;
}
final Set<UidRange> ranges = nai.networkCapabilities.getUids();
final int vpnAppUid = nai.networkCapabilities.getEstablishingVpnAppUid();
// 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 whitelisted interfaces so here new rules can be added before the
// old rules are being removed.
if (wasFiltering) {
mPermissionMonitor.onVpnUidRangesRemoved(oldIface, ranges, vpnAppUid);
}
if (needsFiltering) {
mPermissionMonitor.onVpnUidRangesAdded(newIface, ranges, vpnAppUid);
}
}
private int getNetworkPermission(NetworkCapabilities nc) {
if (!nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)) {
return INetd.PERMISSION_SYSTEM;
@@ -5772,6 +5817,34 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
}
/**
* Returns whether VPN isolation (ingress interface filtering) should be applied on the given
* network.
*
* 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
* 1. the network is an app VPN (not legacy VPN)
* 2. the VPN does not allow bypass
* 3. the VPN is fully-routed
* 4. the VPN interface is non-null
*
* @See INetd#firewallAddUidInterfaceRules
* @See INetd#firewallRemoveUidInterfaceRules
*/
private boolean requiresVpnIsolation(@NonNull NetworkAgentInfo nai, NetworkCapabilities nc,
LinkProperties lp) {
if (nc == null || lp == null) return false;
return nai.isVPN()
&& !nai.networkMisc.allowBypass
&& nc.getEstablishingVpnAppUid() != Process.SYSTEM_UID
&& lp.getInterfaceName() != null
&& (lp.hasIPv4DefaultRoute() || lp.hasIPv6DefaultRoute());
}
private void updateUids(NetworkAgentInfo nai, NetworkCapabilities prevNc,
NetworkCapabilities newNc) {
Set<UidRange> prevRanges = null == prevNc ? null : prevNc.getUids();
@@ -5784,6 +5857,12 @@ public class ConnectivityService extends IConnectivityManager.Stub
newRanges.removeAll(prevRangesCopy);
try {
// When updating the VPN uid routing rules, add the new range first then remove the old
// range. If old range were removed first, there would be a window between the old
// range being removed and the new range being added, during which UIDs contained
// in both ranges are not subject to any VPN routing rules. Adding new range before
// removing old range works because, unlike the filtering rules below, it's possible to
// add duplicate UID routing rules.
if (!newRanges.isEmpty()) {
final UidRange[] addedRangesArray = new UidRange[newRanges.size()];
newRanges.toArray(addedRangesArray);
@@ -5794,9 +5873,31 @@ public class ConnectivityService extends IConnectivityManager.Stub
prevRanges.toArray(removedRangesArray);
mNMS.removeVpnUidRanges(nai.network.netId, removedRangesArray);
}
final boolean wasFiltering = requiresVpnIsolation(nai, prevNc, nai.linkProperties);
final boolean shouldFilter = requiresVpnIsolation(nai, newNc, nai.linkProperties);
final String iface = nai.linkProperties.getInterfaceName();
// 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 invidiual UIDs. For example
// the UIDs might be updated from [0, 99999] to ([0, 10012], [10014, 99999]) which means
// prevRanges = [0, 99999] while newRanges = [0, 10012], [10014, 99999]. If prevRanges
// were added first and then newRanges got removed later, there would be only one uid
// 10013 left. A consequence of removing old ranges before adding new ranges is that
// there is now a window of opportunity when the UIDs are not subject to any filtering.
// Note that this is in contrast with the (more robust) update of VPN routing rules
// 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.
if (wasFiltering && !prevRanges.isEmpty()) {
mPermissionMonitor.onVpnUidRangesRemoved(iface, prevRanges,
prevNc.getEstablishingVpnAppUid());
}
if (shouldFilter && !newRanges.isEmpty()) {
mPermissionMonitor.onVpnUidRangesAdded(iface, newRanges,
newNc.getEstablishingVpnAppUid());
}
} catch (Exception e) {
// Never crash!
loge("Exception in updateUids: " + e);
loge("Exception in updateUids: ", e);
}
}

View File

@@ -37,22 +37,27 @@ import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PackageManagerInternal;
import android.content.pm.UserInfo;
import android.net.INetd;
import android.net.UidRange;
import android.os.Build;
import android.os.INetworkManagementService;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.os.UserHandle;
import android.os.UserManager;
import android.system.OsConstants;
import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseIntArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.LocalServices;
import com.android.server.SystemConfig;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -60,6 +65,7 @@ import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* A utility class to inform Netd of UID permisisons.
* Does a mass update at boot and then monitors for app install/remove.
@@ -73,18 +79,29 @@ public class PermissionMonitor {
protected static final Boolean NETWORK = Boolean.FALSE;
private static final int VERSION_Q = Build.VERSION_CODES.Q;
private final Context mContext;
private final PackageManager mPackageManager;
private final UserManager mUserManager;
private final INetworkManagementService mNMS;
private final INetd mNetd;
// Values are User IDs.
@GuardedBy("this")
private final Set<Integer> mUsers = new HashSet<>();
// Keys are App IDs. Values are true for SYSTEM permission and false for NETWORK permission.
// Keys are app uids. Values are true for SYSTEM permission and false for NETWORK permission.
@GuardedBy("this")
private final Map<Integer, Boolean> mApps = new HashMap<>();
// Keys are active non-bypassable and fully-routed VPN's interface name, Values are uid ranges
// for apps under the VPN
@GuardedBy("this")
private final Map<String, Set<UidRange>> mVpnUidRanges = new HashMap<>();
// 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
// added/removed.
@GuardedBy("this")
private final Set<Integer> mAllApps = new HashSet<>();
private class PackageListObserver implements PackageManagerInternal.PackageListObserver {
private int getPermissionForUid(int uid) {
@@ -118,12 +135,10 @@ public class PermissionMonitor {
}
}
public PermissionMonitor(Context context, INetworkManagementService nms, INetd netdService) {
mContext = context;
public PermissionMonitor(Context context, INetd netd) {
mPackageManager = context.getPackageManager();
mUserManager = UserManager.get(context);
mNMS = nms;
mNetd = netdService;
mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
mNetd = netd;
}
// Intended to be called only once at startup, after the system is ready. Installs a broadcast
@@ -151,6 +166,7 @@ public class PermissionMonitor {
if (uid < 0) {
continue;
}
mAllApps.add(UserHandle.getAppId(uid));
boolean isNetwork = hasNetworkPermission(app);
boolean hasRestrictedPermission = hasRestrictedNetworkPermission(app);
@@ -270,10 +286,11 @@ public class PermissionMonitor {
}
}
private int[] toIntArray(List<Integer> list) {
private int[] toIntArray(Collection<Integer> list) {
int[] array = new int[list.size()];
for (int i = 0; i < list.size(); i++) {
array[i] = list.get(i);
int i = 0;
for (Integer item : list) {
array[i++] = item;
}
return array;
}
@@ -289,11 +306,11 @@ public class PermissionMonitor {
}
try {
if (add) {
mNMS.setPermission("NETWORK", toIntArray(network));
mNMS.setPermission("SYSTEM", toIntArray(system));
mNetd.networkSetPermissionForUser(INetd.PERMISSION_NETWORK, toIntArray(network));
mNetd.networkSetPermissionForUser(INetd.PERMISSION_SYSTEM, toIntArray(system));
} else {
mNMS.clearPermission(toIntArray(network));
mNMS.clearPermission(toIntArray(system));
mNetd.networkClearPermissionForUser(toIntArray(network));
mNetd.networkClearPermissionForUser(toIntArray(system));
}
} catch (RemoteException e) {
loge("Exception when updating permissions: " + e);
@@ -376,6 +393,19 @@ public class PermissionMonitor {
apps.put(uid, permission);
update(mUsers, apps, true);
}
// If the newly-installed package falls within some VPN's uid range, update Netd with it.
// This needs to happen after the mApps update above, since removeBypassingUids() depends
// on mApps to check if the package can bypass VPN.
for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
if (UidRange.containsUid(vpn.getValue(), uid)) {
final Set<Integer> changedUids = new HashSet<>();
changedUids.add(uid);
removeBypassingUids(changedUids, /* vpnAppUid */ -1);
updateVpnUids(vpn.getKey(), changedUids, true);
}
}
mAllApps.add(UserHandle.getAppId(uid));
}
/**
@@ -386,8 +416,23 @@ public class PermissionMonitor {
* @hide
*/
public synchronized void onPackageRemoved(int uid) {
Map<Integer, Boolean> apps = new HashMap<>();
// If the newly-removed package falls within some VPN's uid range, update Netd with it.
// This needs to happen before the mApps update below, since removeBypassingUids() depends
// on mApps to check if the package can bypass VPN.
for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
if (UidRange.containsUid(vpn.getValue(), uid)) {
final Set<Integer> changedUids = new HashSet<>();
changedUids.add(uid);
removeBypassingUids(changedUids, /* vpnAppUid */ -1);
updateVpnUids(vpn.getKey(), changedUids, false);
}
}
// If the package has been removed from all users on the device, clear it form mAllApps.
if (mPackageManager.getNameForUid(uid) == null) {
mAllApps.remove(UserHandle.getAppId(uid));
}
Map<Integer, Boolean> apps = new HashMap<>();
Boolean permission = null;
String[] packages = mPackageManager.getPackagesForUid(uid);
if (packages != null && packages.length > 0) {
@@ -442,6 +487,121 @@ 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 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,
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.
final Set<Integer> changedUids = intersectUids(rangesToAdd, mAllApps);
removeBypassingUids(changedUids, vpnAppUid);
updateVpnUids(iface, changedUids, true);
if (mVpnUidRanges.containsKey(iface)) {
mVpnUidRanges.get(iface).addAll(rangesToAdd);
} else {
mVpnUidRanges.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 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,
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);
updateVpnUids(iface, changedUids, false);
Set<UidRange> existingRanges = mVpnUidRanges.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);
}
}
/**
* Compute the intersection of a set of UidRanges and appIds. Returns a set of uids
* that satisfies:
* 1. falls into one of the UidRange
* 2. matches one of the appIds
*/
private Set<Integer> intersectUids(Set<UidRange> ranges, Set<Integer> appIds) {
Set<Integer> result = new HashSet<>();
for (UidRange range : ranges) {
for (int userId = range.getStartUser(); userId <= range.getEndUser(); userId++) {
for (int appId : appIds) {
final int uid = UserHandle.getUid(userId, appId);
if (range.contains(uid)) {
result.add(uid);
}
}
}
}
return result;
}
/**
* Remove all apps which can elect to bypass the VPN from the list of uids
*
* An app can elect to bypass the VPN if it hold SYSTEM permission, or if its the active VPN
* app itself.
*
* @param uids The list of uids to operate on
* @param vpnAppUid The uid of the VPN app
*/
private void removeBypassingUids(Set<Integer> uids, int vpnAppUid) {
uids.remove(vpnAppUid);
uids.removeIf(uid -> mApps.getOrDefault(uid, NETWORK) == SYSTEM);
}
/**
* Update netd about the list of uids that are under an active VPN connection which they cannot
* bypass.
*
* 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).
*
* @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
* are to be removed from the interface.
*/
private void updateVpnUids(String iface, Set<Integer> uids, boolean add) {
if (uids.size() == 0) {
return;
}
try {
if (add) {
mNetd.firewallAddUidInterfaceRules(iface, toIntArray(uids));
} else {
mNetd.firewallRemoveUidInterfaceRules(toIntArray(uids));
}
} catch (ServiceSpecificException e) {
// Silently ignore exception when device does not support eBPF, otherwise just log
// the exception and do not crash
if (e.errorCode != OsConstants.EOPNOTSUPP) {
loge("Exception when updating permissions: ", e);
}
} catch (RemoteException e) {
loge("Exception when updating permissions: ", e);
}
}
/**
* Called by PackageListObserver when a package is installed/uninstalled. Send the updated
* permission information to netd.
@@ -528,6 +688,24 @@ public class PermissionMonitor {
}
}
/** Should only be used by unit tests */
@VisibleForTesting
public Set<UidRange> getVpnUidRanges(String iface) {
return mVpnUidRanges.get(iface);
}
/** Dump info to dumpsys */
public void dump(IndentingPrintWriter pw) {
pw.println("Interface filtering rules:");
pw.increaseIndent();
for (Map.Entry<String, Set<UidRange>> vpn : mVpnUidRanges.entrySet()) {
pw.println("Interface: " + vpn.getKey());
pw.println("UIDs: " + vpn.getValue().toString());
pw.println();
}
pw.decreaseIndent();
}
private static void log(String s) {
if (DBG) {
Log.d(TAG, s);