Merge "[Tether06] Migrate tether offload controller into module"

This commit is contained in:
Mark Chien
2019-11-05 12:48:46 +00:00
committed by Gerrit Code Review
6 changed files with 1985 additions and 0 deletions

View File

@@ -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",
],
}

View File

@@ -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 <errno.h>
#include <error.h>
#include <hidl/HidlSupport.h>
#include <jni.h>
#include <nativehelper/JNIHelp.h>
#include <linux/netfilter/nfnetlink.h>
#include <linux/netlink.h>
#include <sys/socket.h>
#include <android-base/unique_fd.h>
#include <android/hardware/tetheroffload/config/1.0/IOffloadConfig.h>
#define LOG_TAG "OffloadHardwareInterface"
#include <utils/Log.h>
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<const sockaddr *>(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<IOffloadConfig> 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

View File

@@ -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<String, LinkProperties> 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<IpPrefix> 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<String> 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<String, ForwardedStats> 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<String, Long> 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<String, ForwardedStats> 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<String, ForwardedStats> 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<IpPrefix> 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<RouteInfo> oldRoutes =
(oldLp != null) ? oldLp.getRoutes() : Collections.EMPTY_LIST;
final List<RouteInfo> 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<String> 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<String> 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<String> computeLocalPrefixStrings(
Set<IpPrefix> localPrefixes, LinkProperties upstreamLinkProperties) {
// Create an editable copy.
final Set<IpPrefix> 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<String> 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;
}
}
}

View File

@@ -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<String> 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<String> 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;
}
}
}
}

View File

@@ -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",

View File

@@ -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<ArrayList> mStringArrayCaptor =
ArgumentCaptor.forClass(ArrayList.class);
private final ArgumentCaptor<ITetheringStatsProvider.Stub> mTetherStatsProviderCaptor =
ArgumentCaptor.forClass(ITetheringStatsProvider.Stub.class);
private final ArgumentCaptor<OffloadHardwareInterface.ControlCallback> 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<IpPrefix> 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<String> 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<String> 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<IpPrefix> 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<String> 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);
}
}