OSDN Git Service

Send add/removeDownstream info to offload HAL
authorErik Kline <ek@google.com>
Thu, 6 Jul 2017 10:49:35 +0000 (19:49 +0900)
committerErik Kline <ek@google.com>
Thu, 24 Aug 2017 03:55:47 +0000 (20:55 -0700)
Test: as follows
    - built
    - flashed
    - booted
    - "runtest frameworks-net" passed
Bug: 29337859
Bug: 32163131

Merged-In: I0cb81ac054fc2bf6c8b8bfe658e9404a15091d7a
Merged-In: I7abcdcc2d7d967179c47081a6db2b417164891f3
Change-Id: I6c59aa7cb80b54f376f294b24c1409710c553d74
(cherry picked from commit ed962a84122ab950a103fbbc0bf911f61b6b9500)

services/core/java/com/android/server/connectivity/Tethering.java
services/core/java/com/android/server/connectivity/tethering/OffloadController.java
services/core/java/com/android/server/connectivity/tethering/OffloadHardwareInterface.java
services/core/java/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java
services/net/java/android/net/util/NetworkConstants.java
services/net/java/android/net/util/PrefixUtils.java
tests/net/java/com/android/server/connectivity/tethering/OffloadControllerTest.java

index 836d61a..5583e86 100644 (file)
@@ -49,6 +49,7 @@ import android.net.ConnectivityManager;
 import android.net.INetworkPolicyManager;
 import android.net.INetworkStatsService;
 import android.net.IpPrefix;
