[Tether06] Migrate tether offload controller into module

The tether offload JNI library in this patch still have many dependencies
with internal libraries. Will have follow up changes to cut the
dependencies to let it be a unbundled library.

Bug: 136040414
Test: -build, flash, boot
      -atest TetheringTests
      -atest FrameworksNetTests

Change-Id: Iacf8e0b94135e35672de3ee77c474ee39a00c591
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 998572f..61bfb92 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -27,6 +27,7 @@
         "androidx.annotation_annotation",
         "netd_aidl_interface-java",
         "networkstack-aidl-interfaces-java",
+        "android.hardware.tetheroffload.control-V1.0-java",
         "tethering-client",
     ],
     manifest: "AndroidManifestBase.xml",
@@ -38,11 +39,39 @@
     defaults: ["TetheringAndroidLibraryDefaults"],
 }
 
+cc_library_shared {
+    name: "libtetheroffloadjni",
+    srcs: [
+        "jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp",
+    ],
+    shared_libs: [
+        "libnativehelper",
+        "libcutils",
+        "android.hardware.tetheroffload.config@1.0",
+    ],
+    static_libs: [
+        "liblog",
+        "libbase",
+        "libhidlbase",
+        "libutils",
+    ],
+
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+        "-Wthread-safety",
+    ],
+}
+
 // Common defaults for compiling the actual APK.
 java_defaults {
     name: "TetheringAppDefaults",
     platform_apis: true,
     privileged: true,
+    jni_libs: [
+        "libtetheroffloadjni",
+    ],
     resource_dirs: [
         "res",
     ],
@@ -71,6 +100,8 @@
     name: "tethering-servicescore-srcs",
     srcs: [
         "src/com/android/server/connectivity/tethering/EntitlementManager.java",
+        "src/com/android/server/connectivity/tethering/OffloadController.java",
+        "src/com/android/server/connectivity/tethering/OffloadHardwareInterface.java",
         "src/com/android/server/connectivity/tethering/TetheringConfiguration.java",
         "src/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java",
     ],
@@ -88,3 +119,11 @@
         "src/android/net/util/PrefixUtils.java",
     ],
 }
