diff --git a/Tethering/Android.bp b/Tethering/Android.bp index 998572fc56..61bfb92363 100644 --- a/Tethering/Android.bp +++ b/Tethering/Android.bp @@ -27,6 +27,7 @@ java_defaults { "androidx.annotation_annotation", "netd_aidl_interface-java", "networkstack-aidl-interfaces-java", + "android.hardware.tetheroffload.control-V1.0-java", "tethering-client", ], manifest: "AndroidManifestBase.xml", @@ -38,11 +39,39 @@ android_library { defaults: ["TetheringAndroidLibraryDefaults"], } +cc_library_shared { + name: "libtetheroffloadjni", + srcs: [ + "jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp", + ], + shared_libs: [ + "libnativehelper", + "libcutils", + "android.hardware.tetheroffload.config@1.0", + ], + static_libs: [ + "liblog", + "libbase", + "libhidlbase", + "libutils", + ], + + cflags: [ + "-Wall", + "-Werror", + "-Wno-unused-parameter", + "-Wthread-safety", + ], +} + // Common defaults for compiling the actual APK. java_defaults { name: "TetheringAppDefaults", platform_apis: true, privileged: true, + jni_libs: [ + "libtetheroffloadjni", + ], resource_dirs: [ "res", ], @@ -71,6 +100,8 @@ filegroup { name: "tethering-servicescore-srcs", srcs: [ "src/com/android/server/connectivity/tethering/EntitlementManager.java", + "src/com/android/server/connectivity/tethering/OffloadController.java", + "src/com/android/server/connectivity/tethering/OffloadHardwareInterface.java", "src/com/android/server/connectivity/tethering/TetheringConfiguration.java", "src/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java", ], @@ -88,3 +119,11 @@ filegroup { "src/android/net/util/PrefixUtils.java", ], } + +// This group would be removed when tethering migration is done. +filegroup { + name: "tethering-jni-srcs", + srcs: [ + "jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp", + ], +} diff --git a/Tethering/jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp b/Tethering/jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp new file mode 100644 index 0000000000..3eaf48845a --- /dev/null +++ b/Tethering/jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp @@ -0,0 +1,148 @@ +/* + * 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LOG_TAG "OffloadHardwareInterface" +#include + +namespace android { + +using hardware::hidl_handle; +using hardware::hidl_string; +using hardware::tetheroffload::config::V1_0::IOffloadConfig; + +namespace { + +inline const sockaddr * asSockaddr(const sockaddr_nl *nladdr) { + return reinterpret_cast(nladdr); +} + +int conntrackSocket(unsigned groups) { + base::unique_fd s(socket(AF_NETLINK, SOCK_DGRAM, NETLINK_NETFILTER)); + if (s.get() < 0) return -errno; + + const struct sockaddr_nl bind_addr = { + .nl_family = AF_NETLINK, + .nl_pad = 0, + .nl_pid = 0, + .nl_groups = groups, + }; + if (bind(s.get(), asSockaddr(&bind_addr), sizeof(bind_addr)) != 0) { + return -errno; + } + + const struct sockaddr_nl kernel_addr = { + .nl_family = AF_NETLINK, + .nl_pad = 0, + .nl_pid = 0, + .nl_groups = groups, + }; + if (connect(s.get(), asSockaddr(&kernel_addr), sizeof(kernel_addr)) != 0) { + return -errno; + } + + return s.release(); +} + +// Return a hidl_handle that owns the file descriptor owned by fd, and will +// auto-close it (otherwise there would be double-close problems). +// +// Rely upon the compiler to eliminate the constexprs used for clarity. +hidl_handle handleFromFileDescriptor(base::unique_fd fd) { + hidl_handle h; + + static constexpr int kNumFds = 1; + static constexpr int kNumInts = 0; + native_handle_t *nh = native_handle_create(kNumFds, kNumInts); + nh->data[0] = fd.release(); + + static constexpr bool kTakeOwnership = true; + h.setTo(nh, kTakeOwnership); + + return h; +} + +} // namespace + +static jboolean android_server_connectivity_tethering_OffloadHardwareInterface_configOffload( + JNIEnv* /* env */) { + sp configInterface = IOffloadConfig::getService(); + if (configInterface.get() == nullptr) { + ALOGD("Could not find IOffloadConfig service."); + return false; + } + + // Per the IConfigOffload definition: + // + // fd1 A file descriptor bound to the following netlink groups + // (NF_NETLINK_CONNTRACK_NEW | NF_NETLINK_CONNTRACK_DESTROY). + // + // fd2 A file descriptor bound to the following netlink groups + // (NF_NETLINK_CONNTRACK_UPDATE | NF_NETLINK_CONNTRACK_DESTROY). + base::unique_fd + fd1(conntrackSocket(NF_NETLINK_CONNTRACK_NEW | NF_NETLINK_CONNTRACK_DESTROY)), + fd2(conntrackSocket(NF_NETLINK_CONNTRACK_UPDATE | NF_NETLINK_CONNTRACK_DESTROY)); + if (fd1.get() < 0 || fd2.get() < 0) { + ALOGE("Unable to create conntrack handles: %d/%s", errno, strerror(errno)); + return false; + } + + hidl_handle h1(handleFromFileDescriptor(std::move(fd1))), + h2(handleFromFileDescriptor(std::move(fd2))); + + bool rval(false); + hidl_string msg; + const auto status = configInterface->setHandles(h1, h2, + [&rval, &msg](bool success, const hidl_string& errMsg) { + rval = success; + msg = errMsg; + }); + if (!status.isOk() || !rval) { + ALOGE("IOffloadConfig::setHandles() error: '%s' / '%s'", + status.description().c_str(), msg.c_str()); + // If status is somehow not ok, make sure rval captures this too. + rval = false; + } + + return rval; +} + +/* + * JNI registration. + */ +static const JNINativeMethod gMethods[] = { + /* name, signature, funcPtr */ + { "configOffload", "()Z", + (void*) android_server_connectivity_tethering_OffloadHardwareInterface_configOffload }, +}; + +int register_android_server_connectivity_tethering_OffloadHardwareInterface(JNIEnv* env) { + return jniRegisterNativeMethods(env, + "com/android/server/connectivity/tethering/OffloadHardwareInterface", + gMethods, NELEM(gMethods)); +} + +}; // namespace android diff --git a/Tethering/src/com/android/server/connectivity/tethering/OffloadController.java b/Tethering/src/com/android/server/connectivity/tethering/OffloadController.java new file mode 100644 index 0000000000..16734d83e3 --- /dev/null +++ b/Tethering/src/com/android/server/connectivity/tethering/OffloadController.java @@ -0,0 +1,671 @@ +/* + * 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.INetworkManagementService; +import android.os.Looper; +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; + +/** + * 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 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); + } + } + + /** Start hardware offload. */ + 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; + } + + /** Stop hardware offload. */ + 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; + } + + @Override + 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); + } + } + + /** Set current tethering upstream LinkProperties. */ + 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); + } + + /** Set local prefixes. */ + public void setLocalPrefixes(Set localPrefixes) { + mExemptPrefixes = localPrefixes; + + if (!started()) return; + computeAndPushLocalPrefixes(UpdateType.IF_NEEDED); + } + + /** Update current downstream LinkProperties. */ + 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); + } + } + + /** Remove downstream interface from offload hardware. */ + 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; + } + + /** Dump information. */ + 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; + } + } +} diff --git a/Tethering/src/com/android/server/connectivity/tethering/OffloadHardwareInterface.java b/Tethering/src/com/android/server/connectivity/tethering/OffloadHardwareInterface.java new file mode 100644 index 0000000000..01339a4a2c --- /dev/null +++ b/Tethering/src/com/android/server/connectivity/tethering/OffloadHardwareInterface.java @@ -0,0 +1,395 @@ +/* + * 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 com.android.internal.util.BitUtils.uint16; + +import android.hardware.tetheroffload.control.V1_0.IOffloadControl; +import android.hardware.tetheroffload.control.V1_0.ITetheringOffloadCallback; +import android.hardware.tetheroffload.control.V1_0.NatTimeoutUpdate; +import android.hardware.tetheroffload.control.V1_0.NetworkProtocol; +import android.hardware.tetheroffload.control.V1_0.OffloadCallbackEvent; +import android.net.util.SharedLog; +import android.os.Handler; +import android.os.RemoteException; +import android.system.OsConstants; + +import java.util.ArrayList; + + +/** + * Capture tethering dependencies, for injection. + * + * @hide + */ +public class OffloadHardwareInterface { + private static final String TAG = OffloadHardwareInterface.class.getSimpleName(); + private static final String YIELDS = " -> "; + // Change this value to control whether tether offload is enabled or + // disabled by default in the absence of an explicit Settings value. + // See accompanying unittest to distinguish 0 from non-0 values. + private static final int DEFAULT_TETHER_OFFLOAD_DISABLED = 0; + private static final String NO_INTERFACE_NAME = ""; + private static final String NO_IPV4_ADDRESS = ""; + private static final String NO_IPV4_GATEWAY = ""; + + private static native boolean configOffload(); + + private final Handler mHandler; + private final SharedLog mLog; + private IOffloadControl mOffloadControl; + private TetheringOffloadCallback mTetheringOffloadCallback; + private ControlCallback mControlCallback; + + /** The callback to notify status of offload management process. */ + public static class ControlCallback { + /** Offload started. */ + public void onStarted() {} + /** + * Offload stopped because an error has occurred in lower layer. + */ + public void onStoppedError() {} + /** + * Offload stopped because the device has moved to a bearer on which hardware offload is + * not supported. Subsequent calls to setUpstreamParameters and add/removeDownstream will + * likely fail and cannot be presumed to be saved inside of the hardware management process. + * Upon receiving #onSupportAvailable(), the caller should reprogram the hardware to begin + * offload again. + */ + public void onStoppedUnsupported() {} + /** Indicate that offload is able to proivde support for this time. */ + public void onSupportAvailable() {} + /** Offload stopped because of usage limit reached. */ + public void onStoppedLimitReached() {} + + /** Indicate to update NAT timeout. */ + public void onNatTimeoutUpdate(int proto, + String srcAddr, int srcPort, + String dstAddr, int dstPort) {} + } + + /** The object which records Tx/Rx forwarded bytes. */ + public static class ForwardedStats { + public long rxBytes; + public long txBytes; + + public ForwardedStats() { + rxBytes = 0; + txBytes = 0; + } + + /** Add Tx/Rx bytes. */ + public void add(ForwardedStats other) { + rxBytes += other.rxBytes; + txBytes += other.txBytes; + } + + /** Returns the string representation of this object. */ + public String toString() { + return String.format("rx:%s tx:%s", rxBytes, txBytes); + } + } + + public OffloadHardwareInterface(Handler h, SharedLog log) { + mHandler = h; + mLog = log.forSubComponent(TAG); + } + + /** Get default value indicating whether offload is supported. */ + public int getDefaultTetherOffloadDisabled() { + return DEFAULT_TETHER_OFFLOAD_DISABLED; + } + + /** Configure offload management process. */ + public boolean initOffloadConfig() { + return configOffload(); + } + + /** Initialize the tethering offload HAL. */ + public boolean initOffloadControl(ControlCallback controlCb) { + mControlCallback = controlCb; + + if (mOffloadControl == null) { + try { + mOffloadControl = IOffloadControl.getService(); + } catch (RemoteException e) { + mLog.e("tethering offload control not supported: " + e); + return false; + } + if (mOffloadControl == null) { + mLog.e("tethering IOffloadControl.getService() returned null"); + return false; + } + } + + final String logmsg = String.format("initOffloadControl(%s)", + (controlCb == null) ? "null" + : "0x" + Integer.toHexString(System.identityHashCode(controlCb))); + + mTetheringOffloadCallback = new TetheringOffloadCallback(mHandler, mControlCallback, mLog); + final CbResults results = new CbResults(); + try { + mOffloadControl.initOffload( + mTetheringOffloadCallback, + (boolean success, String errMsg) -> { + results.mSuccess = success; + results.mErrMsg = errMsg; + }); + } catch (RemoteException e) { + record(logmsg, e); + return false; + } + + record(logmsg, results); + return results.mSuccess; + } + + /** Stop IOffloadControl. */ + public void stopOffloadControl() { + if (mOffloadControl != null) { + try { + mOffloadControl.stopOffload( + (boolean success, String errMsg) -> { + if (!success) mLog.e("stopOffload failed: " + errMsg); + }); + } catch (RemoteException e) { + mLog.e("failed to stopOffload: " + e); + } + } + mOffloadControl = null; + mTetheringOffloadCallback = null; + mControlCallback = null; + mLog.log("stopOffloadControl()"); + } + + /** Get Tx/Rx usage from last query. */ + public ForwardedStats getForwardedStats(String upstream) { + final String logmsg = String.format("getForwardedStats(%s)", upstream); + + final ForwardedStats stats = new ForwardedStats(); + try { + mOffloadControl.getForwardedStats( + upstream, + (long rxBytes, long txBytes) -> { + stats.rxBytes = (rxBytes > 0) ? rxBytes : 0; + stats.txBytes = (txBytes > 0) ? txBytes : 0; + }); + } catch (RemoteException e) { + record(logmsg, e); + return stats; + } + + mLog.log(logmsg + YIELDS + stats); + return stats; + } + + /** Set local prefixes to offload management process. */ + public boolean setLocalPrefixes(ArrayList localPrefixes) { + final String logmsg = String.format("setLocalPrefixes([%s])", + String.join(",", localPrefixes)); + + final CbResults results = new CbResults(); + try { + mOffloadControl.setLocalPrefixes(localPrefixes, + (boolean success, String errMsg) -> { + results.mSuccess = success; + results.mErrMsg = errMsg; + }); + } catch (RemoteException e) { + record(logmsg, e); + return false; + } + + record(logmsg, results); + return results.mSuccess; + } + + /** Set data limit value to offload management process. */ + public boolean setDataLimit(String iface, long limit) { + + final String logmsg = String.format("setDataLimit(%s, %d)", iface, limit); + + final CbResults results = new CbResults(); + try { + mOffloadControl.setDataLimit( + iface, limit, + (boolean success, String errMsg) -> { + results.mSuccess = success; + results.mErrMsg = errMsg; + }); + } catch (RemoteException e) { + record(logmsg, e); + return false; + } + + record(logmsg, results); + return results.mSuccess; + } + + /** Set upstream parameters to offload management process. */ + public boolean setUpstreamParameters( + String iface, String v4addr, String v4gateway, ArrayList v6gws) { + iface = (iface != null) ? iface : NO_INTERFACE_NAME; + v4addr = (v4addr != null) ? v4addr : NO_IPV4_ADDRESS; + v4gateway = (v4gateway != null) ? v4gateway : NO_IPV4_GATEWAY; + v6gws = (v6gws != null) ? v6gws : new ArrayList<>(); + + final String logmsg = String.format("setUpstreamParameters(%s, %s, %s, [%s])", + iface, v4addr, v4gateway, String.join(",", v6gws)); + + final CbResults results = new CbResults(); + try { + mOffloadControl.setUpstreamParameters( + iface, v4addr, v4gateway, v6gws, + (boolean success, String errMsg) -> { + results.mSuccess = success; + results.mErrMsg = errMsg; + }); + } catch (RemoteException e) { + record(logmsg, e); + return false; + } + + record(logmsg, results); + return results.mSuccess; + } + + /** Add downstream prefix to offload management process. */ + public boolean addDownstreamPrefix(String ifname, String prefix) { + final String logmsg = String.format("addDownstreamPrefix(%s, %s)", ifname, prefix); + + final CbResults results = new CbResults(); + try { + mOffloadControl.addDownstream(ifname, prefix, + (boolean success, String errMsg) -> { + results.mSuccess = success; + results.mErrMsg = errMsg; + }); + } catch (RemoteException e) { + record(logmsg, e); + return false; + } + + record(logmsg, results); + return results.mSuccess; + } + + /** Remove downstream prefix from offload management process. */ + public boolean removeDownstreamPrefix(String ifname, String prefix) { + final String logmsg = String.format("removeDownstreamPrefix(%s, %s)", ifname, prefix); + + final CbResults results = new CbResults(); + try { + mOffloadControl.removeDownstream(ifname, prefix, + (boolean success, String errMsg) -> { + results.mSuccess = success; + results.mErrMsg = errMsg; + }); + } catch (RemoteException e) { + record(logmsg, e); + return false; + } + + record(logmsg, results); + return results.mSuccess; + } + + private void record(String msg, Throwable t) { + mLog.e(msg + YIELDS + "exception: " + t); + } + + private void record(String msg, CbResults results) { + final String logmsg = msg + YIELDS + results; + if (!results.mSuccess) { + mLog.e(logmsg); + } else { + mLog.log(logmsg); + } + } + + private static class TetheringOffloadCallback extends ITetheringOffloadCallback.Stub { + public final Handler handler; + public final ControlCallback controlCb; + public final SharedLog log; + + TetheringOffloadCallback(Handler h, ControlCallback cb, SharedLog sharedLog) { + handler = h; + controlCb = cb; + log = sharedLog; + } + + @Override + public void onEvent(int event) { + handler.post(() -> { + switch (event) { + case OffloadCallbackEvent.OFFLOAD_STARTED: + controlCb.onStarted(); + break; + case OffloadCallbackEvent.OFFLOAD_STOPPED_ERROR: + controlCb.onStoppedError(); + break; + case OffloadCallbackEvent.OFFLOAD_STOPPED_UNSUPPORTED: + controlCb.onStoppedUnsupported(); + break; + case OffloadCallbackEvent.OFFLOAD_SUPPORT_AVAILABLE: + controlCb.onSupportAvailable(); + break; + case OffloadCallbackEvent.OFFLOAD_STOPPED_LIMIT_REACHED: + controlCb.onStoppedLimitReached(); + break; + default: + log.e("Unsupported OffloadCallbackEvent: " + event); + } + }); + } + + @Override + public void updateTimeout(NatTimeoutUpdate params) { + handler.post(() -> { + controlCb.onNatTimeoutUpdate( + networkProtocolToOsConstant(params.proto), + params.src.addr, uint16(params.src.port), + params.dst.addr, uint16(params.dst.port)); + }); + } + } + + private static int networkProtocolToOsConstant(int proto) { + switch (proto) { + case NetworkProtocol.TCP: return OsConstants.IPPROTO_TCP; + case NetworkProtocol.UDP: return OsConstants.IPPROTO_UDP; + default: + // The caller checks this value and will log an error. Just make + // sure it won't collide with valid OsContants.IPPROTO_* values. + return -Math.abs(proto); + } + } + + private static class CbResults { + boolean mSuccess; + String mErrMsg; + + @Override + public String toString() { + if (mSuccess) { + return "ok"; + } else { + return "fail: " + mErrMsg; + } + } + } +} diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp index 7c06e5f0d7..363be18a73 100644 --- a/Tethering/tests/unit/Android.bp +++ b/Tethering/tests/unit/Android.bp @@ -25,6 +25,7 @@ android_test { static_libs: [ "androidx.test.rules", "frameworks-base-testutils", + "net-tests-utils", "mockito-target-extended-minus-junit4", "TetheringApiCurrentLib", "testables", @@ -46,6 +47,7 @@ filegroup { name: "tethering-tests-src", srcs: [ "src/com/android/server/connectivity/tethering/EntitlementManagerTest.java", + "src/com/android/server/connectivity/tethering/OffloadControllerTest.java", "src/com/android/server/connectivity/tethering/TetheringConfigurationTest.java", "src/com/android/server/connectivity/tethering/UpstreamNetworkMonitorTest.java", "src/android/net/dhcp/DhcpServingParamsParcelExtTest.java", diff --git a/Tethering/tests/unit/src/com/android/server/connectivity/tethering/OffloadControllerTest.java b/Tethering/tests/unit/src/com/android/server/connectivity/tethering/OffloadControllerTest.java new file mode 100644 index 0000000000..8574f54014 --- /dev/null +++ b/Tethering/tests/unit/src/com/android/server/connectivity/tethering/OffloadControllerTest.java @@ -0,0 +1,730 @@ +/* + * 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_IFACE; +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 static com.android.server.connectivity.tethering.OffloadHardwareInterface.ForwardedStats; +import static com.android.testutils.MiscAssertsKt.assertContainsAll; +import static com.android.testutils.MiscAssertsKt.assertThrows; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; +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 android.content.Context; +import android.content.pm.ApplicationInfo; +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.util.SharedLog; +import android.os.Handler; +import android.os.INetworkManagementService; +import android.os.Looper; +import android.provider.Settings; +import android.provider.Settings.SettingNotFoundException; +import android.test.mock.MockContentResolver; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.util.test.FakeSettingsProvider; +import com.android.testutils.HandlerUtilsKt; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class OffloadControllerTest { + private static final String RNDIS0 = "test_rndis0"; + private static final String RMNET0 = "test_rmnet_data0"; + private static final String WLAN0 = "test_wlan0"; + + private static final String IPV6_LINKLOCAL = "fe80::/64"; + private static final String IPV6_DOC_PREFIX = "2001:db8::/64"; + private static final String IPV6_DISCARD_PREFIX = "100::/64"; + private static final String USB_PREFIX = "192.168.42.0/24"; + private static final String WIFI_PREFIX = "192.168.43.0/24"; + private static final long WAIT_FOR_IDLE_TIMEOUT = 2 * 1000; + + @Mock private OffloadHardwareInterface mHardware; + @Mock private ApplicationInfo mApplicationInfo; + @Mock private Context mContext; + @Mock private INetworkManagementService mNMService; + private final ArgumentCaptor mStringArrayCaptor = + ArgumentCaptor.forClass(ArrayList.class); + private final ArgumentCaptor mTetherStatsProviderCaptor = + ArgumentCaptor.forClass(ITetheringStatsProvider.Stub.class); + private final ArgumentCaptor mControlCallbackCaptor = + ArgumentCaptor.forClass(OffloadHardwareInterface.ControlCallback.class); + private MockContentResolver mContentResolver; + + @Before public void setUp() { + MockitoAnnotations.initMocks(this); + when(mContext.getApplicationInfo()).thenReturn(mApplicationInfo); + when(mContext.getPackageName()).thenReturn("OffloadControllerTest"); + mContentResolver = new MockContentResolver(mContext); + mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); + when(mContext.getContentResolver()).thenReturn(mContentResolver); + FakeSettingsProvider.clearSettingsProvider(); + } + + @After public void tearDown() throws Exception { + FakeSettingsProvider.clearSettingsProvider(); + } + + private void setupFunctioningHardwareInterface() { + when(mHardware.initOffloadConfig()).thenReturn(true); + when(mHardware.initOffloadControl(mControlCallbackCaptor.capture())) + .thenReturn(true); + when(mHardware.setUpstreamParameters(anyString(), any(), any(), any())).thenReturn(true); + when(mHardware.getForwardedStats(any())).thenReturn(new ForwardedStats()); + when(mHardware.setDataLimit(anyString(), anyLong())).thenReturn(true); + } + + private void enableOffload() { + Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 0); + } + + private void waitForIdle() { + HandlerUtilsKt.waitForIdle(new Handler(Looper.getMainLooper()), WAIT_FOR_IDLE_TIMEOUT); + } + + private OffloadController makeOffloadController() throws Exception { + OffloadController offload = new OffloadController(new Handler(Looper.getMainLooper()), + mHardware, mContentResolver, mNMService, new SharedLog("test")); + verify(mNMService).registerTetheringStatsProvider( + mTetherStatsProviderCaptor.capture(), anyString()); + return offload; + } + + @Test + public void testNoSettingsValueDefaultDisabledDoesNotStart() throws Exception { + setupFunctioningHardwareInterface(); + when(mHardware.getDefaultTetherOffloadDisabled()).thenReturn(1); + assertThrows(SettingNotFoundException.class, () -> + Settings.Global.getInt(mContentResolver, TETHER_OFFLOAD_DISABLED)); + + final OffloadController offload = makeOffloadController(); + offload.start(); + + final InOrder inOrder = inOrder(mHardware); + inOrder.verify(mHardware, times(1)).getDefaultTetherOffloadDisabled(); + inOrder.verify(mHardware, never()).initOffloadConfig(); + inOrder.verify(mHardware, never()).initOffloadControl( + any(OffloadHardwareInterface.ControlCallback.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testNoSettingsValueDefaultEnabledDoesStart() throws Exception { + setupFunctioningHardwareInterface(); + when(mHardware.getDefaultTetherOffloadDisabled()).thenReturn(0); + assertThrows(SettingNotFoundException.class, () -> + Settings.Global.getInt(mContentResolver, TETHER_OFFLOAD_DISABLED)); + + final OffloadController offload = makeOffloadController(); + offload.start(); + + final InOrder inOrder = inOrder(mHardware); + inOrder.verify(mHardware, times(1)).getDefaultTetherOffloadDisabled(); + inOrder.verify(mHardware, times(1)).initOffloadConfig(); + inOrder.verify(mHardware, times(1)).initOffloadControl( + any(OffloadHardwareInterface.ControlCallback.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testSettingsAllowsStart() throws Exception { + setupFunctioningHardwareInterface(); + Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 0); + + final OffloadController offload = makeOffloadController(); + offload.start(); + + final InOrder inOrder = inOrder(mHardware); + inOrder.verify(mHardware, times(1)).getDefaultTetherOffloadDisabled(); + inOrder.verify(mHardware, times(1)).initOffloadConfig(); + inOrder.verify(mHardware, times(1)).initOffloadControl( + any(OffloadHardwareInterface.ControlCallback.class)); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testSettingsDisablesStart() throws Exception { + setupFunctioningHardwareInterface(); + Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 1); + + final OffloadController offload = makeOffloadController(); + offload.start(); + + final InOrder inOrder = inOrder(mHardware); + inOrder.verify(mHardware, times(1)).getDefaultTetherOffloadDisabled(); + inOrder.verify(mHardware, never()).initOffloadConfig(); + inOrder.verify(mHardware, never()).initOffloadControl(anyObject()); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testSetUpstreamLinkPropertiesWorking() throws Exception { + setupFunctioningHardwareInterface(); + enableOffload(); + + final OffloadController offload = makeOffloadController(); + offload.start(); + + final InOrder inOrder = inOrder(mHardware); + inOrder.verify(mHardware, times(1)).getDefaultTetherOffloadDisabled(); + inOrder.verify(mHardware, times(1)).initOffloadConfig(); + inOrder.verify(mHardware, times(1)).initOffloadControl( + any(OffloadHardwareInterface.ControlCallback.class)); + inOrder.verifyNoMoreInteractions(); + + // In reality, the UpstreamNetworkMonitor would have passed down to us + // a covering set of local prefixes representing a minimum essential + // set plus all the prefixes on networks with network agents. + // + // We simulate that there, and then add upstream elements one by one + // and watch what happens. + final Set minimumLocalPrefixes = new HashSet<>(); + for (String s : new String[]{ + "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64"}) { + minimumLocalPrefixes.add(new IpPrefix(s)); + } + offload.setLocalPrefixes(minimumLocalPrefixes); + inOrder.verify(mHardware, times(1)).setLocalPrefixes(mStringArrayCaptor.capture()); + ArrayList localPrefixes = mStringArrayCaptor.getValue(); + assertEquals(4, localPrefixes.size()); + assertContainsAll(localPrefixes, + "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64"); + inOrder.verifyNoMoreInteractions(); + + offload.setUpstreamLinkProperties(null); + // No change in local addresses means no call to setLocalPrefixes(). + inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture()); + // This LinkProperties value does not differ from the default upstream. + // There should be no extraneous call to setUpstreamParameters(). + inOrder.verify(mHardware, never()).setUpstreamParameters( + anyObject(), anyObject(), anyObject(), anyObject()); + inOrder.verifyNoMoreInteractions(); + + final LinkProperties lp = new LinkProperties(); + + final String testIfName = "rmnet_data17"; + lp.setInterfaceName(testIfName); + offload.setUpstreamLinkProperties(lp); + // No change in local addresses means no call to setLocalPrefixes(). + inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture()); + inOrder.verify(mHardware, times(1)).setUpstreamParameters( + eq(testIfName), eq(null), eq(null), eq(null)); + inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE)); + inOrder.verifyNoMoreInteractions(); + + final String ipv4Addr = "192.0.2.5"; + final String linkAddr = ipv4Addr + "/24"; + lp.addLinkAddress(new LinkAddress(linkAddr)); + lp.addRoute(new RouteInfo(new IpPrefix("192.0.2.0/24"))); + offload.setUpstreamLinkProperties(lp); + // IPv4 prefixes and addresses on the upstream are simply left as whole + // prefixes (already passed in from UpstreamNetworkMonitor code). If a + // tethering client sends traffic to the IPv4 default router or other + // clients on the upstream this will not be hardware-forwarded, and that + // should be fine for now. Ergo: no change in local addresses, no call + // to setLocalPrefixes(). + inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture()); + inOrder.verify(mHardware, times(1)).setUpstreamParameters( + eq(testIfName), eq(ipv4Addr), eq(null), eq(null)); + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName)); + inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE)); + inOrder.verifyNoMoreInteractions(); + + final String ipv4Gateway = "192.0.2.1"; + lp.addRoute(new RouteInfo(InetAddress.getByName(ipv4Gateway))); + offload.setUpstreamLinkProperties(lp); + // No change in local addresses means no call to setLocalPrefixes(). + inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture()); + inOrder.verify(mHardware, times(1)).setUpstreamParameters( + eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), eq(null)); + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName)); + inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE)); + inOrder.verifyNoMoreInteractions(); + + final String ipv6Gw1 = "fe80::cafe"; + lp.addRoute(new RouteInfo(InetAddress.getByName(ipv6Gw1))); + offload.setUpstreamLinkProperties(lp); + // No change in local addresses means no call to setLocalPrefixes(). + inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture()); + inOrder.verify(mHardware, times(1)).setUpstreamParameters( + eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture()); + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName)); + ArrayList v6gws = mStringArrayCaptor.getValue(); + assertEquals(1, v6gws.size()); + assertTrue(v6gws.contains(ipv6Gw1)); + inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE)); + inOrder.verifyNoMoreInteractions(); + + final String ipv6Gw2 = "fe80::d00d"; + lp.addRoute(new RouteInfo(InetAddress.getByName(ipv6Gw2))); + offload.setUpstreamLinkProperties(lp); + // No change in local addresses means no call to setLocalPrefixes(). + inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture()); + inOrder.verify(mHardware, times(1)).setUpstreamParameters( + eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture()); + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName)); + v6gws = mStringArrayCaptor.getValue(); + assertEquals(2, v6gws.size()); + assertTrue(v6gws.contains(ipv6Gw1)); + assertTrue(v6gws.contains(ipv6Gw2)); + inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE)); + inOrder.verifyNoMoreInteractions(); + + final LinkProperties stacked = new LinkProperties(); + stacked.setInterfaceName("stacked"); + stacked.addLinkAddress(new LinkAddress("192.0.2.129/25")); + stacked.addRoute(new RouteInfo(InetAddress.getByName("192.0.2.254"))); + stacked.addRoute(new RouteInfo(InetAddress.getByName("fe80::bad:f00"))); + assertTrue(lp.addStackedLink(stacked)); + offload.setUpstreamLinkProperties(lp); + // No change in local addresses means no call to setLocalPrefixes(). + inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture()); + inOrder.verify(mHardware, times(1)).setUpstreamParameters( + eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture()); + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName)); + v6gws = mStringArrayCaptor.getValue(); + assertEquals(2, v6gws.size()); + assertTrue(v6gws.contains(ipv6Gw1)); + assertTrue(v6gws.contains(ipv6Gw2)); + inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE)); + inOrder.verifyNoMoreInteractions(); + + // Add in some IPv6 upstream info. When there is a tethered downstream + // making use of the IPv6 prefix we would expect to see the /64 route + // removed from "local prefixes" and /128s added for the upstream IPv6 + // addresses. This is not yet implemented, and for now we simply + // expect to see these /128s. + lp.addRoute(new RouteInfo(new IpPrefix("2001:db8::/64"))); + // "2001:db8::/64" plus "assigned" ASCII in hex + lp.addLinkAddress(new LinkAddress("2001:db8::6173:7369:676e:6564/64")); + // "2001:db8::/64" plus "random" ASCII in hex + lp.addLinkAddress(new LinkAddress("2001:db8::7261:6e64:6f6d/64")); + offload.setUpstreamLinkProperties(lp); + inOrder.verify(mHardware, times(1)).setLocalPrefixes(mStringArrayCaptor.capture()); + localPrefixes = mStringArrayCaptor.getValue(); + assertEquals(6, localPrefixes.size()); + assertContainsAll(localPrefixes, + "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64", + "2001:db8::6173:7369:676e:6564/128", "2001:db8::7261:6e64:6f6d/128"); + // The relevant parts of the LinkProperties have not changed, but at the + // moment we do not de-dup upstream LinkProperties this carefully. + inOrder.verify(mHardware, times(1)).setUpstreamParameters( + eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture()); + v6gws = mStringArrayCaptor.getValue(); + assertEquals(2, v6gws.size()); + assertTrue(v6gws.contains(ipv6Gw1)); + assertTrue(v6gws.contains(ipv6Gw2)); + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName)); + inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE)); + inOrder.verifyNoMoreInteractions(); + + // Completely identical LinkProperties updates are de-duped. + offload.setUpstreamLinkProperties(lp); + // This LinkProperties value does not differ from the default upstream. + // There should be no extraneous call to setUpstreamParameters(). + inOrder.verify(mHardware, never()).setUpstreamParameters( + anyObject(), anyObject(), anyObject(), anyObject()); + inOrder.verifyNoMoreInteractions(); + } + + private void assertNetworkStats(String iface, ForwardedStats stats, NetworkStats.Entry entry) { + assertEquals(iface, entry.iface); + assertEquals(stats.rxBytes, entry.rxBytes); + assertEquals(stats.txBytes, entry.txBytes); + assertEquals(SET_DEFAULT, entry.set); + assertEquals(TAG_NONE, entry.tag); + } + + @Test + public void testGetForwardedStats() throws Exception { + setupFunctioningHardwareInterface(); + enableOffload(); + + final OffloadController offload = makeOffloadController(); + offload.start(); + + final String ethernetIface = "eth1"; + final String mobileIface = "rmnet_data0"; + + ForwardedStats ethernetStats = new ForwardedStats(); + ethernetStats.rxBytes = 12345; + ethernetStats.txBytes = 54321; + + ForwardedStats mobileStats = new ForwardedStats(); + mobileStats.rxBytes = 999; + mobileStats.txBytes = 99999; + + when(mHardware.getForwardedStats(eq(ethernetIface))).thenReturn(ethernetStats); + when(mHardware.getForwardedStats(eq(mobileIface))).thenReturn(mobileStats); + + InOrder inOrder = inOrder(mHardware); + + final LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(ethernetIface); + offload.setUpstreamLinkProperties(lp); + // Previous upstream was null, so no stats are fetched. + inOrder.verify(mHardware, never()).getForwardedStats(any()); + + lp.setInterfaceName(mobileIface); + offload.setUpstreamLinkProperties(lp); + // Expect that we fetch stats from the previous upstream. + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(ethernetIface)); + + lp.setInterfaceName(ethernetIface); + offload.setUpstreamLinkProperties(lp); + // Expect that we fetch stats from the previous upstream. + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(mobileIface)); + + ethernetStats = new ForwardedStats(); + ethernetStats.rxBytes = 100000; + ethernetStats.txBytes = 100000; + when(mHardware.getForwardedStats(eq(ethernetIface))).thenReturn(ethernetStats); + offload.setUpstreamLinkProperties(null); + // Expect that we first clear the HAL's upstream parameters. + inOrder.verify(mHardware, times(1)).setUpstreamParameters( + eq(""), eq("0.0.0.0"), eq("0.0.0.0"), eq(null)); + // Expect that we fetch stats from the previous upstream. + inOrder.verify(mHardware, times(1)).getForwardedStats(eq(ethernetIface)); + + ITetheringStatsProvider provider = mTetherStatsProviderCaptor.getValue(); + NetworkStats stats = provider.getTetherStats(STATS_PER_IFACE); + NetworkStats perUidStats = provider.getTetherStats(STATS_PER_UID); + waitForIdle(); + // There is no current upstream, so no stats are fetched. + inOrder.verify(mHardware, never()).getForwardedStats(any()); + inOrder.verifyNoMoreInteractions(); + + assertEquals(2, stats.size()); + assertEquals(2, perUidStats.size()); + + NetworkStats.Entry entry = null; + for (int i = 0; i < stats.size(); i++) { + assertEquals(UID_ALL, stats.getValues(i, entry).uid); + assertEquals(UID_TETHERING, perUidStats.getValues(i, entry).uid); + } + + int ethernetPosition = ethernetIface.equals(stats.getValues(0, entry).iface) ? 0 : 1; + int mobilePosition = 1 - ethernetPosition; + + entry = stats.getValues(mobilePosition, entry); + assertNetworkStats(mobileIface, mobileStats, entry); + entry = perUidStats.getValues(mobilePosition, entry); + assertNetworkStats(mobileIface, mobileStats, entry); + + ethernetStats.rxBytes = 12345 + 100000; + ethernetStats.txBytes = 54321 + 100000; + entry = stats.getValues(ethernetPosition, entry); + assertNetworkStats(ethernetIface, ethernetStats, entry); + entry = perUidStats.getValues(ethernetPosition, entry); + assertNetworkStats(ethernetIface, ethernetStats, entry); + } + + @Test + public void testSetInterfaceQuota() throws Exception { + setupFunctioningHardwareInterface(); + enableOffload(); + + final OffloadController offload = makeOffloadController(); + offload.start(); + + final String ethernetIface = "eth1"; + final String mobileIface = "rmnet_data0"; + final long ethernetLimit = 12345; + final long mobileLimit = 12345678; + + final LinkProperties lp = new LinkProperties(); + lp.setInterfaceName(ethernetIface); + offload.setUpstreamLinkProperties(lp); + + ITetheringStatsProvider provider = mTetherStatsProviderCaptor.getValue(); + final InOrder inOrder = inOrder(mHardware); + when(mHardware.setUpstreamParameters(any(), any(), any(), any())).thenReturn(true); + when(mHardware.setDataLimit(anyString(), anyLong())).thenReturn(true); + + // Applying an interface quota to the current upstream immediately sends it to the hardware. + provider.setInterfaceQuota(ethernetIface, ethernetLimit); + waitForIdle(); + inOrder.verify(mHardware).setDataLimit(ethernetIface, ethernetLimit); + inOrder.verifyNoMoreInteractions(); + + // Applying an interface quota to another upstream does not take any immediate action. + provider.setInterfaceQuota(mobileIface, mobileLimit); + waitForIdle(); + inOrder.verify(mHardware, never()).setDataLimit(anyString(), anyLong()); + + // Switching to that upstream causes the quota to be applied if the parameters were applied + // correctly. + lp.setInterfaceName(mobileIface); + offload.setUpstreamLinkProperties(lp); + waitForIdle(); + inOrder.verify(mHardware).setDataLimit(mobileIface, mobileLimit); + + // Setting a limit of ITetheringStatsProvider.QUOTA_UNLIMITED causes the limit to be set + // to Long.MAX_VALUE. + provider.setInterfaceQuota(mobileIface, ITetheringStatsProvider.QUOTA_UNLIMITED); + waitForIdle(); + inOrder.verify(mHardware).setDataLimit(mobileIface, Long.MAX_VALUE); + + // If setting upstream parameters fails, then the data limit is not set. + when(mHardware.setUpstreamParameters(any(), any(), any(), any())).thenReturn(false); + lp.setInterfaceName(ethernetIface); + offload.setUpstreamLinkProperties(lp); + provider.setInterfaceQuota(mobileIface, mobileLimit); + waitForIdle(); + inOrder.verify(mHardware, never()).setDataLimit(anyString(), anyLong()); + + // If setting the data limit fails while changing upstreams, offload is stopped. + when(mHardware.setUpstreamParameters(any(), any(), any(), any())).thenReturn(true); + when(mHardware.setDataLimit(anyString(), anyLong())).thenReturn(false); + lp.setInterfaceName(mobileIface); + offload.setUpstreamLinkProperties(lp); + provider.setInterfaceQuota(mobileIface, mobileLimit); + waitForIdle(); + inOrder.verify(mHardware).getForwardedStats(ethernetIface); + inOrder.verify(mHardware).stopOffloadControl(); + } + + @Test + public void testDataLimitCallback() throws Exception { + setupFunctioningHardwareInterface(); + enableOffload(); + + final OffloadController offload = makeOffloadController(); + offload.start(); + + OffloadHardwareInterface.ControlCallback callback = mControlCallbackCaptor.getValue(); + callback.onStoppedLimitReached(); + verify(mNMService, times(1)).tetherLimitReached(mTetherStatsProviderCaptor.getValue()); + } + + @Test + public void testAddRemoveDownstreams() throws Exception { + setupFunctioningHardwareInterface(); + enableOffload(); + + final OffloadController offload = makeOffloadController(); + offload.start(); + + final InOrder inOrder = inOrder(mHardware); + inOrder.verify(mHardware, times(1)).initOffloadConfig(); + inOrder.verify(mHardware, times(1)).initOffloadControl( + any(OffloadHardwareInterface.ControlCallback.class)); + inOrder.verifyNoMoreInteractions(); + + // Tethering makes several calls to setLocalPrefixes() before add/remove + // downstream calls are made. This is not tested here; only the behavior + // of notifyDownstreamLinkProperties() and removeDownstreamInterface() + // are tested. + + // [1] USB tethering is started. + final LinkProperties usbLinkProperties = new LinkProperties(); + usbLinkProperties.setInterfaceName(RNDIS0); + usbLinkProperties.addLinkAddress(new LinkAddress("192.168.42.1/24")); + usbLinkProperties.addRoute(new RouteInfo(new IpPrefix(USB_PREFIX))); + offload.notifyDownstreamLinkProperties(usbLinkProperties); + inOrder.verify(mHardware, times(1)).addDownstreamPrefix(RNDIS0, USB_PREFIX); + inOrder.verifyNoMoreInteractions(); + + // [2] Routes for IPv6 link-local prefixes should never be added. + usbLinkProperties.addRoute(new RouteInfo(new IpPrefix(IPV6_LINKLOCAL))); + offload.notifyDownstreamLinkProperties(usbLinkProperties); + inOrder.verify(mHardware, never()).addDownstreamPrefix(eq(RNDIS0), anyString()); + inOrder.verifyNoMoreInteractions(); + + // [3] Add an IPv6 prefix for good measure. Only new offload-able + // prefixes should be passed to the HAL. + usbLinkProperties.addLinkAddress(new LinkAddress("2001:db8::1/64")); + usbLinkProperties.addRoute(new RouteInfo(new IpPrefix(IPV6_DOC_PREFIX))); + offload.notifyDownstreamLinkProperties(usbLinkProperties); + inOrder.verify(mHardware, times(1)).addDownstreamPrefix(RNDIS0, IPV6_DOC_PREFIX); + inOrder.verifyNoMoreInteractions(); + + // [4] Adding addresses doesn't affect notifyDownstreamLinkProperties(). + // The address is passed in by a separate setLocalPrefixes() invocation. + usbLinkProperties.addLinkAddress(new LinkAddress("2001:db8::2/64")); + offload.notifyDownstreamLinkProperties(usbLinkProperties); + inOrder.verify(mHardware, never()).addDownstreamPrefix(eq(RNDIS0), anyString()); + + // [5] Differences in local routes are converted into addDownstream() + // and removeDownstream() invocations accordingly. + usbLinkProperties.removeRoute(new RouteInfo(new IpPrefix(IPV6_DOC_PREFIX), null, RNDIS0)); + usbLinkProperties.addRoute(new RouteInfo(new IpPrefix(IPV6_DISCARD_PREFIX))); + offload.notifyDownstreamLinkProperties(usbLinkProperties); + inOrder.verify(mHardware, times(1)).removeDownstreamPrefix(RNDIS0, IPV6_DOC_PREFIX); + inOrder.verify(mHardware, times(1)).addDownstreamPrefix(RNDIS0, IPV6_DISCARD_PREFIX); + inOrder.verifyNoMoreInteractions(); + + // [6] Removing a downstream interface which was never added causes no + // interactions with the HAL. + offload.removeDownstreamInterface(WLAN0); + inOrder.verifyNoMoreInteractions(); + + // [7] Removing an active downstream removes all remaining prefixes. + offload.removeDownstreamInterface(RNDIS0); + inOrder.verify(mHardware, times(1)).removeDownstreamPrefix(RNDIS0, USB_PREFIX); + inOrder.verify(mHardware, times(1)).removeDownstreamPrefix(RNDIS0, IPV6_DISCARD_PREFIX); + inOrder.verifyNoMoreInteractions(); + } + + @Test + public void testControlCallbackOnStoppedUnsupportedFetchesAllStats() throws Exception { + setupFunctioningHardwareInterface(); + enableOffload(); + + final OffloadController offload = makeOffloadController(); + offload.start(); + + // Pretend to set a few different upstreams (only the interface name + // matters for this test; we're ignoring IP and route information). + final LinkProperties upstreamLp = new LinkProperties(); + for (String ifname : new String[]{RMNET0, WLAN0, RMNET0}) { + upstreamLp.setInterfaceName(ifname); + offload.setUpstreamLinkProperties(upstreamLp); + } + + // Clear invocation history, especially the getForwardedStats() calls + // that happen with setUpstreamParameters(). + clearInvocations(mHardware); + + OffloadHardwareInterface.ControlCallback callback = mControlCallbackCaptor.getValue(); + callback.onStoppedUnsupported(); + + // Verify forwarded stats behaviour. + verify(mHardware, times(1)).getForwardedStats(eq(RMNET0)); + verify(mHardware, times(1)).getForwardedStats(eq(WLAN0)); + verifyNoMoreInteractions(mHardware); + verify(mNMService, times(1)).tetherLimitReached(mTetherStatsProviderCaptor.getValue()); + verifyNoMoreInteractions(mNMService); + } + + @Test + public void testControlCallbackOnSupportAvailableFetchesAllStatsAndPushesAllParameters() + throws Exception { + setupFunctioningHardwareInterface(); + enableOffload(); + + final OffloadController offload = makeOffloadController(); + offload.start(); + + // Pretend to set a few different upstreams (only the interface name + // matters for this test; we're ignoring IP and route information). + final LinkProperties upstreamLp = new LinkProperties(); + for (String ifname : new String[]{RMNET0, WLAN0, RMNET0}) { + upstreamLp.setInterfaceName(ifname); + offload.setUpstreamLinkProperties(upstreamLp); + } + + // Pretend that some local prefixes and downstreams have been added + // (and removed, for good measure). + final Set minimumLocalPrefixes = new HashSet<>(); + for (String s : new String[]{ + "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64"}) { + minimumLocalPrefixes.add(new IpPrefix(s)); + } + offload.setLocalPrefixes(minimumLocalPrefixes); + + final LinkProperties usbLinkProperties = new LinkProperties(); + usbLinkProperties.setInterfaceName(RNDIS0); + usbLinkProperties.addLinkAddress(new LinkAddress("192.168.42.1/24")); + usbLinkProperties.addRoute(new RouteInfo(new IpPrefix(USB_PREFIX))); + offload.notifyDownstreamLinkProperties(usbLinkProperties); + + final LinkProperties wifiLinkProperties = new LinkProperties(); + wifiLinkProperties.setInterfaceName(WLAN0); + wifiLinkProperties.addLinkAddress(new LinkAddress("192.168.43.1/24")); + wifiLinkProperties.addRoute(new RouteInfo(new IpPrefix(WIFI_PREFIX))); + wifiLinkProperties.addRoute(new RouteInfo(new IpPrefix(IPV6_LINKLOCAL))); + // Use a benchmark prefix (RFC 5180 + erratum), since the documentation + // prefix is included in the excluded prefix list. + wifiLinkProperties.addLinkAddress(new LinkAddress("2001:2::1/64")); + wifiLinkProperties.addLinkAddress(new LinkAddress("2001:2::2/64")); + wifiLinkProperties.addRoute(new RouteInfo(new IpPrefix("2001:2::/64"))); + offload.notifyDownstreamLinkProperties(wifiLinkProperties); + + offload.removeDownstreamInterface(RNDIS0); + + // Clear invocation history, especially the getForwardedStats() calls + // that happen with setUpstreamParameters(). + clearInvocations(mHardware); + + OffloadHardwareInterface.ControlCallback callback = mControlCallbackCaptor.getValue(); + callback.onSupportAvailable(); + + // Verify forwarded stats behaviour. + verify(mHardware, times(1)).getForwardedStats(eq(RMNET0)); + verify(mHardware, times(1)).getForwardedStats(eq(WLAN0)); + verify(mNMService, times(1)).tetherLimitReached(mTetherStatsProviderCaptor.getValue()); + verifyNoMoreInteractions(mNMService); + + // TODO: verify local prefixes and downstreams are also pushed to the HAL. + verify(mHardware, times(1)).setLocalPrefixes(mStringArrayCaptor.capture()); + ArrayList localPrefixes = mStringArrayCaptor.getValue(); + assertEquals(4, localPrefixes.size()); + assertContainsAll(localPrefixes, + // TODO: The logic to find and exclude downstream IP prefixes + // is currently in Tethering's OffloadWrapper but must be moved + // into OffloadController proper. After this, also check for: + // "192.168.43.1/32", "2001:2::1/128", "2001:2::2/128" + "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64"); + verify(mHardware, times(1)).addDownstreamPrefix(WLAN0, "192.168.43.0/24"); + verify(mHardware, times(1)).addDownstreamPrefix(WLAN0, "2001:2::/64"); + verify(mHardware, times(1)).setUpstreamParameters(eq(RMNET0), any(), any(), any()); + verify(mHardware, times(1)).setDataLimit(eq(RMNET0), anyLong()); + verifyNoMoreInteractions(mHardware); + } + +}