Add bandwidth limiting to CS
Adds ingress rate limiting functionality to ConnectivityService. The tc
rate limit is installed before we tell netd about the interface, and
removed after the network is removed from netd. When the setting
changes, the old rate limit needs to be removed before a new one can be
added (unfortunately, we cannot use NLM_F_REPLACE when configuring the
tc-police filter).
Currently, this functionality is always enabled, but may or may not work
based on kernel support.
Bug: 157552970
Test: atest FrameworksNetTests:ConnectivityServiceTest
Change-Id: I4e64b2c40490f061e42b40a1b1b3a6618c3d1a87
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index d833bc2..ac795c8 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -90,6 +90,7 @@
import static android.os.Process.INVALID_UID;
import static android.os.Process.VPN_UID;
import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
+import static android.system.OsConstants.ETH_P_ALL;
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.IPPROTO_UDP;
@@ -196,6 +197,7 @@
import android.net.resolv.aidl.Nat64PrefixEventParcel;
import android.net.resolv.aidl.PrivateDnsValidationEventParcel;
import android.net.shared.PrivateDnsConfig;
+import android.net.util.InterfaceParams;
import android.net.util.MultinetworkPolicyTracker;
import android.os.BatteryStatsManager;
import android.os.Binder;
@@ -247,6 +249,7 @@
import com.android.net.module.util.LocationPermissionChecker;
import com.android.net.module.util.NetworkCapabilitiesUtils;
import com.android.net.module.util.PermissionUtils;
+import com.android.net.module.util.TcUtils;
import com.android.net.module.util.netlink.InetDiagMessage;
import com.android.server.connectivity.AutodestructReference;
import com.android.server.connectivity.CarrierPrivilegeAuthenticator;
@@ -273,6 +276,7 @@
import libcore.io.IoUtils;
import java.io.FileDescriptor;
+import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.net.Inet4Address;
@@ -708,6 +712,11 @@
private static final int EVENT_SET_TEST_ALLOW_BAD_WIFI_UNTIL = 55;
/**
+ * Used internally when INGRESS_RATE_LIMIT_BYTES_PER_SECOND setting changes.
+ */
+ private static final int EVENT_INGRESS_RATE_LIMIT_CHANGED = 56;
+
+ /**
* Argument for {@link #EVENT_PROVISIONING_NOTIFICATION} to indicate that the notification
* should be shown.
*/
@@ -724,6 +733,18 @@
*/
private static final long MAX_TEST_ALLOW_BAD_WIFI_UNTIL_MS = 5 * 60 * 1000L;
+ /**
+ * The priority of the tc police rate limiter -- smaller value is higher priority.
+ * This value needs to be coordinated with PRIO_CLAT, PRIO_TETHER4, and PRIO_TETHER6.
+ */
+ private static final short TC_PRIO_POLICE = 1;
+
+ /**
+ * The BPF program attached to the tc-police hook to account for to-be-dropped traffic.
+ */
+ private static final String TC_POLICE_BPF_PROG_PATH =
+ "/sys/fs/bpf/prog_netd_schedact_ingress_account";
+
private static String eventName(int what) {
return sMagicDecoderRing.get(what, Integer.toString(what));
}
@@ -814,6 +835,12 @@
final Map<IBinder, ConnectivityDiagnosticsCallbackInfo> mConnectivityDiagnosticsCallbacks =
new HashMap<>();
+ // Rate limit applicable to all internet capable networks (-1 = disabled). This value is
+ // configured via {@link
+ // ConnectivitySettingsManager#INGRESS_RATE_LIMIT_BYTES_PER_SECOND}
+ // Only the handler thread is allowed to access this field.
+ private long mIngressRateLimit = -1;
+
/**
* Implements support for the legacy "one network per network type" model.
*
@@ -1366,6 +1393,48 @@
public BpfNetMaps getBpfNetMaps(INetd netd) {
return new BpfNetMaps(netd);
}
+
+ /**
+ * Wraps {@link TcUtils#tcFilterAddDevIngressPolice}
+ */
+ public void enableIngressRateLimit(String iface, long rateInBytesPerSecond) {
+ final InterfaceParams params = InterfaceParams.getByName(iface);
+ if (params == null) {
+ // the interface might have disappeared.
+ logw("Failed to get interface params for interface " + iface);
+ return;
+ }
+ try {
+ // converting rateInBytesPerSecond from long to int is safe here because the
+ // setting's range is limited to INT_MAX.
+ // TODO: add long/uint64 support to tcFilterAddDevIngressPolice.
+ TcUtils.tcFilterAddDevIngressPolice(params.index, TC_PRIO_POLICE, (short) ETH_P_ALL,
+ (int) rateInBytesPerSecond, TC_POLICE_BPF_PROG_PATH);
+ } catch (IOException e) {
+ loge("TcUtils.tcFilterAddDevIngressPolice(ifaceIndex=" + params.index
+ + ", PRIO_POLICE, ETH_P_ALL, rateInBytesPerSecond="
+ + rateInBytesPerSecond + ", bpfProgPath=" + TC_POLICE_BPF_PROG_PATH
+ + ") failure: ", e);
+ }
+ }
+
+ /**
+ * Wraps {@link TcUtils#tcFilterDelDev}
+ */
+ public void disableIngressRateLimit(String iface) {
+ final InterfaceParams params = InterfaceParams.getByName(iface);
+ if (params == null) {
+ // the interface might have disappeared.
+ logw("Failed to get interface params for interface " + iface);
+ return;
+ }
+ try {
+ TcUtils.tcFilterDelDev(params.index, true, TC_PRIO_POLICE, (short) ETH_P_ALL);
+ } catch (IOException e) {
+ loge("TcUtils.tcFilterDelDev(ifaceIndex=" + params.index
+ + ", ingress=true, PRIO_POLICE, ETH_P_ALL) failure: ", e);
+ }
+ }
}
public ConnectivityService(Context context) {
@@ -1540,6 +1609,9 @@
} catch (ErrnoException e) {
loge("Unable to create DscpPolicyTracker");
}
+
+ mIngressRateLimit = ConnectivitySettingsManager.getIngressRateLimitInBytesPerSecond(
+ mContext);
}
private static NetworkCapabilities createDefaultNetworkCapabilitiesForUid(int uid) {
@@ -1610,6 +1682,11 @@
mHandler.sendEmptyMessage(EVENT_MOBILE_DATA_PREFERRED_UIDS_CHANGED);
}
+ @VisibleForTesting
+ void updateIngressRateLimit() {
+ mHandler.sendEmptyMessage(EVENT_INGRESS_RATE_LIMIT_CHANGED);
+ }
+
private void handleAlwaysOnNetworkRequest(NetworkRequest networkRequest, int id) {
final boolean enable = mContext.getResources().getBoolean(id);
handleAlwaysOnNetworkRequest(networkRequest, enable);
@@ -1671,6 +1748,12 @@
mSettingsObserver.observe(
Settings.Secure.getUriFor(ConnectivitySettingsManager.MOBILE_DATA_PREFERRED_UIDS),
EVENT_MOBILE_DATA_PREFERRED_UIDS_CHANGED);
+
+ // Watch for ingress rate limit changes.
+ mSettingsObserver.observe(
+ Settings.Secure.getUriFor(
+ ConnectivitySettingsManager.INGRESS_RATE_LIMIT_BYTES_PER_SECOND),
+ EVENT_INGRESS_RATE_LIMIT_CHANGED);
}
private void registerPrivateDnsSettingsCallbacks() {
@@ -4090,6 +4173,11 @@
// for an unnecessarily long time.
destroyNativeNetwork(nai);
mDnsManager.removeNetwork(nai.network);
+
+ // clean up tc police filters on interface.
+ if (canNetworkBeRateLimited(nai) && mIngressRateLimit >= 0) {
+ mDeps.disableIngressRateLimit(nai.linkProperties.getInterfaceName());
+ }
}
mNetIdManager.releaseNetId(nai.network.getNetId());
nai.onNetworkDestroyed();
@@ -5157,6 +5245,9 @@
final long timeMs = ((Long) msg.obj).longValue();
mMultinetworkPolicyTracker.setTestAllowBadWifiUntil(timeMs);
break;
+ case EVENT_INGRESS_RATE_LIMIT_CHANGED:
+ handleIngressRateLimitChanged();
+ break;
}
}
}
@@ -8853,6 +8944,19 @@
// A network that has just connected has zero requests and is thus a foreground network.
networkAgent.networkCapabilities.addCapability(NET_CAPABILITY_FOREGROUND);
+ // If a rate limit has been configured and is applicable to this network (network
+ // provides internet connectivity), apply it.
+ // Note: in case of a system server crash, there is a very small chance that this
+ // leaves some interfaces rate limited (i.e. if the rate limit had been changed just
+ // before the crash and was never applied). One solution would be to delete all
+ // potential tc police filters every time this is called. Since this is an unlikely
+ // scenario in the first place (and worst case, the interface stays rate limited until
+ // the device is rebooted), this seems a little overkill.
+ if (canNetworkBeRateLimited(networkAgent) && mIngressRateLimit >= 0) {
+ mDeps.enableIngressRateLimit(networkAgent.linkProperties.getInterfaceName(),
+ mIngressRateLimit);
+ }
+
if (!createNativeNetwork(networkAgent)) return;
if (networkAgent.propagateUnderlyingCapabilities()) {
// Initialize the network's capabilities to their starting values according to the
@@ -10511,6 +10615,39 @@
rematchAllNetworksAndRequests();
}
+ private void handleIngressRateLimitChanged() {
+ final long oldIngressRateLimit = mIngressRateLimit;
+ mIngressRateLimit = ConnectivitySettingsManager.getIngressRateLimitInBytesPerSecond(
+ mContext);
+ for (final NetworkAgentInfo networkAgent : mNetworkAgentInfos) {
+ if (canNetworkBeRateLimited(networkAgent)) {
+ // If rate limit has previously been enabled, remove the old limit first.
+ if (oldIngressRateLimit >= 0) {
+ mDeps.disableIngressRateLimit(networkAgent.linkProperties.getInterfaceName());
+ }
+ if (mIngressRateLimit >= 0) {
+ mDeps.enableIngressRateLimit(networkAgent.linkProperties.getInterfaceName(),
+ mIngressRateLimit);
+ }
+ }
+ }
+ }
+
+ private boolean canNetworkBeRateLimited(@NonNull final NetworkAgentInfo networkAgent) {
+ if (!networkAgent.networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET)) {
+ // rate limits only apply to networks that provide internet connectivity.
+ return false;
+ }
+
+ final String iface = networkAgent.linkProperties.getInterfaceName();
+ if (iface == null) {
+ // This can never happen.
+ logwtf("canNetworkBeRateLimited: LinkProperties#getInterfaceName returns null");
+ return false;
+ }
+ return true;
+ }
+
private void enforceAutomotiveDevice() {
PermissionUtils.enforceSystemFeature(mContext, PackageManager.FEATURE_AUTOMOTIVE,
"setOemNetworkPreference() is only available on automotive devices.");
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 0132525..3b831f4 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -179,7 +179,6 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.when;
import static java.util.Arrays.asList;
@@ -388,6 +387,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
@@ -1963,6 +1963,25 @@
public BpfNetMaps getBpfNetMaps(INetd netd) {
return mBpfNetMaps;
}
+
+ final ArrayTrackRecord<Pair<String, Long>> mRateLimitHistory = new ArrayTrackRecord<>();
+ final Map<String, Long> mActiveRateLimit = new HashMap<>();
+
+ @Override
+ public void enableIngressRateLimit(final String iface, final long rateInBytesPerSecond) {
+ mRateLimitHistory.add(new Pair<>(iface, rateInBytesPerSecond));
+ // Due to a TC limitation, the rate limit needs to be removed before it can be
+ // updated. Check that this happened.
+ assertEquals(-1L, (long) mActiveRateLimit.getOrDefault(iface, -1L));
+ mActiveRateLimit.put(iface, rateInBytesPerSecond);
+ }
+
+ @Override
+ public void disableIngressRateLimit(final String iface) {
+ mRateLimitHistory.add(new Pair<>(iface, -1L));
+ assertNotEquals(-1L, (long) mActiveRateLimit.getOrDefault(iface, -1L));
+ mActiveRateLimit.put(iface, -1L);
+ }
}
private static void initAlarmManager(final AlarmManager am, final Handler alarmHandler) {
@@ -5027,6 +5046,13 @@
waitForIdle();
}
+ private void setIngressRateLimit(int rateLimitInBytesPerSec) {
+ ConnectivitySettingsManager.setIngressRateLimitInBytesPerSecond(mServiceContext,
+ rateLimitInBytesPerSec);
+ mService.updateIngressRateLimit();
+ waitForIdle();
+ }
+
private boolean isForegroundNetwork(TestNetworkAgentWrapper network) {
NetworkCapabilities nc = mCm.getNetworkCapabilities(network.getNetwork());
assertNotNull(nc);
@@ -15339,4 +15365,153 @@
ConnectivityManager.TYPE_NONE, null /* hostAddress */, "com.not.package.owner",
null /* callingAttributionTag */));
}
+
+ @Test
+ public void testUpdateRateLimit_EnableDisable() throws Exception {
+ final LinkProperties wifiLp = new LinkProperties();
+ wifiLp.setInterfaceName(WIFI_IFNAME);
+ mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp);
+ mWiFiNetworkAgent.connect(true);
+
+ final LinkProperties cellLp = new LinkProperties();
+ cellLp.setInterfaceName(MOBILE_IFNAME);
+ mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
+ mCellNetworkAgent.connect(false);
+
+ waitForIdle();
+
+ final ArrayTrackRecord<Pair<String, Long>>.ReadHead readHeadWifi =
+ mDeps.mRateLimitHistory.newReadHead();
+ final ArrayTrackRecord<Pair<String, Long>>.ReadHead readHeadCell =
+ mDeps.mRateLimitHistory.newReadHead();
+
+ // set rate limit to 8MBit/s => 1MB/s
+ final int rateLimitInBytesPerSec = 1 * 1000 * 1000;
+ setIngressRateLimit(rateLimitInBytesPerSec);
+
+ assertNotNull(readHeadWifi.poll(TIMEOUT_MS,
+ it -> it.first == wifiLp.getInterfaceName()
+ && it.second == rateLimitInBytesPerSec));
+ assertNotNull(readHeadCell.poll(TIMEOUT_MS,
+ it -> it.first == cellLp.getInterfaceName()
+ && it.second == rateLimitInBytesPerSec));
+
+ // disable rate limiting
+ setIngressRateLimit(-1);
+
+ assertNotNull(readHeadWifi.poll(TIMEOUT_MS,
+ it -> it.first == wifiLp.getInterfaceName() && it.second == -1));
+ assertNotNull(readHeadCell.poll(TIMEOUT_MS,
+ it -> it.first == cellLp.getInterfaceName() && it.second == -1));
+ }
+
+ @Test
+ public void testUpdateRateLimit_WhenNewNetworkIsAdded() throws Exception {
+ final LinkProperties wifiLp = new LinkProperties();
+ wifiLp.setInterfaceName(WIFI_IFNAME);
+ mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp);
+ mWiFiNetworkAgent.connect(true);
+
+ waitForIdle();
+
+ final ArrayTrackRecord<Pair<String, Long>>.ReadHead readHead =
+ mDeps.mRateLimitHistory.newReadHead();
+
+ // set rate limit to 8MBit/s => 1MB/s
+ final int rateLimitInBytesPerSec = 1 * 1000 * 1000;
+ setIngressRateLimit(rateLimitInBytesPerSec);
+ assertNotNull(readHead.poll(TIMEOUT_MS, it -> it.first == wifiLp.getInterfaceName()
+ && it.second == rateLimitInBytesPerSec));
+
+ final LinkProperties cellLp = new LinkProperties();
+ cellLp.setInterfaceName(MOBILE_IFNAME);
+ mCellNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR, cellLp);
+ mCellNetworkAgent.connect(false);
+ assertNotNull(readHead.poll(TIMEOUT_MS, it -> it.first == cellLp.getInterfaceName()
+ && it.second == rateLimitInBytesPerSec));
+ }
+
+ @Test
+ public void testUpdateRateLimit_OnlyAffectsInternetCapableNetworks() throws Exception {
+ final LinkProperties wifiLp = new LinkProperties();
+ wifiLp.setInterfaceName(WIFI_IFNAME);
+
+ mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp);
+ mWiFiNetworkAgent.connectWithoutInternet();
+
+ waitForIdle();
+
+ setIngressRateLimit(1000);
+ setIngressRateLimit(-1);
+
+ final ArrayTrackRecord<Pair<String, Long>>.ReadHead readHeadWifi =
+ mDeps.mRateLimitHistory.newReadHead();
+ assertNull(readHeadWifi.poll(TIMEOUT_MS, it -> it.first == wifiLp.getInterfaceName()));
+ }
+
+ @Test
+ public void testUpdateRateLimit_DisconnectingResetsRateLimit()
+ throws Exception {
+ // Steps:
+ // - connect network
+ // - set rate limit
+ // - disconnect network (interface still exists)
+ // - disable rate limit
+ // - connect network
+ // - ensure network interface is not rate limited
+ final LinkProperties wifiLp = new LinkProperties();
+ wifiLp.setInterfaceName(WIFI_IFNAME);
+ mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp);
+ mWiFiNetworkAgent.connect(true);
+ waitForIdle();
+
+ final ArrayTrackRecord<Pair<String, Long>>.ReadHead readHeadWifi =
+ mDeps.mRateLimitHistory.newReadHead();
+
+ int rateLimitInBytesPerSec = 1000;
+ setIngressRateLimit(rateLimitInBytesPerSec);
+ assertNotNull(readHeadWifi.poll(TIMEOUT_MS,
+ it -> it.first == wifiLp.getInterfaceName()
+ && it.second == rateLimitInBytesPerSec));
+
+ mWiFiNetworkAgent.disconnect();
+ assertNotNull(readHeadWifi.poll(TIMEOUT_MS,
+ it -> it.first == wifiLp.getInterfaceName() && it.second == -1));
+
+ setIngressRateLimit(-1);
+
+ mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp);
+ mWiFiNetworkAgent.connect(true);
+ assertNull(readHeadWifi.poll(TIMEOUT_MS, it -> it.first == wifiLp.getInterfaceName()));
+ }
+
+ @Test
+ public void testUpdateRateLimit_UpdateExistingRateLimit() throws Exception {
+ final LinkProperties wifiLp = new LinkProperties();
+ wifiLp.setInterfaceName(WIFI_IFNAME);
+ mWiFiNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_WIFI, wifiLp);
+ mWiFiNetworkAgent.connect(true);
+ waitForIdle();
+
+ final ArrayTrackRecord<Pair<String, Long>>.ReadHead readHeadWifi =
+ mDeps.mRateLimitHistory.newReadHead();
+
+ // update an active ingress rate limit
+ setIngressRateLimit(1000);
+ setIngressRateLimit(2000);
+
+ // verify the following order of execution:
+ // 1. ingress rate limit set to 1000.
+ // 2. ingress rate limit disabled (triggered by updating active rate limit).
+ // 3. ingress rate limit set to 2000.
+ assertNotNull(readHeadWifi.poll(TIMEOUT_MS,
+ it -> it.first == wifiLp.getInterfaceName()
+ && it.second == 1000));
+ assertNotNull(readHeadWifi.poll(TIMEOUT_MS,
+ it -> it.first == wifiLp.getInterfaceName()
+ && it.second == -1));
+ assertNotNull(readHeadWifi.poll(TIMEOUT_MS,
+ it -> it.first == wifiLp.getInterfaceName()
+ && it.second == 2000));
+ }
}