+
+// This group would be removed when tethering migration is done.
+filegroup {
+    name: "tethering-jni-srcs",
+    srcs: [
+        "jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp",
+    ],
+}
diff --git a/Tethering/jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp b/Tethering/jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp
new file mode 100644
index 0000000..3eaf488
--- /dev/null
+++ b/Tethering/jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <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
diff --git a/Tethering/src/com/android/server/connectivity/tethering/OffloadController.java b/Tethering/src/com/android/server/connectivity/tethering/OffloadController.java
new file mode 100644
index 0000000..16734d8
--- /dev/null
+++ b/Tethering/src/com/android/server/connectivity/tethering/OffloadController.java
@@ -0,0 +1,671 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.tethering;
+
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.STATS_PER_UID;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+import static android.net.TrafficStats.UID_TETHERING;
+import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED;
+
+import android.content.ContentResolver;
+import android.net.ITetheringStatsProvider;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.NetworkStats;
+import android.net.RouteInfo;
+import android.net.netlink.ConntrackMessage;
+import android.net.netlink.NetlinkConstants;
+import android.net.netlink.NetlinkSocket;
+import android.net.util.IpUtils;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+import android.text.TextUtils;
+
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.connectivity.tethering.OffloadHardwareInterface.ForwardedStats;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * A class to encapsulate the business logic of programming the tethering
+ * hardware offload interface.
+ *
+ * @hide
+ */
+public class OffloadController {
+    private static final String TAG = OffloadController.class.getSimpleName();
+    private static final boolean DBG = false;
+    private static final String ANYIP = "0.0.0.0";
+    private static final ForwardedStats EMPTY_STATS = new ForwardedStats();
+
+    private enum UpdateType { IF_NEEDED, FORCE };
+
+    private final Handler mHandler;
+    private final OffloadHardwareInterface mHwInterface;
+    private final ContentResolver mContentResolver;
+    private final INetworkManagementService mNms;
+    private final ITetheringStatsProvider mStatsProvider;
+    private final SharedLog mLog;
+    private final HashMap<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;
+        }
+    }
+}
diff --git a/Tethering/src/com/android/server/connectivity/tethering/OffloadHardwareInterface.java b/Tethering/src/com/android/server/connectivity/tethering/OffloadHardwareInterface.java
new file mode 100644
index 0000000..01339a4
--- /dev/null
+++ b/Tethering/src/com/android/server/connectivity/tethering/OffloadHardwareInterface.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.tethering;
+
+import static com.android.internal.util.BitUtils.uint16;
+
+import android.hardware.tetheroffload.control.V1_0.IOffloadControl;
+import android.hardware.tetheroffload.control.V1_0.ITetheringOffloadCallback;
+import android.hardware.tetheroffload.control.V1_0.NatTimeoutUpdate;
+import android.hardware.tetheroffload.control.V1_0.NetworkProtocol;
+import android.hardware.tetheroffload.control.V1_0.OffloadCallbackEvent;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.system.OsConstants;
+
+import java.util.ArrayList;
+
+
+/**
+ * Capture tethering dependencies, for injection.
+ *
+ * @hide
+ */
+public class OffloadHardwareInterface {
+    private static final String TAG = OffloadHardwareInterface.class.getSimpleName();
+    private static final String YIELDS = " -> ";
+    // Change this value to control whether tether offload is enabled or
+    // disabled by default in the absence of an explicit Settings value.
+    // See accompanying unittest to distinguish 0 from non-0 values.
+    private static final int DEFAULT_TETHER_OFFLOAD_DISABLED = 0;
+    private static final String NO_INTERFACE_NAME = "";
+    private static final String NO_IPV4_ADDRESS = "";
+    private static final String NO_IPV4_GATEWAY = "";
+
+    private static native boolean configOffload();
+
+    private final Handler mHandler;
+    private final SharedLog mLog;
+    private IOffloadControl mOffloadControl;
+    private TetheringOffloadCallback mTetheringOffloadCallback;
+    private ControlCallback mControlCallback;
+
+    /** The callback to notify status of offload management process. */
+    public static class ControlCallback {
+        /** Offload started. */
+        public void onStarted() {}
+        /**
+         * Offload stopped because an error has occurred in lower layer.
+         */
+        public void onStoppedError() {}
+        /**
+         * Offload stopped because the device has moved to a bearer on which hardware offload is
+         * not supported. Subsequent calls to setUpstreamParameters and add/removeDownstream will
+         * likely fail and cannot be presumed to be saved inside of the hardware management process.
+         * Upon receiving #onSupportAvailable(), the caller should reprogram the hardware to begin
+         * offload again.
+         */
+        public void onStoppedUnsupported() {}
+        /** Indicate that offload is able to proivde support for this time. */
+        public void onSupportAvailable() {}
+        /** Offload stopped because of usage limit reached. */
+        public void onStoppedLimitReached() {}
+
+        /** Indicate to update NAT timeout. */
+        public void onNatTimeoutUpdate(int proto,
+                                       String srcAddr, int srcPort,
+                                       String dstAddr, int dstPort) {}
+    }
+
+    /** The object which records Tx/Rx forwarded bytes. */
+    public static class ForwardedStats {
+        public long rxBytes;
+        public long txBytes;
+
+        public ForwardedStats() {
+            rxBytes = 0;
+            txBytes = 0;
+        }
+
+        /** Add Tx/Rx bytes. */
+        public void add(ForwardedStats other) {
+            rxBytes += other.rxBytes;
+            txBytes += other.txBytes;
+        }
+
+        /** Returns the string representation of this object. */
+        public String toString() {
+            return String.format("rx:%s tx:%s", rxBytes, txBytes);
+        }
+    }
+
+    public OffloadHardwareInterface(Handler h, SharedLog log) {
+        mHandler = h;
+        mLog = log.forSubComponent(TAG);
+    }
+
+    /** Get default value indicating whether offload is supported. */
+    public int getDefaultTetherOffloadDisabled() {
+        return DEFAULT_TETHER_OFFLOAD_DISABLED;
+    }
+
+    /** Configure offload management process. */
+    public boolean initOffloadConfig() {
+        return configOffload();
+    }
+
+    /** Initialize the tethering offload HAL. */
+    public boolean initOffloadControl(ControlCallback controlCb) {
+        mControlCallback = controlCb;
+
+        if (mOffloadControl == null) {
+            try {
+                mOffloadControl = IOffloadControl.getService();
+            } catch (RemoteException e) {
+                mLog.e("tethering offload control not supported: " + e);
+                return false;
+            }
+            if (mOffloadControl == null) {
+                mLog.e("tethering IOffloadControl.getService() returned null");
+                return false;
+            }
+        }
+
+        final String logmsg = String.format("initOffloadControl(%s)",
+                (controlCb == null) ? "null"
+                        : "0x" + Integer.toHexString(System.identityHashCode(controlCb)));
+
+        mTetheringOffloadCallback = new TetheringOffloadCallback(mHandler, mControlCallback, mLog);
+        final CbResults results = new CbResults();
+        try {
+            mOffloadControl.initOffload(
+                    mTetheringOffloadCallback,
+                    (boolean success, String errMsg) -> {
+                        results.mSuccess = success;
+                        results.mErrMsg = errMsg;
+                    });
+        } catch (RemoteException e) {
+            record(logmsg, e);
+            return false;
+        }
+
+        record(logmsg, results);
+        return results.mSuccess;
+    }
+
+    /** Stop IOffloadControl. */
+    public void stopOffloadControl() {
+        if (mOffloadControl != null) {
+            try {
+                mOffloadControl.stopOffload(
+                        (boolean success, String errMsg) -> {
+                            if (!success) mLog.e("stopOffload failed: " + errMsg);
+                        });
+            } catch (RemoteException e) {
+                mLog.e("failed to stopOffload: " + e);
+            }
+        }
+        mOffloadControl = null;
+        mTetheringOffloadCallback = null;
+        mControlCallback = null;
+        mLog.log("stopOffloadControl()");
+    }
+
+    /** Get Tx/Rx usage from last query. */
+    public ForwardedStats getForwardedStats(String upstream) {
+        final String logmsg = String.format("getForwardedStats(%s)",  upstream);
+
+        final ForwardedStats stats = new ForwardedStats();
+        try {
+            mOffloadControl.getForwardedStats(
+                    upstream,
+                    (long rxBytes, long txBytes) -> {
+                        stats.rxBytes = (rxBytes > 0) ? rxBytes : 0;
+                        stats.txBytes = (txBytes > 0) ? txBytes : 0;
+                    });
+        } catch (RemoteException e) {
+            record(logmsg, e);
+            return stats;
+        }
+
+        mLog.log(logmsg + YIELDS + stats);
+        return stats;
+    }
+
+    /** Set local prefixes to offload management process. */
+    public boolean setLocalPrefixes(ArrayList<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;
+            }
+        }
+    }
+}
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index 7c06e5f..363be18 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -25,6 +25,7 @@
     static_libs: [
         "androidx.test.rules",
         "frameworks-base-testutils",
+        "net-tests-utils",
         "mockito-target-extended-minus-junit4",
         "TetheringApiCurrentLib",
         "testables",
@@ -46,6 +47,7 @@
     name: "tethering-tests-src",
     srcs: [
         "src/com/android/server/connectivity/tethering/EntitlementManagerTest.java",
+        "src/com/android/server/connectivity/tethering/OffloadControllerTest.java",
         "src/com/android/server/connectivity/tethering/TetheringConfigurationTest.java",
         "src/com/android/server/connectivity/tethering/UpstreamNetworkMonitorTest.java",
         "src/android/net/dhcp/DhcpServingParamsParcelExtTest.java",
diff --git a/Tethering/tests/unit/src/com/android/server/connectivity/tethering/OffloadControllerTest.java b/Tethering/tests/unit/src/com/android/server/connectivity/tethering/OffloadControllerTest.java
new file mode 100644
index 0000000..8574f54
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/server/connectivity/tethering/OffloadControllerTest.java
@@ -0,0 +1,730 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity.tethering;
+
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.STATS_PER_IFACE;
+import static android.net.NetworkStats.STATS_PER_UID;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+import static android.net.TrafficStats.UID_TETHERING;
+import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED;
+
+import static com.android.server.connectivity.tethering.OffloadHardwareInterface.ForwardedStats;
+import static com.android.testutils.MiscAssertsKt.assertContainsAll;
+import static com.android.testutils.MiscAssertsKt.assertThrows;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyLong;
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.net.ITetheringStatsProvider;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.NetworkStats;
+import android.net.RouteInfo;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.test.mock.MockContentResolver;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.testutils.HandlerUtilsKt;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class OffloadControllerTest {
+    private static final String RNDIS0 = "test_rndis0";
+    private static final String RMNET0 = "test_rmnet_data0";
+    private static final String WLAN0 = "test_wlan0";
+
+    private static final String IPV6_LINKLOCAL = "fe80::/64";
+    private static final String IPV6_DOC_PREFIX = "2001:db8::/64";
+    private static final String IPV6_DISCARD_PREFIX = "100::/64";
+    private static final String USB_PREFIX = "192.168.42.0/24";
+    private static final String WIFI_PREFIX = "192.168.43.0/24";
+    private static final long WAIT_FOR_IDLE_TIMEOUT = 2 * 1000;
+
+    @Mock private OffloadHardwareInterface mHardware;
+    @Mock private ApplicationInfo mApplicationInfo;
+    @Mock private Context mContext;
+    @Mock private INetworkManagementService mNMService;
+    private final ArgumentCaptor<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);
+    }
+
+}