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)); + } }