From a20843638f7340ce4c5198bdb489b8ef66722f73 Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Mon, 3 Jan 2022 15:55:52 +0100 Subject: [PATCH 1/2] Add setting that controls network rate limit The INGRESS_RATE_LIMIT_BYTES_PER_SECOND setting controls the rate limit for internet networks. If set to -1, no rate limit applies. There is one global rate limit that will be applied to all networks with NET_CAPABILITY_INTERNET. Test: atest ConnectivitySettingsManagerTest Bug: 157552970 Change-Id: Ia82aa867686d484ce46734f76d4a48bf864eff84 --- framework/api/module-lib-current.txt | 2 + .../net/ConnectivitySettingsManager.java | 41 +++++++++++++++++++ .../net/ConnectivitySettingsManagerTest.kt | 17 ++++++++ 3 files changed, 60 insertions(+) diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt index 5961e722b9..e66039ec36 100644 --- a/framework/api/module-lib-current.txt +++ b/framework/api/module-lib-current.txt @@ -69,6 +69,7 @@ package android.net { method @NonNull public static java.time.Duration getDnsResolverSampleValidityDuration(@NonNull android.content.Context, @NonNull java.time.Duration); method public static int getDnsResolverSuccessThresholdPercent(@NonNull android.content.Context, int); method @Nullable public static android.net.ProxyInfo getGlobalProxy(@NonNull android.content.Context); + method public static long getIngressRateLimitInBytesPerSecond(@NonNull android.content.Context); method @NonNull public static java.time.Duration getMobileDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration); method public static boolean getMobileDataAlwaysOn(@NonNull android.content.Context, boolean); method @NonNull public static java.util.Set getMobileDataPreferredUids(@NonNull android.content.Context); @@ -89,6 +90,7 @@ package android.net { method public static void setDnsResolverSampleValidityDuration(@NonNull android.content.Context, @NonNull java.time.Duration); method public static void setDnsResolverSuccessThresholdPercent(@NonNull android.content.Context, @IntRange(from=0, to=100) int); method public static void setGlobalProxy(@NonNull android.content.Context, @NonNull android.net.ProxyInfo); + method public static void setIngressRateLimitInBytesPerSecond(@NonNull android.content.Context, @IntRange(from=0xffffffff, to=java.lang.Integer.MAX_VALUE) long); method public static void setMobileDataActivityTimeout(@NonNull android.content.Context, @NonNull java.time.Duration); method public static void setMobileDataAlwaysOn(@NonNull android.content.Context, boolean); method public static void setMobileDataPreferredUids(@NonNull android.content.Context, @NonNull java.util.Set); diff --git a/framework/src/android/net/ConnectivitySettingsManager.java b/framework/src/android/net/ConnectivitySettingsManager.java index 8fc0065347..4e28b29a85 100644 --- a/framework/src/android/net/ConnectivitySettingsManager.java +++ b/framework/src/android/net/ConnectivitySettingsManager.java @@ -383,6 +383,14 @@ public class ConnectivitySettingsManager { public static final String UIDS_ALLOWED_ON_RESTRICTED_NETWORKS = "uids_allowed_on_restricted_networks"; + /** + * A global rate limit that applies to all networks with NET_CAPABILITY_INTERNET when enabled. + * + * @hide + */ + public static final String INGRESS_RATE_LIMIT_BYTES_PER_SECOND = + "ingress_rate_limit_bytes_per_second"; + /** * Get mobile data activity timeout from {@link Settings}. * @@ -1071,4 +1079,37 @@ public class ConnectivitySettingsManager { Settings.Global.putString(context.getContentResolver(), UIDS_ALLOWED_ON_RESTRICTED_NETWORKS, uids); } + + /** + * Get the global network bandwidth rate limit. + * + * The limit is only applicable to networks that provide internet connectivity. If the setting + * is unset, it defaults to -1. + * + * @param context The {@link Context} to query the setting. + * @return The rate limit in number of bytes per second or -1 if disabled. + */ + public static long getIngressRateLimitInBytesPerSecond(@NonNull Context context) { + return Settings.Global.getLong(context.getContentResolver(), + INGRESS_RATE_LIMIT_BYTES_PER_SECOND, -1); + } + + /** + * Set the global network bandwidth rate limit. + * + * The limit is only applicable to networks that provide internet connectivity. + * + * @param context The {@link Context} to set the setting. + * @param rateLimitInBytesPerSec The rate limit in number of bytes per second or -1 to disable. + */ + public static void setIngressRateLimitInBytesPerSecond(@NonNull Context context, + @IntRange(from = -1, to = Integer.MAX_VALUE) long rateLimitInBytesPerSec) { + if (rateLimitInBytesPerSec < -1) { + throw new IllegalArgumentException( + "Rate limit must be within the range [-1, Integer.MAX_VALUE]"); + } + Settings.Global.putLong(context.getContentResolver(), + INGRESS_RATE_LIMIT_BYTES_PER_SECOND, + rateLimitInBytesPerSec); + } } diff --git a/tests/common/java/android/net/ConnectivitySettingsManagerTest.kt b/tests/common/java/android/net/ConnectivitySettingsManagerTest.kt index ebaa787cc6..8d8958dd93 100644 --- a/tests/common/java/android/net/ConnectivitySettingsManagerTest.kt +++ b/tests/common/java/android/net/ConnectivitySettingsManagerTest.kt @@ -39,6 +39,7 @@ import android.net.ConnectivitySettingsManager.getConnectivityKeepPendingIntentD import android.net.ConnectivitySettingsManager.getDnsResolverSampleRanges import android.net.ConnectivitySettingsManager.getDnsResolverSampleValidityDuration import android.net.ConnectivitySettingsManager.getDnsResolverSuccessThresholdPercent +import android.net.ConnectivitySettingsManager.getIngressRateLimitInBytesPerSecond import android.net.ConnectivitySettingsManager.getMobileDataActivityTimeout import android.net.ConnectivitySettingsManager.getMobileDataAlwaysOn import android.net.ConnectivitySettingsManager.getNetworkSwitchNotificationMaximumDailyCount @@ -51,6 +52,7 @@ import android.net.ConnectivitySettingsManager.setConnectivityKeepPendingIntentD import android.net.ConnectivitySettingsManager.setDnsResolverSampleRanges import android.net.ConnectivitySettingsManager.setDnsResolverSampleValidityDuration import android.net.ConnectivitySettingsManager.setDnsResolverSuccessThresholdPercent +import android.net.ConnectivitySettingsManager.setIngressRateLimitInBytesPerSecond import android.net.ConnectivitySettingsManager.setMobileDataActivityTimeout import android.net.ConnectivitySettingsManager.setMobileDataAlwaysOn import android.net.ConnectivitySettingsManager.setNetworkSwitchNotificationMaximumDailyCount @@ -292,4 +294,19 @@ class ConnectivitySettingsManagerTest { setter = { setWifiAlwaysRequested(context, it) }, testIntValues = intArrayOf(0)) } + + @Test + fun testInternetNetworkRateLimitInBytesPerSecond() { + val defaultRate = getIngressRateLimitInBytesPerSecond(context) + val testRate = 1000L + setIngressRateLimitInBytesPerSecond(context, testRate) + assertEquals(testRate, getIngressRateLimitInBytesPerSecond(context)) + + setIngressRateLimitInBytesPerSecond(context, defaultRate) + assertEquals(defaultRate, getIngressRateLimitInBytesPerSecond(context)) + + assertFailsWith("Expected failure, but setting accepted") { + setIngressRateLimitInBytesPerSecond(context, -10) + } + } } \ No newline at end of file From 2857ac4dd09b0aea9015caa6ea8f80392fb6987a Mon Sep 17 00:00:00 2001 From: Patrick Rohr Date: Fri, 21 Jan 2022 14:58:16 +0100 Subject: [PATCH 2/2] 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 --- .../android/server/ConnectivityService.java | 137 ++++++++++++++ .../server/ConnectivityServiceTest.java | 177 +++++++++++++++++- 2 files changed, 313 insertions(+), 1 deletion(-) diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java index d833bc2f0c..ac795c847f 100644 --- a/service/src/com/android/server/ConnectivityService.java +++ b/service/src/com/android/server/ConnectivityService.java @@ -90,6 +90,7 @@ import static android.net.shared.NetworkMonitorUtils.isPrivateDnsValidationRequi 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.IDnsResolverUnsolicitedEventListener; 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.LinkPropertiesUtils.CompareResult; 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 com.android.server.connectivity.UidRangeUtils; import libcore.io.IoUtils; import java.io.FileDescriptor; +import java.io.IOException; import java.io.PrintWriter; import java.io.Writer; import java.net.Inet4Address; @@ -707,6 +711,11 @@ public class ConnectivityService extends IConnectivityManager.Stub */ 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 @@ public class ConnectivityService extends IConnectivityManager.Stub */ 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 @@ public class ConnectivityService extends IConnectivityManager.Stub final Map 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 class ConnectivityService extends IConnectivityManager.Stub 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 @@ public class ConnectivityService extends IConnectivityManager.Stub } catch (ErrnoException e) { loge("Unable to create DscpPolicyTracker"); } + + mIngressRateLimit = ConnectivitySettingsManager.getIngressRateLimitInBytesPerSecond( + mContext); } private static NetworkCapabilities createDefaultNetworkCapabilitiesForUid(int uid) { @@ -1610,6 +1682,11 @@ public class ConnectivityService extends IConnectivityManager.Stub 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 @@ public class ConnectivityService extends IConnectivityManager.Stub 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 @@ public class ConnectivityService extends IConnectivityManager.Stub // 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 @@ public class ConnectivityService extends IConnectivityManager.Stub final long timeMs = ((Long) msg.obj).longValue(); mMultinetworkPolicyTracker.setTestAllowBadWifiUntil(timeMs); break; + case EVENT_INGRESS_RATE_LIMIT_CHANGED: + handleIngressRateLimitChanged(); + break; } } } @@ -8853,6 +8944,19 @@ public class ConnectivityService extends IConnectivityManager.Stub // 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 @@ public class ConnectivityService extends IConnectivityManager.Stub 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 013252576e..3b831f442c 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.timeout; 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.Comparator; 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 class ConnectivityServiceTest { public BpfNetMaps getBpfNetMaps(INetd netd) { return mBpfNetMaps; } + + final ArrayTrackRecord> mRateLimitHistory = new ArrayTrackRecord<>(); + final Map 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 @@ public class ConnectivityServiceTest { 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 @@ public class ConnectivityServiceTest { 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>.ReadHead readHeadWifi = + mDeps.mRateLimitHistory.newReadHead(); + final ArrayTrackRecord>.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>.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>.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>.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>.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)); + } }