+import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
@@ -1252,6 +1253,7 @@ public class Tethering extends BaseNetworkObserver {
         // to tear itself down.
         private final ArrayList<TetherInterfaceStateMachine> mNotifyList;
         private final IPv6TetheringCoordinator mIPv6TetheringCoordinator;
+        private final OffloadWrapper mOffload;
 
         private static final int UPSTREAM_SETTLE_TIME_MS     = 10000;
 
@@ -1276,33 +1278,11 @@ public class Tethering extends BaseNetworkObserver {
 
             mNotifyList = new ArrayList<>();
             mIPv6TetheringCoordinator = new IPv6TetheringCoordinator(mNotifyList, mLog);
+            mOffload = new OffloadWrapper();
 
             setInitialState(mInitialState);
         }
 
-        private void startOffloadController() {
-            mOffloadController.start();
-            sendOffloadExemptPrefixes();
-        }
-
-        private void sendOffloadExemptPrefixes() {
-            sendOffloadExemptPrefixes(mUpstreamNetworkMonitor.getLocalPrefixes());
-        }
-
-        private void sendOffloadExemptPrefixes(Set<IpPrefix> localPrefixes) {
-            // Add in well-known minimum set.
-            PrefixUtils.addNonForwardablePrefixes(localPrefixes);
-            // Add tragically hardcoded prefixes.
-            localPrefixes.add(PrefixUtils.DEFAULT_WIFI_P2P_PREFIX);
-
-            // Add prefixes for all downstreams, regardless of IP serving mode.
-            for (TetherInterfaceStateMachine tism : mNotifyList) {
-                localPrefixes.addAll(PrefixUtils.localPrefixesFrom(tism.linkProperties()));
-            }
-
-            mOffloadController.setLocalPrefixes(localPrefixes);
-        }
-
         class InitialState extends State {
             @Override
             public boolean processMessage(Message message) {
@@ -1460,7 +1440,7 @@ public class Tethering extends BaseNetworkObserver {
 
         protected void handleNewUpstreamNetworkState(NetworkState ns) {
             mIPv6TetheringCoordinator.updateUpstreamNetworkState(ns);
-            mOffloadController.setUpstreamLinkProperties((ns != null) ? ns.linkProperties : null);
+            mOffload.updateUpstreamNetworkState(ns);
         }
 
         private void handleInterfaceServingStateActive(int mode, TetherInterfaceStateMachine who) {
@@ -1470,9 +1450,12 @@ public class Tethering extends BaseNetworkObserver {
             }
 
             if (mode == IControlsTethering.STATE_TETHERED) {
+                // No need to notify OffloadController just yet as there are no
+                // "offload-able" prefixes to pass along. This will handled
+                // when the TISM informs Tethering of its LinkProperties.
                 mForwardedDownstreams.add(who);
             } else {
-                mOffloadController.removeDownstreamInterface(who.interfaceName());
+                mOffload.excludeDownstreamInterface(who.interfaceName());
                 mForwardedDownstreams.remove(who);
             }
 
@@ -1497,7 +1480,7 @@ public class Tethering extends BaseNetworkObserver {
         private void handleInterfaceServingStateInactive(TetherInterfaceStateMachine who) {
             mNotifyList.remove(who);
             mIPv6TetheringCoordinator.removeActiveDownstream(who);
-            mOffloadController.removeDownstreamInterface(who.interfaceName());
+            mOffload.excludeDownstreamInterface(who.interfaceName());
             mForwardedDownstreams.remove(who);
 
             // If this is a Wi-Fi interface, tell WifiManager of any errors.
@@ -1511,7 +1494,7 @@ public class Tethering extends BaseNetworkObserver {
 
         private void handleUpstreamNetworkMonitorCallback(int arg1, Object o) {
             if (arg1 == UpstreamNetworkMonitor.NOTIFY_LOCAL_PREFIXES) {
-                sendOffloadExemptPrefixes((Set<IpPrefix>) o);
+                mOffload.sendOffloadExemptPrefixes((Set<IpPrefix>) o);
                 return;
             }
 
@@ -1581,7 +1564,7 @@ public class Tethering extends BaseNetworkObserver {
                 // TODO: De-duplicate with updateUpstreamWanted() below.
                 if (upstreamWanted()) {
                     mUpstreamWanted = true;
-                    startOffloadController();
+                    mOffload.start();
                     chooseUpstreamType(true);
                     mTryCell = false;
                 }
@@ -1589,7 +1572,7 @@ public class Tethering extends BaseNetworkObserver {
 
             @Override
             public void exit() {
-                mOffloadController.stop();
+                mOffload.stop();
                 mUpstreamNetworkMonitor.stop();
                 mSimChange.stopListening();
                 notifyDownstreamsOfNewUpstreamIface(null);
@@ -1601,9 +1584,9 @@ public class Tethering extends BaseNetworkObserver {
                 mUpstreamWanted = upstreamWanted();
                 if (mUpstreamWanted != previousUpstreamWanted) {
                     if (mUpstreamWanted) {
-                        startOffloadController();
+                        mOffload.start();
                     } else {
-                        mOffloadController.stop();
+                        mOffload.stop();
                     }
                 }
                 return previousUpstreamWanted;
@@ -1658,12 +1641,9 @@ public class Tethering extends BaseNetworkObserver {
                     case EVENT_IFACE_UPDATE_LINKPROPERTIES: {
                         final LinkProperties newLp = (LinkProperties) message.obj;
                         if (message.arg1 == IControlsTethering.STATE_TETHERED) {
-                            mOffloadController.notifyDownstreamLinkProperties(newLp);
+                            mOffload.updateDownstreamLinkProperties(newLp);
                         } else {
-                            mOffloadController.removeDownstreamInterface(newLp.getInterfaceName());
-                            // Another interface might be in local-only hotspot mode;
-                            // resend all local prefixes to the OffloadController.
-                            sendOffloadExemptPrefixes();
+                            mOffload.excludeDownstreamInterface(newLp.getInterfaceName());
                         }
                         break;
                     }
@@ -1778,6 +1758,82 @@ public class Tethering extends BaseNetworkObserver {
                 } catch (Exception e) {}
             }
         }
+
+        // A wrapper class to handle multiple situations where several calls to
+        // the OffloadController need to happen together.
+        //
+        // TODO: This suggests that the interface between OffloadController and
+        // Tethering is in need of improvement. Refactor these calls into the
+        // OffloadController implementation.
+        class OffloadWrapper {
+            public void start() {
+                mOffloadController.start();
+                sendOffloadExemptPrefixes();
+            }
+
+            public void stop() {
+                mOffloadController.stop();
+            }
+
+            public void updateUpstreamNetworkState(NetworkState ns) {
+                mOffloadController.setUpstreamLinkProperties(
+                        (ns != null) ? ns.linkProperties : null);
+            }
+
+            public void updateDownstreamLinkProperties(LinkProperties newLp) {
+                // Update the list of offload-exempt prefixes before adding
+                // new prefixes on downstream interfaces to the offload HAL.
+                sendOffloadExemptPrefixes();
+                mOffloadController.notifyDownstreamLinkProperties(newLp);
+            }
+
+            public void excludeDownstreamInterface(String ifname) {
+                // This and other interfaces may be in local-only hotspot mode;
+                // resend all local prefixes to the OffloadController.
+                sendOffloadExemptPrefixes();
+                mOffloadController.removeDownstreamInterface(ifname);
+            }
+
+            public void sendOffloadExemptPrefixes() {
+                sendOffloadExemptPrefixes(mUpstreamNetworkMonitor.getLocalPrefixes());
+            }
+
+            public void sendOffloadExemptPrefixes(final Set<IpPrefix> localPrefixes) {
+                // Add in well-known minimum set.
+                PrefixUtils.addNonForwardablePrefixes(localPrefixes);
+                // Add tragically hardcoded prefixes.
+                localPrefixes.add(PrefixUtils.DEFAULT_WIFI_P2P_PREFIX);
+
+                // Maybe add prefixes or addresses for downstreams, depending on
+                // the IP serving mode of each.
+                for (TetherInterfaceStateMachine tism : mNotifyList) {
+                    final LinkProperties lp = tism.linkProperties();
+
+                    switch (tism.servingMode()) {
+                        case IControlsTethering.STATE_UNAVAILABLE:
+                        case IControlsTethering.STATE_AVAILABLE:
+                            // No usable LinkProperties in these states.
+                            continue;
+                        case IControlsTethering.STATE_TETHERED:
+                            // Only add IPv4 /32 and IPv6 /128 prefixes. The
+                            // directly-connected prefixes will be sent as
+                            // downstream "offload-able" prefixes.
+                            for (LinkAddress addr : lp.getAllLinkAddresses()) {
+                                final InetAddress ip = addr.getAddress();
+                                if (ip.isLinkLocalAddress()) continue;
+                                localPrefixes.add(PrefixUtils.ipAddressAsPrefix(ip));
+                            }
+                            break;
+                        case IControlsTethering.STATE_LOCAL_ONLY:
+                            // Add prefixes covering all local IPs.
+                            localPrefixes.addAll(PrefixUtils.localPrefixesFrom(lp));
+                            break;
+                    }
+                }
+
+                mOffloadController.setLocalPrefixes(localPrefixes);
+            }
+        }
     }
 
     @Override
index ef18e4e..6d5c428 100644 (file)
@@ -48,6 +48,7 @@ import java.net.InetAddress;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -69,6 +70,7 @@ public class OffloadController {
     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;
@@ -100,6 +102,7 @@ public class OffloadController {
         mNms = nms;
         mStatsProvider = new OffloadTetheringStatsProvider();
         mLog = log.forSubComponent(TAG);
+        mDownstreams = new HashMap<>();
         mExemptPrefixes = new HashSet<>();
         mLastLocalPrefixStrs = new HashSet<>();
 
@@ -257,6 +260,11 @@ public class OffloadController {
         }
     }
 
+    private String currentUpstreamInterface() {
+        return (mUpstreamLinkProperties != null)
+                ? mUpstreamLinkProperties.getInterfaceName() : null;
+    }
+
     private void maybeUpdateStats(String iface) {
         if (TextUtils.isEmpty(iface)) {
             return;
@@ -281,9 +289,7 @@ public class OffloadController {
 
     private boolean maybeUpdateDataLimit(String iface) {
         // setDataLimit may only be called while offload is occuring on this upstream.
-        if (!started() ||
-                mUpstreamLinkProperties == null ||
-                !TextUtils.equals(iface, mUpstreamLinkProperties.getInterfaceName())) {
+        if (!started() || !TextUtils.equals(iface, currentUpstreamInterface())) {
             return true;
         }
 
@@ -296,9 +302,7 @@ public class OffloadController {
     }
 
     private void updateStatsForCurrentUpstream() {
-        if (mUpstreamLinkProperties != null) {
-            maybeUpdateStats(mUpstreamLinkProperties.getInterfaceName());
-        }
+        maybeUpdateStats(currentUpstreamInterface());
     }
 
     public void setUpstreamLinkProperties(LinkProperties lp) {
@@ -325,17 +329,42 @@ public class OffloadController {
     }
 
     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;
 
-        // TODO: Cache LinkProperties on a per-ifname basis and compute the
-        // deltas, calling addDownstream()/removeDownstream() accordingly.
+        final List<RouteInfo> oldRoutes = (oldLp != null) ? oldLp.getRoutes() : new ArrayList<>();
+        final List<RouteInfo> newRoutes = lp.getRoutes();
+
+        // For each old route, if not in new routes: remove.
+        for (RouteInfo oldRoute : oldRoutes) {
+            if (shouldIgnoreDownstreamRoute(oldRoute)) continue;
+            if (!newRoutes.contains(oldRoute)) {
+                mHwInterface.removeDownstreamPrefix(ifname, oldRoute.getDestination().toString());
+            }
+        }
+
+        // For each new route, if not in old routes: add.
+        for (RouteInfo newRoute : newRoutes) {
+            if (shouldIgnoreDownstreamRoute(newRoute)) continue;
+            if (!oldRoutes.contains(newRoute)) {
+                mHwInterface.addDownstreamPrefix(ifname, newRoute.getDestination().toString());
+            }
+        }
     }
 
     public void removeDownstreamInterface(String ifname) {
+        final LinkProperties lp = mDownstreams.remove(ifname);
+        if (lp == null) return;
+
         if (!started()) return;
 
-        // TODO: Check cache for LinkProperties of ifname and, if present,
-        // call removeDownstream() accordingly.
+        for (RouteInfo route : lp.getRoutes()) {
+            if (shouldIgnoreDownstreamRoute(route)) continue;
+            mHwInterface.removeDownstreamPrefix(ifname, route.getDestination().toString());
+        }
     }
 
     private boolean isOffloadDisabled() {
@@ -442,6 +471,13 @@ public class OffloadController {
         return localPrefixStrs;
     }
 
+    private static boolean shouldIgnoreDownstreamRoute(RouteInfo route) {
+        // Ignore any link-local routes.
+        if (!route.getDestinationLinkAddress().isGlobalPreferred()) return true;
+
+        return false;
+    }
+
     public void dump(IndentingPrintWriter pw) {
         if (isOffloadDisabled()) {
             pw.println("Offload disabled");
index 86ff0a6..865a989 100644 (file)
@@ -236,6 +236,44 @@ public class OffloadHardwareInterface {
         return results.success;
     }
 
+    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.success = success;
+                        results.errMsg = errMsg;
+                    });
+        } catch (RemoteException e) {
+            record(logmsg, e);
+            return false;
+        }
+
+        record(logmsg, results);
+        return results.success;
+    }
+
+    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.success = success;
+                        results.errMsg = errMsg;
+                    });
+        } catch (RemoteException e) {
+            record(logmsg, e);
+            return false;
+        }
+
+        record(logmsg, results);
+        return results.success;
+    }
+
     private void record(String msg, Throwable t) {
         mLog.e(msg + YIELDS + "exception: " + t);
     }
index 69678df..57d2502 100644 (file)
@@ -115,6 +115,7 @@ public class TetherInterfaceStateMachine extends StateMachine {
     private final LinkProperties mLinkProperties;
 
     private int mLastError;
+    private int mServingMode;
     private String mMyUpstreamIfaceName;  // may change over time
     private NetworkInterface mNetworkInterface;
     private byte[] mHwAddr;
@@ -142,6 +143,7 @@ public class TetherInterfaceStateMachine extends StateMachine {
         mLinkProperties = new LinkProperties();
         resetLinkProperties();
         mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
+        mServingMode = IControlsTethering.STATE_AVAILABLE;
 
         mInitialState = new InitialState();
         mLocalHotspotState = new LocalHotspotState();
@@ -161,6 +163,8 @@ public class TetherInterfaceStateMachine extends StateMachine {
 
     public int lastError() { return mLastError; }
 
+    public int servingMode() { return mServingMode; }
+
     public LinkProperties linkProperties() { return new LinkProperties(mLinkProperties); }
 
     public void stop() { sendMessage(CMD_INTERFACE_DOWN); }
@@ -448,6 +452,7 @@ public class TetherInterfaceStateMachine extends StateMachine {
     }
 
     private void sendInterfaceState(int newInterfaceState) {
+        mServingMode = newInterfaceState;
         mTetherController.updateInterfaceState(
                 TetherInterfaceStateMachine.this, newInterfaceState, mLastError);
         sendLinkProperties();
index 9b3bc3f..6065268 100644 (file)
@@ -87,6 +87,7 @@ public final class NetworkConstants {
     public static final int IPV4_PROTOCOL_OFFSET = 9;
     public static final int IPV4_SRC_ADDR_OFFSET = 12;
     public static final int IPV4_DST_ADDR_OFFSET = 16;
+    public static final int IPV4_ADDR_BITS = 32;
     public static final int IPV4_ADDR_LEN = 4;
 
     /**
@@ -99,6 +100,7 @@ public final class NetworkConstants {
     public static final int IPV6_PROTOCOL_OFFSET = 6;
     public static final int IPV6_SRC_ADDR_OFFSET = 8;
     public static final int IPV6_DST_ADDR_OFFSET = 24;
+    public static final int IPV6_ADDR_BITS = 128;
     public static final int IPV6_ADDR_LEN = 16;
     public static final int IPV6_MIN_MTU = 1280;
     public static final int RFC7421_PREFIX_LENGTH = 64;
index 962aab4..f60694a 100644 (file)
@@ -20,6 +20,8 @@ import android.net.IpPrefix;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
 
+import java.net.Inet4Address;
+import java.net.InetAddress;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
@@ -68,6 +70,13 @@ public class PrefixUtils {
         return new IpPrefix(addr.getAddress(), addr.getPrefixLength());
     }
 
+    public static IpPrefix ipAddressAsPrefix(InetAddress ip) {
+        final int bitLength = (ip instanceof Inet4Address)
+                ? NetworkConstants.IPV4_ADDR_BITS
+                : NetworkConstants.IPV6_ADDR_BITS;
+        return new IpPrefix(ip, bitLength);
+    }
+
     private static IpPrefix pfx(String prefixStr) {
         return new IpPrefix(prefixStr);
     }
index c769492..679c369 100644 (file)
@@ -79,6 +79,15 @@ import org.mockito.MockitoAnnotations;
 @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";
 
     @Mock private OffloadHardwareInterface mHardware;
     @Mock private ApplicationInfo mApplicationInfo;
@@ -238,10 +247,8 @@ public class OffloadControllerTest {
         inOrder.verify(mHardware, times(1)).setLocalPrefixes(mStringArrayCaptor.capture());
         ArrayList<String> localPrefixes = mStringArrayCaptor.getValue();
         assertEquals(4, localPrefixes.size());
-        assertTrue(localPrefixes.contains("127.0.0.0/8"));
-        assertTrue(localPrefixes.contains("192.0.2.0/24"));
-        assertTrue(localPrefixes.contains("fe80::/64"));
-        assertTrue(localPrefixes.contains("2001:db8::/64"));
+        assertArrayListContains(localPrefixes,
+                "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64");
         inOrder.verifyNoMoreInteractions();
 
         offload.setUpstreamLinkProperties(null);
@@ -356,12 +363,9 @@ public class OffloadControllerTest {
         inOrder.verify(mHardware, times(1)).setLocalPrefixes(mStringArrayCaptor.capture());
         localPrefixes = mStringArrayCaptor.getValue();
         assertEquals(6, localPrefixes.size());
-        assertTrue(localPrefixes.contains("127.0.0.0/8"));
-        assertTrue(localPrefixes.contains("192.0.2.0/24"));
-        assertTrue(localPrefixes.contains("fe80::/64"));
-        assertTrue(localPrefixes.contains("2001:db8::/64"));
-        assertTrue(localPrefixes.contains("2001:db8::6173:7369:676e:6564/128"));
-        assertTrue(localPrefixes.contains("2001:db8::7261:6e64:6f6d/128"));
+        assertArrayListContains(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(
@@ -445,6 +449,8 @@ public class OffloadControllerTest {
         waitForIdle();
         // There is no current upstream, so no stats are fetched.
         inOrder.verify(mHardware, never()).getForwardedStats(eq(ethernetIface));
+        inOrder.verify(mHardware, times(1)).setUpstreamParameters(
+                eq(null), eq(null), eq(null), eq(null));
         inOrder.verifyNoMoreInteractions();
 
         assertEquals(2, stats.size());
@@ -549,4 +555,79 @@ public class OffloadControllerTest {
         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();
+    }
+
+    private static void assertArrayListContains(ArrayList<String> list, String... elems) {
+        for (String element : elems) {
+            assertTrue(list.contains(element));
+        }
+    }
 }