/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.connectivity.tethering; import static android.net.NetworkStats.SET_DEFAULT; import static android.net.NetworkStats.STATS_PER_UID; import static android.net.NetworkStats.TAG_NONE; import static android.net.NetworkStats.UID_ALL; import static android.net.TrafficStats.UID_TETHERING; import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED; import android.content.ContentResolver; import android.net.ITetheringStatsProvider; import android.net.IpPrefix; import android.net.LinkAddress; import android.net.LinkProperties; import android.net.NetworkStats; import android.net.RouteInfo; import android.net.netlink.ConntrackMessage; import android.net.netlink.NetlinkConstants; import android.net.netlink.NetlinkSocket; import android.net.util.IpUtils; import android.net.util.SharedLog; import android.os.Handler; import android.os.Looper; import android.os.INetworkManagementService; import android.os.RemoteException; import android.os.SystemClock; import android.provider.Settings; import android.system.ErrnoException; import android.system.OsConstants; import android.text.TextUtils; import com.android.internal.util.IndentingPrintWriter; import com.android.server.connectivity.tethering.OffloadHardwareInterface.ForwardedStats; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.util.ArrayList; import java.util.Collections; 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.ConcurrentHashMap; import java.util.concurrent.TimeUnit; /** * A class to encapsulate the business logic of programming the tethering * hardware offload interface. * * @hide */ public class OffloadController { private static final String TAG = OffloadController.class.getSimpleName(); private static final boolean DBG = false; private static final String ANYIP = "0.0.0.0"; private static final ForwardedStats EMPTY_STATS = new ForwardedStats(); private static enum UpdateType { IF_NEEDED, FORCE }; private final Handler mHandler; private final OffloadHardwareInterface mHwInterface; private final ContentResolver mContentResolver; private final INetworkManagementService mNms; private final ITetheringStatsProvider mStatsProvider; private final SharedLog mLog; private final HashMap mDownstreams; private boolean mConfigInitialized; private boolean mControlInitialized; private LinkProperties mUpstreamLinkProperties; // The complete set of offload-exempt prefixes passed in via Tethering from // all upstream and downstream sources. private Set mExemptPrefixes; // A strictly "smaller" set of prefixes, wherein offload-approved prefixes // (e.g. downstream on-link prefixes) have been removed and replaced with // prefixes representing only the locally-assigned IP addresses. private Set mLastLocalPrefixStrs; // Maps upstream interface names to offloaded traffic statistics. // Always contains the latest value received from the hardware for each interface, regardless of // whether offload is currently running on that interface. private ConcurrentHashMap mForwardedStats = new ConcurrentHashMap<>(16, 0.75F, 1); // Maps upstream interface names to interface quotas. // Always contains the latest value received from the framework for each interface, regardless // of whether offload is currently running (or is even supported) on that interface. Only // includes upstream interfaces that have a quota set. private HashMap mInterfaceQuotas = new HashMap<>(); private int mNatUpdateCallbacksReceived; private int mNatUpdateNetlinkErrors; public OffloadController(Handler h, OffloadHardwareInterface hwi, ContentResolver contentResolver, INetworkManagementService nms, SharedLog log) { mHandler = h; mHwInterface = hwi; mContentResolver = contentResolver; mNms = nms; mStatsProvider = new OffloadTetheringStatsProvider(); mLog = log.forSubComponent(TAG); mDownstreams = new HashMap<>(); mExemptPrefixes = new HashSet<>(); mLastLocalPrefixStrs = new HashSet<>(); try { mNms.registerTetheringStatsProvider(mStatsProvider, getClass().getSimpleName()); } catch (RemoteException e) { mLog.e("Cannot register offload stats provider: " + e); } } public boolean start() { if (started()) return true; if (isOffloadDisabled()) { mLog.i("tethering offload disabled"); return false; } if (!mConfigInitialized) { mConfigInitialized = mHwInterface.initOffloadConfig(); if (!mConfigInitialized) { mLog.i("tethering offload config not supported"); stop(); return false; } } mControlInitialized = mHwInterface.initOffloadControl( // OffloadHardwareInterface guarantees that these callback // methods are called on the handler passed to it, which is the // same as mHandler, as coordinated by the setup in Tethering. new OffloadHardwareInterface.ControlCallback() { @Override public void onStarted() { if (!started()) return; mLog.log("onStarted"); } @Override public void onStoppedError() { if (!started()) return; mLog.log("onStoppedError"); } @Override public void onStoppedUnsupported() { if (!started()) return; mLog.log("onStoppedUnsupported"); // Poll for statistics and trigger a sweep of tethering // stats by observers. This might not succeed, but it's // worth trying anyway. We need to do this because from // this point on we continue with software forwarding, // and we need to synchronize stats and limits between // software and hardware forwarding. updateStatsForAllUpstreams(); forceTetherStatsPoll(); } @Override public void onSupportAvailable() { if (!started()) return; mLog.log("onSupportAvailable"); // [1] Poll for statistics and trigger a sweep of stats // by observers. We need to do this to ensure that any // limits set take into account any software tethering // traffic that has been happening in the meantime. updateStatsForAllUpstreams(); forceTetherStatsPoll(); // [2] (Re)Push all state. computeAndPushLocalPrefixes(UpdateType.FORCE); pushAllDownstreamState(); pushUpstreamParameters(null); } @Override public void onStoppedLimitReached() { if (!started()) return; mLog.log("onStoppedLimitReached"); // We cannot reliably determine on which interface the limit was reached, // because the HAL interface does not specify it. We cannot just use the // current upstream, because that might have changed since the time that // the HAL queued the callback. // TODO: rev the HAL so that it provides an interface name. // Fetch current stats, so that when our notification reaches // NetworkStatsService and triggers a poll, we will respond with // current data (which will be above the limit that was reached). // Note that if we just changed upstream, this is unnecessary but harmless. // The stats for the previous upstream were already updated on this thread // just after the upstream was changed, so they are also up-to-date. updateStatsForCurrentUpstream(); forceTetherStatsPoll(); } @Override public void onNatTimeoutUpdate(int proto, String srcAddr, int srcPort, String dstAddr, int dstPort) { if (!started()) return; updateNatTimeout(proto, srcAddr, srcPort, dstAddr, dstPort); } }); final boolean isStarted = started(); if (!isStarted) { mLog.i("tethering offload control not supported"); stop(); } else { mLog.log("tethering offload started"); mNatUpdateCallbacksReceived = 0; mNatUpdateNetlinkErrors = 0; } return isStarted; } public void stop() { // Completely stops tethering offload. After this method is called, it is no longer safe to // call any HAL method, no callbacks from the hardware will be delivered, and any in-flight // callbacks must be ignored. Offload may be started again by calling start(). final boolean wasStarted = started(); updateStatsForCurrentUpstream(); mUpstreamLinkProperties = null; mHwInterface.stopOffloadControl(); mControlInitialized = false; mConfigInitialized = false; if (wasStarted) mLog.log("tethering offload stopped"); } private boolean started() { return mConfigInitialized && mControlInitialized; } private class OffloadTetheringStatsProvider extends ITetheringStatsProvider.Stub { @Override public NetworkStats getTetherStats(int how) { // getTetherStats() is the only function in OffloadController that can be called from // a different thread. Do not attempt to update stats by querying the offload HAL // synchronously from a different thread than our Handler thread. http://b/64771555. Runnable updateStats = () -> { updateStatsForCurrentUpstream(); }; if (Looper.myLooper() == mHandler.getLooper()) { updateStats.run(); } else { mHandler.post(updateStats); } NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 0); NetworkStats.Entry entry = new NetworkStats.Entry(); entry.set = SET_DEFAULT; entry.tag = TAG_NONE; entry.uid = (how == STATS_PER_UID) ? UID_TETHERING : UID_ALL; for (Map.Entry kv : mForwardedStats.entrySet()) { ForwardedStats value = kv.getValue(); entry.iface = kv.getKey(); entry.rxBytes = value.rxBytes; entry.txBytes = value.txBytes; stats.addValues(entry); } return stats; } public void setInterfaceQuota(String iface, long quotaBytes) { mHandler.post(() -> { if (quotaBytes == ITetheringStatsProvider.QUOTA_UNLIMITED) { mInterfaceQuotas.remove(iface); } else { mInterfaceQuotas.put(iface, quotaBytes); } maybeUpdateDataLimit(iface); }); } } private String currentUpstreamInterface() { return (mUpstreamLinkProperties != null) ? mUpstreamLinkProperties.getInterfaceName() : null; } private void maybeUpdateStats(String iface) { if (TextUtils.isEmpty(iface)) { return; } // Always called on the handler thread. // // Use get()/put() instead of updating ForwardedStats in place because we can be called // concurrently with getTetherStats. In combination with the guarantees provided by // ConcurrentHashMap, this ensures that getTetherStats always gets the most recent copy of // the stats for each interface, and does not observe partial writes where rxBytes is // updated and txBytes is not. ForwardedStats diff = mHwInterface.getForwardedStats(iface); ForwardedStats base = mForwardedStats.get(iface); if (base != null) { diff.add(base); } mForwardedStats.put(iface, diff); // diff is a new object, just created by getForwardedStats(). Therefore, anyone reading from // mForwardedStats (i.e., any caller of getTetherStats) will see the new stats immediately. } private boolean maybeUpdateDataLimit(String iface) { // setDataLimit may only be called while offload is occurring on this upstream. if (!started() || !TextUtils.equals(iface, currentUpstreamInterface())) { return true; } Long limit = mInterfaceQuotas.get(iface); if (limit == null) { limit = Long.MAX_VALUE; } return mHwInterface.setDataLimit(iface, limit); } private void updateStatsForCurrentUpstream() { maybeUpdateStats(currentUpstreamInterface()); } private void updateStatsForAllUpstreams() { // In practice, there should only ever be a single digit number of // upstream interfaces over the lifetime of an active tethering session. // Roughly speaking, imagine a very ambitious one or two of each of the // following interface types: [ "rmnet_data", "wlan", "eth", "rndis" ]. for (Map.Entry kv : mForwardedStats.entrySet()) { maybeUpdateStats(kv.getKey()); } } private void forceTetherStatsPoll() { try { mNms.tetherLimitReached(mStatsProvider); } catch (RemoteException e) { mLog.e("Cannot report data limit reached: " + e); } } public void setUpstreamLinkProperties(LinkProperties lp) { if (!started() || Objects.equals(mUpstreamLinkProperties, lp)) return; final String prevUpstream = currentUpstreamInterface(); mUpstreamLinkProperties = (lp != null) ? new LinkProperties(lp) : null; // Make sure we record this interface in the ForwardedStats map. final String iface = currentUpstreamInterface(); if (!TextUtils.isEmpty(iface)) mForwardedStats.putIfAbsent(iface, EMPTY_STATS); // TODO: examine return code and decide what to do if programming // upstream parameters fails (probably just wait for a subsequent // onOffloadEvent() callback to tell us offload is available again and // then reapply all state). computeAndPushLocalPrefixes(UpdateType.IF_NEEDED); pushUpstreamParameters(prevUpstream); } public void setLocalPrefixes(Set localPrefixes) { mExemptPrefixes = localPrefixes; if (!started()) return; computeAndPushLocalPrefixes(UpdateType.IF_NEEDED); } public void notifyDownstreamLinkProperties(LinkProperties lp) { final String ifname = lp.getInterfaceName(); final LinkProperties oldLp = mDownstreams.put(ifname, new LinkProperties(lp)); if (Objects.equals(oldLp, lp)) return; if (!started()) return; pushDownstreamState(oldLp, lp); } private void pushDownstreamState(LinkProperties oldLp, LinkProperties newLp) { final String ifname = newLp.getInterfaceName(); final List oldRoutes = (oldLp != null) ? oldLp.getRoutes() : Collections.EMPTY_LIST; final List newRoutes = newLp.getRoutes(); // For each old route, if not in new routes: remove. for (RouteInfo ri : oldRoutes) { if (shouldIgnoreDownstreamRoute(ri)) continue; if (!newRoutes.contains(ri)) { mHwInterface.removeDownstreamPrefix(ifname, ri.getDestination().toString()); } } // For each new route, if not in old routes: add. for (RouteInfo ri : newRoutes) { if (shouldIgnoreDownstreamRoute(ri)) continue; if (!oldRoutes.contains(ri)) { mHwInterface.addDownstreamPrefix(ifname, ri.getDestination().toString()); } } } private void pushAllDownstreamState() { for (LinkProperties lp : mDownstreams.values()) { pushDownstreamState(null, lp); } } public void removeDownstreamInterface(String ifname) { final LinkProperties lp = mDownstreams.remove(ifname); if (lp == null) return; if (!started()) return; for (RouteInfo route : lp.getRoutes()) { if (shouldIgnoreDownstreamRoute(route)) continue; mHwInterface.removeDownstreamPrefix(ifname, route.getDestination().toString()); } } private boolean isOffloadDisabled() { final int defaultDisposition = mHwInterface.getDefaultTetherOffloadDisabled(); return (Settings.Global.getInt( mContentResolver, TETHER_OFFLOAD_DISABLED, defaultDisposition) != 0); } private boolean pushUpstreamParameters(String prevUpstream) { final String iface = currentUpstreamInterface(); if (TextUtils.isEmpty(iface)) { final boolean rval = mHwInterface.setUpstreamParameters("", ANYIP, ANYIP, null); // Update stats after we've told the hardware to stop forwarding so // we don't miss packets. maybeUpdateStats(prevUpstream); return rval; } // A stacked interface cannot be an upstream for hardware offload. // Consequently, we examine only the primary interface name, look at // getAddresses() rather than getAllAddresses(), and check getRoutes() // rather than getAllRoutes(). final ArrayList v6gateways = new ArrayList<>(); String v4addr = null; String v4gateway = null; for (InetAddress ip : mUpstreamLinkProperties.getAddresses()) { if (ip instanceof Inet4Address) { v4addr = ip.getHostAddress(); break; } } // Find the gateway addresses of all default routes of either address family. for (RouteInfo ri : mUpstreamLinkProperties.getRoutes()) { if (!ri.hasGateway()) continue; final String gateway = ri.getGateway().getHostAddress(); if (ri.isIPv4Default()) { v4gateway = gateway; } else if (ri.isIPv6Default()) { v6gateways.add(gateway); } } boolean success = mHwInterface.setUpstreamParameters( iface, v4addr, v4gateway, (v6gateways.isEmpty() ? null : v6gateways)); if (!success) { return success; } // Update stats after we've told the hardware to change routing so we don't miss packets. maybeUpdateStats(prevUpstream); // Data limits can only be set once offload is running on the upstream. success = maybeUpdateDataLimit(iface); if (!success) { // If we failed to set a data limit, don't use this upstream, because we don't want to // blow through the data limit that we were told to apply. mLog.log("Setting data limit for " + iface + " failed, disabling offload."); stop(); } return success; } private boolean computeAndPushLocalPrefixes(UpdateType how) { final boolean force = (how == UpdateType.FORCE); final Set localPrefixStrs = computeLocalPrefixStrings( mExemptPrefixes, mUpstreamLinkProperties); if (!force && mLastLocalPrefixStrs.equals(localPrefixStrs)) return true; mLastLocalPrefixStrs = localPrefixStrs; return mHwInterface.setLocalPrefixes(new ArrayList<>(localPrefixStrs)); } // TODO: Factor in downstream LinkProperties once that information is available. private static Set computeLocalPrefixStrings( Set localPrefixes, LinkProperties upstreamLinkProperties) { // Create an editable copy. final Set prefixSet = new HashSet<>(localPrefixes); // TODO: If a downstream interface (not currently passed in) is reusing // the /64 of the upstream (64share) then: // // [a] remove that /64 from the local prefixes // [b] add in /128s for IP addresses on the downstream interface // [c] add in /128s for IP addresses on the upstream interface // // Until downstream information is available here, simply add /128s from // the upstream network; they'll just be redundant with their /64. if (upstreamLinkProperties != null) { for (LinkAddress linkAddr : upstreamLinkProperties.getLinkAddresses()) { if (!linkAddr.isGlobalPreferred()) continue; final InetAddress ip = linkAddr.getAddress(); if (!(ip instanceof Inet6Address)) continue; prefixSet.add(new IpPrefix(ip, 128)); } } final HashSet localPrefixStrs = new HashSet<>(); for (IpPrefix pfx : prefixSet) localPrefixStrs.add(pfx.toString()); return localPrefixStrs; } private static boolean shouldIgnoreDownstreamRoute(RouteInfo route) { // Ignore any link-local routes. if (!route.getDestinationLinkAddress().isGlobalPreferred()) return true; return false; } public void dump(IndentingPrintWriter pw) { if (isOffloadDisabled()) { pw.println("Offload disabled"); return; } final boolean isStarted = started(); pw.println("Offload HALs " + (isStarted ? "started" : "not started")); LinkProperties lp = mUpstreamLinkProperties; String upstream = (lp != null) ? lp.getInterfaceName() : null; pw.println("Current upstream: " + upstream); pw.println("Exempt prefixes: " + mLastLocalPrefixStrs); pw.println("NAT timeout update callbacks received during the " + (isStarted ? "current" : "last") + " offload session: " + mNatUpdateCallbacksReceived); pw.println("NAT timeout update netlink errors during the " + (isStarted ? "current" : "last") + " offload session: " + mNatUpdateNetlinkErrors); } private void updateNatTimeout( int proto, String srcAddr, int srcPort, String dstAddr, int dstPort) { final String protoName = protoNameFor(proto); if (protoName == null) { mLog.e("Unknown NAT update callback protocol: " + proto); return; } final Inet4Address src = parseIPv4Address(srcAddr); if (src == null) { mLog.e("Failed to parse IPv4 address: " + srcAddr); return; } if (!IpUtils.isValidUdpOrTcpPort(srcPort)) { mLog.e("Invalid src port: " + srcPort); return; } final Inet4Address dst = parseIPv4Address(dstAddr); if (dst == null) { mLog.e("Failed to parse IPv4 address: " + dstAddr); return; } if (!IpUtils.isValidUdpOrTcpPort(dstPort)) { mLog.e("Invalid dst port: " + dstPort); return; } mNatUpdateCallbacksReceived++; final String natDescription = String.format("%s (%s, %s) -> (%s, %s)", protoName, srcAddr, srcPort, dstAddr, dstPort); if (DBG) { mLog.log("NAT timeout update: " + natDescription); } final int timeoutSec = connectionTimeoutUpdateSecondsFor(proto); final byte[] msg = ConntrackMessage.newIPv4TimeoutUpdateRequest( proto, src, srcPort, dst, dstPort, timeoutSec); try { NetlinkSocket.sendOneShotKernelMessage(OsConstants.NETLINK_NETFILTER, msg); } catch (ErrnoException e) { mNatUpdateNetlinkErrors++; mLog.e("Error updating NAT conntrack entry >" + natDescription + "<: " + e + ", msg: " + NetlinkConstants.hexify(msg)); mLog.log("NAT timeout update callbacks received: " + mNatUpdateCallbacksReceived); mLog.log("NAT timeout update netlink errors: " + mNatUpdateNetlinkErrors); } } private static Inet4Address parseIPv4Address(String addrString) { try { final InetAddress ip = InetAddress.parseNumericAddress(addrString); // TODO: Consider other sanitization steps here, including perhaps: // not eql to 0.0.0.0 // not within 169.254.0.0/16 // not within ::ffff:0.0.0.0/96 // not within ::/96 // et cetera. if (ip instanceof Inet4Address) { return (Inet4Address) ip; } } catch (IllegalArgumentException iae) {} return null; } private static String protoNameFor(int proto) { // OsConstants values are not constant expressions; no switch statement. if (proto == OsConstants.IPPROTO_UDP) { return "UDP"; } else if (proto == OsConstants.IPPROTO_TCP) { return "TCP"; } return null; } private static int connectionTimeoutUpdateSecondsFor(int proto) { // TODO: Replace this with more thoughtful work, perhaps reading from // and maybe writing to any required // // /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_* // /proc/sys/net/netfilter/nf_conntrack_udp_timeout{,_stream} // // entries. TBD. if (proto == OsConstants.IPPROTO_TCP) { // Cf. /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established return 432000; } else { // Cf. /proc/sys/net/netfilter/nf_conntrack_udp_timeout_stream return 180; } } }