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;
// 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;
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) {
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) {
}
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);
}
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.
private void handleUpstreamNetworkMonitorCallback(int arg1, Object o) {
if (arg1 == UpstreamNetworkMonitor.NOTIFY_LOCAL_PREFIXES) {
- sendOffloadExemptPrefixes((Set<IpPrefix>) o);
+ mOffload.sendOffloadExemptPrefixes((Set<IpPrefix>) o);
return;
}
// TODO: De-duplicate with updateUpstreamWanted() below.
if (upstreamWanted()) {
mUpstreamWanted = true;
- startOffloadController();
+ mOffload.start();
chooseUpstreamType(true);
mTryCell = false;
}
@Override
public void exit() {
- mOffloadController.stop();
+ mOffload.stop();
mUpstreamNetworkMonitor.stop();
mSimChange.stopListening();
notifyDownstreamsOfNewUpstreamIface(null);
mUpstreamWanted = upstreamWanted();
if (mUpstreamWanted != previousUpstreamWanted) {
if (mUpstreamWanted) {
- startOffloadController();
+ mOffload.start();
} else {
- mOffloadController.stop();
+ mOffload.stop();
}
}
return previousUpstreamWanted;
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;
}
} 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
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;
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;
mNms = nms;
mStatsProvider = new OffloadTetheringStatsProvider();
mLog = log.forSubComponent(TAG);
+ mDownstreams = new HashMap<>();
mExemptPrefixes = new HashSet<>();
mLastLocalPrefixStrs = new HashSet<>();
}
}
+ private String currentUpstreamInterface() {
+ return (mUpstreamLinkProperties != null)
+ ? mUpstreamLinkProperties.getInterfaceName() : null;
+ }
+
private void maybeUpdateStats(String iface) {
if (TextUtils.isEmpty(iface)) {
return;
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;
}
}
private void updateStatsForCurrentUpstream() {
- if (mUpstreamLinkProperties != null) {
- maybeUpdateStats(mUpstreamLinkProperties.getInterfaceName());
- }
+ maybeUpdateStats(currentUpstreamInterface());
}
public void setUpstreamLinkProperties(LinkProperties lp) {
}
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() {
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");
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);
}
private final LinkProperties mLinkProperties;
private int mLastError;
+ private int mServingMode;
private String mMyUpstreamIfaceName; // may change over time
private NetworkInterface mNetworkInterface;
private byte[] mHwAddr;
mLinkProperties = new LinkProperties();
resetLinkProperties();
mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
+ mServingMode = IControlsTethering.STATE_AVAILABLE;
mInitialState = new InitialState();
mLocalHotspotState = new LocalHotspotState();
public int lastError() { return mLastError; }
+ public int servingMode() { return mServingMode; }
+
public LinkProperties linkProperties() { return new LinkProperties(mLinkProperties); }
public void stop() { sendMessage(CMD_INTERFACE_DOWN); }
}
private void sendInterfaceState(int newInterfaceState) {
+ mServingMode = newInterfaceState;
mTetherController.updateInterfaceState(
TetherInterfaceStateMachine.this, newInterfaceState, mLastError);
sendLinkProperties();
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;
/**
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;
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;
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);
}
@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;
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);
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(
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());
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));
+ }
+ }
}