Merge "[Tether06] Migrate tether offload controller into module"
This commit is contained in:
@@ -27,6 +27,7 @@ java_defaults {
|
|||||||
"androidx.annotation_annotation",
|
"androidx.annotation_annotation",
|
||||||
"netd_aidl_interface-java",
|
"netd_aidl_interface-java",
|
||||||
"networkstack-aidl-interfaces-java",
|
"networkstack-aidl-interfaces-java",
|
||||||
|
"android.hardware.tetheroffload.control-V1.0-java",
|
||||||
"tethering-client",
|
"tethering-client",
|
||||||
],
|
],
|
||||||
manifest: "AndroidManifestBase.xml",
|
manifest: "AndroidManifestBase.xml",
|
||||||
@@ -38,11 +39,39 @@ android_library {
|
|||||||
defaults: ["TetheringAndroidLibraryDefaults"],
|
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.
|
// Common defaults for compiling the actual APK.
|
||||||
java_defaults {
|
java_defaults {
|
||||||
name: "TetheringAppDefaults",
|
name: "TetheringAppDefaults",
|
||||||
platform_apis: true,
|
platform_apis: true,
|
||||||
privileged: true,
|
privileged: true,
|
||||||
|
jni_libs: [
|
||||||
|
"libtetheroffloadjni",
|
||||||
|
],
|
||||||
resource_dirs: [
|
resource_dirs: [
|
||||||
"res",
|
"res",
|
||||||
],
|
],
|
||||||
@@ -71,6 +100,8 @@ filegroup {
|
|||||||
name: "tethering-servicescore-srcs",
|
name: "tethering-servicescore-srcs",
|
||||||
srcs: [
|
srcs: [
|
||||||
"src/com/android/server/connectivity/tethering/EntitlementManager.java",
|
"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/TetheringConfiguration.java",
|
||||||
"src/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java",
|
"src/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java",
|
||||||
],
|
],
|
||||||
@@ -88,3 +119,11 @@ filegroup {
|
|||||||
"src/android/net/util/PrefixUtils.java",
|
"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",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ android_test {
|
|||||||
static_libs: [
|
static_libs: [
|
||||||
"androidx.test.rules",
|
"androidx.test.rules",
|
||||||
"frameworks-base-testutils",
|
"frameworks-base-testutils",
|
||||||
|
"net-tests-utils",
|
||||||
"mockito-target-extended-minus-junit4",
|
"mockito-target-extended-minus-junit4",
|
||||||
"TetheringApiCurrentLib",
|
"TetheringApiCurrentLib",
|
||||||
"testables",
|
"testables",
|
||||||
@@ -46,6 +47,7 @@ filegroup {
|
|||||||
name: "tethering-tests-src",
|
name: "tethering-tests-src",
|
||||||
srcs: [
|
srcs: [
|
||||||
"src/com/android/server/connectivity/tethering/EntitlementManagerTest.java",
|
"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/TetheringConfigurationTest.java",
|
||||||
"src/com/android/server/connectivity/tethering/UpstreamNetworkMonitorTest.java",
|
"src/com/android/server/connectivity/tethering/UpstreamNetworkMonitorTest.java",
|
||||||
"src/android/net/dhcp/DhcpServingParamsParcelExtTest.java",
|
"src/android/net/dhcp/DhcpServingParamsParcelExtTest.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<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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user