[Tether03] Migrate IpServer into module
Add IpServer which is used to serve ip configuration, dhcp, dns proxy
and nat for downstream interface.
Bug: 136040414
Test: -build, flash, boot
-atest TetheringTests
-atest FrameworksNetTests
Change-Id: I23652ae0b9509abe7d38da96d523eb22ab00a343
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index dc88fd4..ca69c18 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -24,6 +24,8 @@
],
static_libs: [
"androidx.annotation_annotation",
+ "netd_aidl_interface-java",
+ "networkstack-aidl-interfaces-java",
"tethering-client",
],
manifest: "AndroidManifestBase.xml",
@@ -68,5 +70,10 @@
name: "tethering-services-srcs",
srcs: [
"src/com/android/server/connectivity/tethering/TetheringConfiguration.java",
+ "src/android/net/dhcp/DhcpServerCallbacks.java",
+ "src/android/net/dhcp/DhcpServingParamsParcelExt.java",
+ "src/android/net/ip/IpServer.java",
+ "src/android/net/ip/RouterAdvertisementDaemon.java",
+ "src/android/net/util/InterfaceSet.java",
],
}
diff --git a/Tethering/src/android/net/dhcp/DhcpServerCallbacks.java b/Tethering/src/android/net/dhcp/DhcpServerCallbacks.java
new file mode 100644
index 0000000..7c41377
--- /dev/null
+++ b/Tethering/src/android/net/dhcp/DhcpServerCallbacks.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2018 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 android.net.dhcp;
+
+/**
+ * Convenience wrapper around IDhcpServerCallbacks.Stub that implements getInterfaceVersion().
+ * @hide
+ */
+public abstract class DhcpServerCallbacks extends IDhcpServerCallbacks.Stub {
+ /**
+ * Get the version of the aidl interface implemented by the callbacks.
+ */
+ @Override
+ public int getInterfaceVersion() {
+ return IDhcpServerCallbacks.VERSION;
+ }
+}
diff --git a/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java b/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java
new file mode 100644
index 0000000..1fe2328
--- /dev/null
+++ b/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2018 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 android.net.dhcp;
+
+import static android.net.shared.Inet4AddressUtils.inet4AddressToIntHTH;
+
+import android.annotation.NonNull;
+import android.net.LinkAddress;
+
+import com.google.android.collect.Sets;
+
+import java.net.Inet4Address;
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * Subclass of {@link DhcpServingParamsParcel} with additional utility methods for building.
+ *
+ * <p>This utility class does not check for validity of the parameters: invalid parameters are
+ * reported by the receiving module when unparceling the parcel.
+ *
+ * @see DhcpServingParams
+ * @hide
+ */
+public class DhcpServingParamsParcelExt extends DhcpServingParamsParcel {
+ public static final int MTU_UNSET = 0;
+
+ /**
+ * Set the server address and served prefix for the DHCP server.
+ *
+ * <p>This parameter is required.
+ */
+ public DhcpServingParamsParcelExt setServerAddr(@NonNull LinkAddress serverAddr) {
+ this.serverAddr = inet4AddressToIntHTH((Inet4Address) serverAddr.getAddress());
+ this.serverAddrPrefixLength = serverAddr.getPrefixLength();
+ return this;
+ }
+
+ /**
+ * Set the default routers to be advertised to DHCP clients.
+ *
+ * <p>Each router must be inside the served prefix. This may be an empty set, but it must
+ * always be set explicitly.
+ */
+ public DhcpServingParamsParcelExt setDefaultRouters(@NonNull Set<Inet4Address> defaultRouters) {
+ this.defaultRouters = toIntArray(defaultRouters);
+ return this;
+ }
+
+ /**
+ * Set the default routers to be advertised to DHCP clients.
+ *
+ * <p>Each router must be inside the served prefix. This may be an empty list of routers,
+ * but it must always be set explicitly.
+ */
+ public DhcpServingParamsParcelExt setDefaultRouters(@NonNull Inet4Address... defaultRouters) {
+ return setDefaultRouters(Sets.newArraySet(defaultRouters));
+ }
+
+ /**
+ * Convenience method to build the parameters with no default router.
+ *
+ * <p>Equivalent to calling {@link #setDefaultRouters(Inet4Address...)} with no address.
+ */
+ public DhcpServingParamsParcelExt setNoDefaultRouter() {
+ return setDefaultRouters();
+ }
+
+ /**
+ * Set the DNS servers to be advertised to DHCP clients.
+ *
+ * <p>This may be an empty set, but it must always be set explicitly.
+ */
+ public DhcpServingParamsParcelExt setDnsServers(@NonNull Set<Inet4Address> dnsServers) {
+ this.dnsServers = toIntArray(dnsServers);
+ return this;
+ }
+
+ /**
+ * Set the DNS servers to be advertised to DHCP clients.
+ *
+ * <p>This may be an empty list of servers, but it must always be set explicitly.
+ */
+ public DhcpServingParamsParcelExt setDnsServers(@NonNull Inet4Address... dnsServers) {
+ return setDnsServers(Sets.newArraySet(dnsServers));
+ }
+
+ /**
+ * Convenience method to build the parameters with no DNS server.
+ *
+ * <p>Equivalent to calling {@link #setDnsServers(Inet4Address...)} with no address.
+ */
+ public DhcpServingParamsParcelExt setNoDnsServer() {
+ return setDnsServers();
+ }
+
+ /**
+ * Set excluded addresses that the DHCP server is not allowed to assign to clients.
+ *
+ * <p>This parameter is optional. DNS servers and default routers are always excluded
+ * and do not need to be set here.
+ */
+ public DhcpServingParamsParcelExt setExcludedAddrs(@NonNull Set<Inet4Address> excludedAddrs) {
+ this.excludedAddrs = toIntArray(excludedAddrs);
+ return this;
+ }
+
+ /**
+ * Set excluded addresses that the DHCP server is not allowed to assign to clients.
+ *
+ * <p>This parameter is optional. DNS servers and default routers are always excluded
+ * and do not need to be set here.
+ */
+ public DhcpServingParamsParcelExt setExcludedAddrs(@NonNull Inet4Address... excludedAddrs) {
+ return setExcludedAddrs(Sets.newArraySet(excludedAddrs));
+ }
+
+ /**
+ * Set the lease time for leases assigned by the DHCP server.
+ *
+ * <p>This parameter is required.
+ */
+ public DhcpServingParamsParcelExt setDhcpLeaseTimeSecs(long dhcpLeaseTimeSecs) {
+ this.dhcpLeaseTimeSecs = dhcpLeaseTimeSecs;
+ return this;
+ }
+
+ /**
+ * Set the link MTU to be advertised to DHCP clients.
+ *
+ * <p>If set to {@link #MTU_UNSET}, no MTU will be advertised to clients. This parameter
+ * is optional and defaults to {@link #MTU_UNSET}.
+ */
+ public DhcpServingParamsParcelExt setLinkMtu(int linkMtu) {
+ this.linkMtu = linkMtu;
+ return this;
+ }
+
+ /**
+ * Set whether the DHCP server should send the ANDROID_METERED vendor-specific option.
+ *
+ * <p>If not set, the default value is false.
+ */
+ public DhcpServingParamsParcelExt setMetered(boolean metered) {
+ this.metered = metered;
+ return this;
+ }
+
+ private static int[] toIntArray(@NonNull Collection<Inet4Address> addrs) {
+ int[] res = new int[addrs.size()];
+ int i = 0;
+ for (Inet4Address addr : addrs) {
+ res[i] = inet4AddressToIntHTH(addr);
+ i++;
+ }
+ return res;
+ }
+}
diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java
new file mode 100644
index 0000000..ff3d7bc
--- /dev/null
+++ b/Tethering/src/android/net/ip/IpServer.java
@@ -0,0 +1,1039 @@
+/*
+ * Copyright (C) 2016 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 android.net.ip;
+
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
+import static android.net.util.NetworkConstants.FF;
+import static android.net.util.NetworkConstants.RFC7421_PREFIX_LENGTH;
+import static android.net.util.NetworkConstants.asByte;
+
+import android.net.ConnectivityManager;
+import android.net.INetd;
+import android.net.INetworkStackStatusCallback;
+import android.net.INetworkStatsService;
+import android.net.InterfaceConfiguration;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.NetworkStackClient;
+import android.net.RouteInfo;
+import android.net.dhcp.DhcpServerCallbacks;
+import android.net.dhcp.DhcpServingParamsParcel;
+import android.net.dhcp.DhcpServingParamsParcelExt;
+import android.net.dhcp.IDhcpServer;
+import android.net.ip.RouterAdvertisementDaemon.RaParams;
+import android.net.util.InterfaceParams;
+import android.net.util.InterfaceSet;
+import android.net.util.NetdService;
+import android.net.util.SharedLog;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.util.MessageUtils;
+import com.android.internal.util.Protocol;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Random;
+import java.util.Set;
+
+/**
+ * Provides the interface to IP-layer serving functionality for a given network
+ * interface, e.g. for tethering or "local-only hotspot" mode.
+ *
+ * @hide
+ */
+public class IpServer extends StateMachine {
+ public static final int STATE_UNAVAILABLE = 0;
+ public static final int STATE_AVAILABLE = 1;
+ public static final int STATE_TETHERED = 2;
+ public static final int STATE_LOCAL_ONLY = 3;
+
+ /** Get string name of |state|.*/
+ public static String getStateString(int state) {
+ switch (state) {
+ case STATE_UNAVAILABLE: return "UNAVAILABLE";
+ case STATE_AVAILABLE: return "AVAILABLE";
+ case STATE_TETHERED: return "TETHERED";
+ case STATE_LOCAL_ONLY: return "LOCAL_ONLY";
+ }
+ return "UNKNOWN: " + state;
+ }
+
+ private static final byte DOUG_ADAMS = (byte) 42;
+
+ private static final String USB_NEAR_IFACE_ADDR = "192.168.42.129";
+ private static final int USB_PREFIX_LENGTH = 24;
+ private static final String WIFI_HOST_IFACE_ADDR = "192.168.43.1";
+ private static final int WIFI_HOST_IFACE_PREFIX_LENGTH = 24;
+ private static final String WIFI_P2P_IFACE_ADDR = "192.168.49.1";
+ private static final int WIFI_P2P_IFACE_PREFIX_LENGTH = 24;
+
+ // TODO: have PanService use some visible version of this constant
+ private static final String BLUETOOTH_IFACE_ADDR = "192.168.44.1";
+ private static final int BLUETOOTH_DHCP_PREFIX_LENGTH = 24;
+
+ // TODO: have this configurable
+ private static final int DHCP_LEASE_TIME_SECS = 3600;
+
+ private static final String TAG = "IpServer";
+ private static final boolean DBG = false;
+ private static final boolean VDBG = false;
+ private static final Class[] sMessageClasses = {
+ IpServer.class
+ };
+ private static final SparseArray<String> sMagicDecoderRing =
+ MessageUtils.findMessageNames(sMessageClasses);
+
+ /** IpServer callback. */
+ public static class Callback {
+ /**
+ * Notify that |who| has changed its tethering state.
+ *
+ * @param who the calling instance of IpServer
+ * @param state one of STATE_*
+ * @param lastError one of ConnectivityManager.TETHER_ERROR_*
+ */
+ public void updateInterfaceState(IpServer who, int state, int lastError) {}
+
+ /**
+ * Notify that |who| has new LinkProperties.
+ *
+ * @param who the calling instance of IpServer
+ * @param newLp the new LinkProperties to report
+ */
+ public void updateLinkProperties(IpServer who, LinkProperties newLp) {}
+ }
+
+ /** Capture IpServer dependencies, for injection. */
+ public static class Dependencies {
+ /** Create a RouterAdvertisementDaemon instance to be used by IpServer.*/
+ public RouterAdvertisementDaemon getRouterAdvertisementDaemon(InterfaceParams ifParams) {
+ return new RouterAdvertisementDaemon(ifParams);
+ }
+
+ /** Get |ifName|'s interface information.*/
+ public InterfaceParams getInterfaceParams(String ifName) {
+ return InterfaceParams.getByName(ifName);
+ }
+
+ public INetd getNetdService() {
+ return NetdService.getInstance();
+ }
+
+ /**
+ * Create a DhcpServer instance to be used by IpServer.
+ */
+ public void makeDhcpServer(String ifName, DhcpServingParamsParcel params,
+ DhcpServerCallbacks cb) {
+ NetworkStackClient.getInstance().makeDhcpServer(ifName, params, cb);
+ }
+ }
+
+ private static final int BASE_IFACE = Protocol.BASE_TETHERING + 100;
+ // request from the user that it wants to tether
+ public static final int CMD_TETHER_REQUESTED = BASE_IFACE + 2;
+ // request from the user that it wants to untether
+ public static final int CMD_TETHER_UNREQUESTED = BASE_IFACE + 3;
+ // notification that this interface is down
+ public static final int CMD_INTERFACE_DOWN = BASE_IFACE + 4;
+ // notification from the master SM that it had trouble enabling IP Forwarding
+ public static final int CMD_IP_FORWARDING_ENABLE_ERROR = BASE_IFACE + 7;
+ // notification from the master SM that it had trouble disabling IP Forwarding
+ public static final int CMD_IP_FORWARDING_DISABLE_ERROR = BASE_IFACE + 8;
+ // notification from the master SM that it had trouble starting tethering
+ public static final int CMD_START_TETHERING_ERROR = BASE_IFACE + 9;
+ // notification from the master SM that it had trouble stopping tethering
+ public static final int CMD_STOP_TETHERING_ERROR = BASE_IFACE + 10;
+ // notification from the master SM that it had trouble setting the DNS forwarders
+ public static final int CMD_SET_DNS_FORWARDERS_ERROR = BASE_IFACE + 11;
+ // the upstream connection has changed
+ public static final int CMD_TETHER_CONNECTION_CHANGED = BASE_IFACE + 12;
+ // new IPv6 tethering parameters need to be processed
+ public static final int CMD_IPV6_TETHER_UPDATE = BASE_IFACE + 13;
+
+ private final State mInitialState;
+ private final State mLocalHotspotState;
+ private final State mTetheredState;
+ private final State mUnavailableState;
+
+ private final SharedLog mLog;
+ private final INetworkManagementService mNMService;
+ private final INetd mNetd;
+ private final INetworkStatsService mStatsService;
+ private final Callback mCallback;
+ private final InterfaceController mInterfaceCtrl;
+
+ private final String mIfaceName;
+ private final int mInterfaceType;
+ private final LinkProperties mLinkProperties;
+ private final boolean mUsingLegacyDhcp;
+
+ private final Dependencies mDeps;
+
+ private int mLastError;
+ private int mServingMode;
+ private InterfaceSet mUpstreamIfaceSet; // may change over time
+ private InterfaceParams mInterfaceParams;
+ // TODO: De-duplicate this with mLinkProperties above. Currently, these link
+ // properties are those selected by the IPv6TetheringCoordinator and relayed
+ // to us. By comparison, mLinkProperties contains the addresses and directly
+ // connected routes that have been formed from these properties iff. we have
+ // succeeded in configuring them and are able to announce them within Router
+ // Advertisements (otherwise, we do not add them to mLinkProperties at all).
+ private LinkProperties mLastIPv6LinkProperties;
+ private RouterAdvertisementDaemon mRaDaemon;
+
+ // To be accessed only on the handler thread
+ private int mDhcpServerStartIndex = 0;
+ private IDhcpServer mDhcpServer;
+ private RaParams mLastRaParams;
+
+ public IpServer(
+ String ifaceName, Looper looper, int interfaceType, SharedLog log,
+ INetworkManagementService nMService, INetworkStatsService statsService,
+ Callback callback, boolean usingLegacyDhcp, Dependencies deps) {
+ super(ifaceName, looper);
+ mLog = log.forSubComponent(ifaceName);
+ mNMService = nMService;
+ mNetd = deps.getNetdService();
+ mStatsService = statsService;
+ mCallback = callback;
+ mInterfaceCtrl = new InterfaceController(ifaceName, mNetd, mLog);
+ mIfaceName = ifaceName;
+ mInterfaceType = interfaceType;
+ mLinkProperties = new LinkProperties();
+ mUsingLegacyDhcp = usingLegacyDhcp;
+ mDeps = deps;
+ resetLinkProperties();
+ mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
+ mServingMode = STATE_AVAILABLE;
+
+ mInitialState = new InitialState();
+ mLocalHotspotState = new LocalHotspotState();
+ mTetheredState = new TetheredState();
+ mUnavailableState = new UnavailableState();
+ addState(mInitialState);
+ addState(mLocalHotspotState);
+ addState(mTetheredState);
+ addState(mUnavailableState);
+
+ setInitialState(mInitialState);
+ }
+
+ /** Interface name which IpServer served.*/
+ public String interfaceName() {
+ return mIfaceName;
+ }
+
+ /**
+ * Tethering downstream type. It would be one of ConnectivityManager#TETHERING_*.
+ */
+ public int interfaceType() {
+ return mInterfaceType;
+ }
+
+ /** Last error from this IpServer. */
+ public int lastError() {
+ return mLastError;
+ }
+
+ /** Serving mode is the current state of IpServer state machine. */
+ public int servingMode() {
+ return mServingMode;
+ }
+
+ /** The properties of the network link which IpServer is serving. */
+ public LinkProperties linkProperties() {
+ return new LinkProperties(mLinkProperties);
+ }
+
+ /** Stop this IpServer. After this is called this IpServer should not be used any more. */
+ public void stop() {
+ sendMessage(CMD_INTERFACE_DOWN);
+ }
+
+ /**
+ * Tethering is canceled. IpServer state machine will be available and wait for
+ * next tethering request.
+ */
+ public void unwanted() {
+ sendMessage(CMD_TETHER_UNREQUESTED);
+ }
+
+ /** Internals. */
+
+ private boolean startIPv4() {
+ return configureIPv4(true);
+ }
+
+ /**
+ * Convenience wrapper around INetworkStackStatusCallback to run callbacks on the IpServer
+ * handler.
+ *
+ * <p>Different instances of this class can be created for each call to IDhcpServer methods,
+ * with different implementations of the callback, to differentiate handling of success/error in
+ * each call.
+ */
+ private abstract class OnHandlerStatusCallback extends INetworkStackStatusCallback.Stub {
+ @Override
+ public void onStatusAvailable(int statusCode) {
+ getHandler().post(() -> callback(statusCode));
+ }
+
+ public abstract void callback(int statusCode);
+
+ @Override
+ public int getInterfaceVersion() {
+ return this.VERSION;
+ }
+ }
+
+ private class DhcpServerCallbacksImpl extends DhcpServerCallbacks {
+ private final int mStartIndex;
+
+ private DhcpServerCallbacksImpl(int startIndex) {
+ mStartIndex = startIndex;
+ }
+
+ @Override
+ public void onDhcpServerCreated(int statusCode, IDhcpServer server) throws RemoteException {
+ getHandler().post(() -> {
+ // We are on the handler thread: mDhcpServerStartIndex can be read safely.
+ if (mStartIndex != mDhcpServerStartIndex) {
+ // This start request is obsolete. When the |server| binder token goes out of
+ // scope, the garbage collector will finalize it, which causes the network stack
+ // process garbage collector to collect the server itself.
+ return;
+ }
+
+ if (statusCode != STATUS_SUCCESS) {
+ mLog.e("Error obtaining DHCP server: " + statusCode);
+ handleError();
+ return;
+ }
+
+ mDhcpServer = server;
+ try {
+ mDhcpServer.start(new OnHandlerStatusCallback() {
+ @Override
+ public void callback(int startStatusCode) {
+ if (startStatusCode != STATUS_SUCCESS) {
+ mLog.e("Error starting DHCP server: " + startStatusCode);
+ handleError();
+ }
+ }
+ });
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ });
+ }
+
+ private void handleError() {
+ mLastError = ConnectivityManager.TETHER_ERROR_DHCPSERVER_ERROR;
+ transitionTo(mInitialState);
+ }
+ }
+
+ private boolean startDhcp(Inet4Address addr, int prefixLen) {
+ if (mUsingLegacyDhcp) {
+ return true;
+ }
+ final DhcpServingParamsParcel params;
+ params = new DhcpServingParamsParcelExt()
+ .setDefaultRouters(addr)
+ .setDhcpLeaseTimeSecs(DHCP_LEASE_TIME_SECS)
+ .setDnsServers(addr)
+ .setServerAddr(new LinkAddress(addr, prefixLen))
+ .setMetered(true);
+ // TODO: also advertise link MTU
+
+ mDhcpServerStartIndex++;
+ mDeps.makeDhcpServer(
+ mIfaceName, params, new DhcpServerCallbacksImpl(mDhcpServerStartIndex));
+ return true;
+ }
+
+ private void stopDhcp() {
+ // Make all previous start requests obsolete so servers are not started later
+ mDhcpServerStartIndex++;
+
+ if (mDhcpServer != null) {
+ try {
+ mDhcpServer.stop(new OnHandlerStatusCallback() {
+ @Override
+ public void callback(int statusCode) {
+ if (statusCode != STATUS_SUCCESS) {
+ mLog.e("Error stopping DHCP server: " + statusCode);
+ mLastError = ConnectivityManager.TETHER_ERROR_DHCPSERVER_ERROR;
+ // Not much more we can do here
+ }
+ }
+ });
+ mDhcpServer = null;
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ private boolean configureDhcp(boolean enable, Inet4Address addr, int prefixLen) {
+ if (enable) {
+ return startDhcp(addr, prefixLen);
+ } else {
+ stopDhcp();
+ return true;
+ }
+ }
+
+ private void stopIPv4() {
+ configureIPv4(false);
+ // NOTE: All of configureIPv4() will be refactored out of existence
+ // into calls to InterfaceController, shared with startIPv4().
+ mInterfaceCtrl.clearIPv4Address();
+ }
+
+ // TODO: Refactor this in terms of calls to InterfaceController.
+ private boolean configureIPv4(boolean enabled) {
+ if (VDBG) Log.d(TAG, "configureIPv4(" + enabled + ")");
+
+ // TODO: Replace this hard-coded information with dynamically selected
+ // config passed down to us by a higher layer IP-coordinating element.
+ String ipAsString = null;
+ int prefixLen = 0;
+ if (mInterfaceType == ConnectivityManager.TETHERING_USB) {
+ ipAsString = USB_NEAR_IFACE_ADDR;
+ prefixLen = USB_PREFIX_LENGTH;
+ } else if (mInterfaceType == ConnectivityManager.TETHERING_WIFI) {
+ ipAsString = getRandomWifiIPv4Address();
+ prefixLen = WIFI_HOST_IFACE_PREFIX_LENGTH;
+ } else if (mInterfaceType == ConnectivityManager.TETHERING_WIFI_P2P) {
+ ipAsString = WIFI_P2P_IFACE_ADDR;
+ prefixLen = WIFI_P2P_IFACE_PREFIX_LENGTH;
+ } else {
+ // BT configures the interface elsewhere: only start DHCP.
+ final Inet4Address srvAddr = (Inet4Address) parseNumericAddress(BLUETOOTH_IFACE_ADDR);
+ return configureDhcp(enabled, srvAddr, BLUETOOTH_DHCP_PREFIX_LENGTH);
+ }
+
+ final LinkAddress linkAddr;
+ try {
+ final InterfaceConfiguration ifcg = mNMService.getInterfaceConfig(mIfaceName);
+ if (ifcg == null) {
+ mLog.e("Received null interface config");
+ return false;
+ }
+
+ InetAddress addr = parseNumericAddress(ipAsString);
+ linkAddr = new LinkAddress(addr, prefixLen);
+ ifcg.setLinkAddress(linkAddr);
+ if (mInterfaceType == ConnectivityManager.TETHERING_WIFI) {
+ // The WiFi stack has ownership of the interface up/down state.
+ // It is unclear whether the Bluetooth or USB stacks will manage their own
+ // state.
+ ifcg.ignoreInterfaceUpDownStatus();
+ } else {
+ if (enabled) {
+ ifcg.setInterfaceUp();
+ } else {
+ ifcg.setInterfaceDown();
+ }
+ }
+ ifcg.clearFlag("running");
+
+ // TODO: this may throw if the interface is already gone. Do proper handling and
+ // simplify the DHCP server start/stop.
+ mNMService.setInterfaceConfig(mIfaceName, ifcg);
+
+ if (!configureDhcp(enabled, (Inet4Address) addr, prefixLen)) {
+ return false;
+ }
+ } catch (Exception e) {
+ mLog.e("Error configuring interface " + e);
+ if (!enabled) {
+ try {
+ // Calling stopDhcp several times is fine
+ stopDhcp();
+ } catch (Exception dhcpError) {
+ mLog.e("Error stopping DHCP", dhcpError);
+ }
+ }
+ return false;
+ }
+
+ // Directly-connected route.
+ final RouteInfo route = new RouteInfo(linkAddr);
+ if (enabled) {
+ mLinkProperties.addLinkAddress(linkAddr);
+ mLinkProperties.addRoute(route);
+ } else {
+ mLinkProperties.removeLinkAddress(linkAddr);
+ mLinkProperties.removeRoute(route);
+ }
+ return true;
+ }
+
+ private String getRandomWifiIPv4Address() {
+ try {
+ byte[] bytes = parseNumericAddress(WIFI_HOST_IFACE_ADDR).getAddress();
+ bytes[3] = getRandomSanitizedByte(DOUG_ADAMS, asByte(0), asByte(1), FF);
+ return InetAddress.getByAddress(bytes).getHostAddress();
+ } catch (Exception e) {
+ return WIFI_HOST_IFACE_ADDR;
+ }
+ }
+
+ private boolean startIPv6() {
+ mInterfaceParams = mDeps.getInterfaceParams(mIfaceName);
+ if (mInterfaceParams == null) {
+ mLog.e("Failed to find InterfaceParams");
+ stopIPv6();
+ return false;
+ }
+
+ mRaDaemon = mDeps.getRouterAdvertisementDaemon(mInterfaceParams);
+ if (!mRaDaemon.start()) {
+ stopIPv6();
+ return false;
+ }
+
+ return true;
+ }
+
+ private void stopIPv6() {
+ mInterfaceParams = null;
+ setRaParams(null);
+
+ if (mRaDaemon != null) {
+ mRaDaemon.stop();
+ mRaDaemon = null;
+ }
+ }
+
+ // IPv6TetheringCoordinator sends updates with carefully curated IPv6-only
+ // LinkProperties. These have extraneous data filtered out and only the
+ // necessary prefixes included (per its prefix distribution policy).
+ //
+ // TODO: Evaluate using a data structure than is more directly suited to
+ // communicating only the relevant information.
+ private void updateUpstreamIPv6LinkProperties(LinkProperties v6only) {
+ if (mRaDaemon == null) return;
+
+ // Avoid unnecessary work on spurious updates.
+ if (Objects.equals(mLastIPv6LinkProperties, v6only)) {
+ return;
+ }
+
+ RaParams params = null;
+
+ if (v6only != null) {
+ params = new RaParams();
+ params.mtu = v6only.getMtu();
+ params.hasDefaultRoute = v6only.hasIpv6DefaultRoute();
+
+ if (params.hasDefaultRoute) params.hopLimit = getHopLimit(v6only.getInterfaceName());
+
+ for (LinkAddress linkAddr : v6only.getLinkAddresses()) {
+ if (linkAddr.getPrefixLength() != RFC7421_PREFIX_LENGTH) continue;
+
+ final IpPrefix prefix = new IpPrefix(
+ linkAddr.getAddress(), linkAddr.getPrefixLength());
+ params.prefixes.add(prefix);
+
+ final Inet6Address dnsServer = getLocalDnsIpFor(prefix);
+ if (dnsServer != null) {
+ params.dnses.add(dnsServer);
+ }
+ }
+ }
+ // If v6only is null, we pass in null to setRaParams(), which handles
+ // deprecation of any existing RA data.
+
+ setRaParams(params);
+ mLastIPv6LinkProperties = v6only;
+ }
+
+ private void configureLocalIPv6Routes(
+ HashSet<IpPrefix> deprecatedPrefixes, HashSet<IpPrefix> newPrefixes) {
+ // [1] Remove the routes that are deprecated.
+ if (!deprecatedPrefixes.isEmpty()) {
+ final ArrayList<RouteInfo> toBeRemoved =
+ getLocalRoutesFor(mIfaceName, deprecatedPrefixes);
+ try {
+ final int removalFailures = mNMService.removeRoutesFromLocalNetwork(toBeRemoved);
+ if (removalFailures > 0) {
+ mLog.e(String.format("Failed to remove %d IPv6 routes from local table.",
+ removalFailures));
+ }
+ } catch (RemoteException e) {
+ mLog.e("Failed to remove IPv6 routes from local table: " + e);
+ }
+
+ for (RouteInfo route : toBeRemoved) mLinkProperties.removeRoute(route);
+ }
+
+ // [2] Add only the routes that have not previously been added.
+ if (newPrefixes != null && !newPrefixes.isEmpty()) {
+ HashSet<IpPrefix> addedPrefixes = (HashSet) newPrefixes.clone();
+ if (mLastRaParams != null) {
+ addedPrefixes.removeAll(mLastRaParams.prefixes);
+ }
+
+ if (!addedPrefixes.isEmpty()) {
+ final ArrayList<RouteInfo> toBeAdded =
+ getLocalRoutesFor(mIfaceName, addedPrefixes);
+ try {
+ // It's safe to call addInterfaceToLocalNetwork() even if
+ // the interface is already in the local_network. Note also
+ // that adding routes that already exist does not cause an
+ // error (EEXIST is silently ignored).
+ mNMService.addInterfaceToLocalNetwork(mIfaceName, toBeAdded);
+ } catch (Exception e) {
+ mLog.e("Failed to add IPv6 routes to local table: " + e);
+ }
+
+ for (RouteInfo route : toBeAdded) mLinkProperties.addRoute(route);
+ }
+ }
+ }
+
+ private void configureLocalIPv6Dns(
+ HashSet<Inet6Address> deprecatedDnses, HashSet<Inet6Address> newDnses) {
+ // TODO: Is this really necessary? Can we not fail earlier if INetd cannot be located?
+ if (mNetd == null) {
+ if (newDnses != null) newDnses.clear();
+ mLog.e("No netd service instance available; not setting local IPv6 addresses");
+ return;
+ }
+
+ // [1] Remove deprecated local DNS IP addresses.
+ if (!deprecatedDnses.isEmpty()) {
+ for (Inet6Address dns : deprecatedDnses) {
+ if (!mInterfaceCtrl.removeAddress(dns, RFC7421_PREFIX_LENGTH)) {
+ mLog.e("Failed to remove local dns IP " + dns);
+ }
+
+ mLinkProperties.removeLinkAddress(new LinkAddress(dns, RFC7421_PREFIX_LENGTH));
+ }
+ }
+
+ // [2] Add only the local DNS IP addresses that have not previously been added.
+ if (newDnses != null && !newDnses.isEmpty()) {
+ final HashSet<Inet6Address> addedDnses = (HashSet) newDnses.clone();
+ if (mLastRaParams != null) {
+ addedDnses.removeAll(mLastRaParams.dnses);
+ }
+
+ for (Inet6Address dns : addedDnses) {
+ if (!mInterfaceCtrl.addAddress(dns, RFC7421_PREFIX_LENGTH)) {
+ mLog.e("Failed to add local dns IP " + dns);
+ newDnses.remove(dns);
+ }
+
+ mLinkProperties.addLinkAddress(new LinkAddress(dns, RFC7421_PREFIX_LENGTH));
+ }
+ }
+
+ try {
+ mNetd.tetherApplyDnsInterfaces();
+ } catch (ServiceSpecificException | RemoteException e) {
+ mLog.e("Failed to update local DNS caching server");
+ if (newDnses != null) newDnses.clear();
+ }
+ }
+
+ private byte getHopLimit(String upstreamIface) {
+ try {
+ int upstreamHopLimit = Integer.parseUnsignedInt(
+ mNetd.getProcSysNet(INetd.IPV6, INetd.CONF, upstreamIface, "hop_limit"));
+ // Add one hop to account for this forwarding device
+ upstreamHopLimit++;
+ // Cap the hop limit to 255.
+ return (byte) Integer.min(upstreamHopLimit, 255);
+ } catch (Exception e) {
+ mLog.e("Failed to find upstream interface hop limit", e);
+ }
+ return RaParams.DEFAULT_HOPLIMIT;
+ }
+
+ private void setRaParams(RaParams newParams) {
+ if (mRaDaemon != null) {
+ final RaParams deprecatedParams =
+ RaParams.getDeprecatedRaParams(mLastRaParams, newParams);
+
+ configureLocalIPv6Routes(deprecatedParams.prefixes,
+ (newParams != null) ? newParams.prefixes : null);
+
+ configureLocalIPv6Dns(deprecatedParams.dnses,
+ (newParams != null) ? newParams.dnses : null);
+
+ mRaDaemon.buildNewRa(deprecatedParams, newParams);
+ }
+
+ mLastRaParams = newParams;
+ }
+
+ private void logMessage(State state, int what) {
+ mLog.log(state.getName() + " got " + sMagicDecoderRing.get(what, Integer.toString(what)));
+ }
+
+ private void sendInterfaceState(int newInterfaceState) {
+ mServingMode = newInterfaceState;
+ mCallback.updateInterfaceState(this, newInterfaceState, mLastError);
+ sendLinkProperties();
+ }
+
+ private void sendLinkProperties() {
+ mCallback.updateLinkProperties(this, new LinkProperties(mLinkProperties));
+ }
+
+ private void resetLinkProperties() {
+ mLinkProperties.clear();
+ mLinkProperties.setInterfaceName(mIfaceName);
+ }
+
+ class InitialState extends State {
+ @Override
+ public void enter() {
+ sendInterfaceState(STATE_AVAILABLE);
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ logMessage(this, message.what);
+ switch (message.what) {
+ case CMD_TETHER_REQUESTED:
+ mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
+ switch (message.arg1) {
+ case STATE_LOCAL_ONLY:
+ transitionTo(mLocalHotspotState);
+ break;
+ case STATE_TETHERED:
+ transitionTo(mTetheredState);
+ break;
+ default:
+ mLog.e("Invalid tethering interface serving state specified.");
+ }
+ break;
+ case CMD_INTERFACE_DOWN:
+ transitionTo(mUnavailableState);
+ break;
+ case CMD_IPV6_TETHER_UPDATE:
+ updateUpstreamIPv6LinkProperties((LinkProperties) message.obj);
+ break;
+ default:
+ return NOT_HANDLED;
+ }
+ return HANDLED;
+ }
+ }
+
+ class BaseServingState extends State {
+ @Override
+ public void enter() {
+ if (!startIPv4()) {
+ mLastError = ConnectivityManager.TETHER_ERROR_IFACE_CFG_ERROR;
+ return;
+ }
+
+ try {
+ mNMService.tetherInterface(mIfaceName);
+ } catch (Exception e) {
+ mLog.e("Error Tethering: " + e);
+ mLastError = ConnectivityManager.TETHER_ERROR_TETHER_IFACE_ERROR;
+ return;
+ }
+
+ if (!startIPv6()) {
+ mLog.e("Failed to startIPv6");
+ // TODO: Make this a fatal error once Bluetooth IPv6 is sorted.
+ return;
+ }
+ }
+
+ @Override
+ public void exit() {
+ // Note that at this point, we're leaving the tethered state. We can fail any
+ // of these operations, but it doesn't really change that we have to try them
+ // all in sequence.
+ stopIPv6();
+
+ try {
+ mNMService.untetherInterface(mIfaceName);
+ } catch (Exception e) {
+ mLastError = ConnectivityManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
+ mLog.e("Failed to untether interface: " + e);
+ }
+
+ stopIPv4();
+
+ resetLinkProperties();
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ logMessage(this, message.what);
+ switch (message.what) {
+ case CMD_TETHER_UNREQUESTED:
+ transitionTo(mInitialState);
+ if (DBG) Log.d(TAG, "Untethered (unrequested)" + mIfaceName);
+ break;
+ case CMD_INTERFACE_DOWN:
+ transitionTo(mUnavailableState);
+ if (DBG) Log.d(TAG, "Untethered (ifdown)" + mIfaceName);
+ break;
+ case CMD_IPV6_TETHER_UPDATE:
+ updateUpstreamIPv6LinkProperties((LinkProperties) message.obj);
+ sendLinkProperties();
+ break;
+ case CMD_IP_FORWARDING_ENABLE_ERROR:
+ case CMD_IP_FORWARDING_DISABLE_ERROR:
+ case CMD_START_TETHERING_ERROR:
+ case CMD_STOP_TETHERING_ERROR:
+ case CMD_SET_DNS_FORWARDERS_ERROR:
+ mLastError = ConnectivityManager.TETHER_ERROR_MASTER_ERROR;
+ transitionTo(mInitialState);
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+ }
+
+ // Handling errors in BaseServingState.enter() by transitioning is
+ // problematic because transitioning during a multi-state jump yields
+ // a Log.wtf(). Ultimately, there should be only one ServingState,
+ // and forwarding and NAT rules should be handled by a coordinating
+ // functional element outside of IpServer.
+ class LocalHotspotState extends BaseServingState {
+ @Override
+ public void enter() {
+ super.enter();
+ if (mLastError != ConnectivityManager.TETHER_ERROR_NO_ERROR) {
+ transitionTo(mInitialState);
+ }
+
+ if (DBG) Log.d(TAG, "Local hotspot " + mIfaceName);
+ sendInterfaceState(STATE_LOCAL_ONLY);
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ if (super.processMessage(message)) return true;
+
+ logMessage(this, message.what);
+ switch (message.what) {
+ case CMD_TETHER_REQUESTED:
+ mLog.e("CMD_TETHER_REQUESTED while in local-only hotspot mode.");
+ break;
+ case CMD_TETHER_CONNECTION_CHANGED:
+ // Ignored in local hotspot state.
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+ }
+
+ // Handling errors in BaseServingState.enter() by transitioning is
+ // problematic because transitioning during a multi-state jump yields
+ // a Log.wtf(). Ultimately, there should be only one ServingState,
+ // and forwarding and NAT rules should be handled by a coordinating
+ // functional element outside of IpServer.
+ class TetheredState extends BaseServingState {
+ @Override
+ public void enter() {
+ super.enter();
+ if (mLastError != ConnectivityManager.TETHER_ERROR_NO_ERROR) {
+ transitionTo(mInitialState);
+ }
+
+ if (DBG) Log.d(TAG, "Tethered " + mIfaceName);
+ sendInterfaceState(STATE_TETHERED);
+ }
+
+ @Override
+ public void exit() {
+ cleanupUpstream();
+ super.exit();
+ }
+
+ private void cleanupUpstream() {
+ if (mUpstreamIfaceSet == null) return;
+
+ for (String ifname : mUpstreamIfaceSet.ifnames) cleanupUpstreamInterface(ifname);
+ mUpstreamIfaceSet = null;
+ }
+
+ private void cleanupUpstreamInterface(String upstreamIface) {
+ // Note that we don't care about errors here.
+ // Sometimes interfaces are gone before we get
+ // to remove their rules, which generates errors.
+ // Just do the best we can.
+ try {
+ // About to tear down NAT; gather remaining statistics.
+ mStatsService.forceUpdate();
+ } catch (Exception e) {
+ if (VDBG) Log.e(TAG, "Exception in forceUpdate: " + e.toString());
+ }
+ try {
+ mNMService.stopInterfaceForwarding(mIfaceName, upstreamIface);
+ } catch (Exception e) {
+ if (VDBG) Log.e(TAG, "Exception in removeInterfaceForward: " + e.toString());
+ }
+ try {
+ mNMService.disableNat(mIfaceName, upstreamIface);
+ } catch (Exception e) {
+ if (VDBG) Log.e(TAG, "Exception in disableNat: " + e.toString());
+ }
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ if (super.processMessage(message)) return true;
+
+ logMessage(this, message.what);
+ switch (message.what) {
+ case CMD_TETHER_REQUESTED:
+ mLog.e("CMD_TETHER_REQUESTED while already tethering.");
+ break;
+ case CMD_TETHER_CONNECTION_CHANGED:
+ final InterfaceSet newUpstreamIfaceSet = (InterfaceSet) message.obj;
+ if (noChangeInUpstreamIfaceSet(newUpstreamIfaceSet)) {
+ if (VDBG) Log.d(TAG, "Connection changed noop - dropping");
+ break;
+ }
+
+ if (newUpstreamIfaceSet == null) {
+ cleanupUpstream();
+ break;
+ }
+
+ for (String removed : upstreamInterfacesRemoved(newUpstreamIfaceSet)) {
+ cleanupUpstreamInterface(removed);
+ }
+
+ final Set<String> added = upstreamInterfacesAdd(newUpstreamIfaceSet);
+ // This makes the call to cleanupUpstream() in the error
+ // path for any interface neatly cleanup all the interfaces.
+ mUpstreamIfaceSet = newUpstreamIfaceSet;
+
+ for (String ifname : added) {
+ try {
+ mNMService.enableNat(mIfaceName, ifname);
+ mNMService.startInterfaceForwarding(mIfaceName, ifname);
+ } catch (Exception e) {
+ mLog.e("Exception enabling NAT: " + e);
+ cleanupUpstream();
+ mLastError = ConnectivityManager.TETHER_ERROR_ENABLE_NAT_ERROR;
+ transitionTo(mInitialState);
+ return true;
+ }
+ }
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ private boolean noChangeInUpstreamIfaceSet(InterfaceSet newIfaces) {
+ if (mUpstreamIfaceSet == null && newIfaces == null) return true;
+ if (mUpstreamIfaceSet != null && newIfaces != null) {
+ return mUpstreamIfaceSet.equals(newIfaces);
+ }
+ return false;
+ }
+
+ private Set<String> upstreamInterfacesRemoved(InterfaceSet newIfaces) {
+ if (mUpstreamIfaceSet == null) return new HashSet<>();
+
+ final HashSet<String> removed = new HashSet<>(mUpstreamIfaceSet.ifnames);
+ removed.removeAll(newIfaces.ifnames);
+ return removed;
+ }
+
+ private Set<String> upstreamInterfacesAdd(InterfaceSet newIfaces) {
+ final HashSet<String> added = new HashSet<>(newIfaces.ifnames);
+ if (mUpstreamIfaceSet != null) added.removeAll(mUpstreamIfaceSet.ifnames);
+ return added;
+ }
+ }
+
+ /**
+ * This state is terminal for the per interface state machine. At this
+ * point, the master state machine should have removed this interface
+ * specific state machine from its list of possible recipients of
+ * tethering requests. The state machine itself will hang around until
+ * the garbage collector finds it.
+ */
+ class UnavailableState extends State {
+ @Override
+ public void enter() {
+ mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
+ sendInterfaceState(STATE_UNAVAILABLE);
+ }
+ }
+
+ // Accumulate routes representing "prefixes to be assigned to the local
+ // interface", for subsequent modification of local_network routing.
+ private static ArrayList<RouteInfo> getLocalRoutesFor(
+ String ifname, HashSet<IpPrefix> prefixes) {
+ final ArrayList<RouteInfo> localRoutes = new ArrayList<RouteInfo>();
+ for (IpPrefix ipp : prefixes) {
+ localRoutes.add(new RouteInfo(ipp, null, ifname));
+ }
+ return localRoutes;
+ }
+
+ // Given a prefix like 2001:db8::/64 return an address like 2001:db8::1.
+ private static Inet6Address getLocalDnsIpFor(IpPrefix localPrefix) {
+ final byte[] dnsBytes = localPrefix.getRawAddress();
+ dnsBytes[dnsBytes.length - 1] = getRandomSanitizedByte(DOUG_ADAMS, asByte(0), asByte(1));
+ try {
+ return Inet6Address.getByAddress(null, dnsBytes, 0);
+ } catch (UnknownHostException e) {
+ Slog.wtf(TAG, "Failed to construct Inet6Address from: " + localPrefix);
+ return null;
+ }
+ }
+
+ private static byte getRandomSanitizedByte(byte dflt, byte... excluded) {
+ final byte random = (byte) (new Random()).nextInt();
+ for (int value : excluded) {
+ if (random == value) return dflt;
+ }
+ return random;
+ }
+}
diff --git a/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
new file mode 100644
index 0000000..4147413
--- /dev/null
+++ b/Tethering/src/android/net/ip/RouterAdvertisementDaemon.java
@@ -0,0 +1,761 @@
+/*
+ * Copyright (C) 2016 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 android.net.ip;
+
+import static android.net.util.NetworkConstants.IPV6_MIN_MTU;
+import static android.net.util.NetworkConstants.RFC7421_PREFIX_LENGTH;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.SOCK_RAW;
+import static android.system.OsConstants.SOL_SOCKET;
+import static android.system.OsConstants.SO_BINDTODEVICE;
+import static android.system.OsConstants.SO_SNDTIMEO;
+
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.NetworkUtils;
+import android.net.TrafficStats;
+import android.net.util.InterfaceParams;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructTimeval;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.TrafficStatsConstants;
+
+import libcore.io.IoBridge;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+
+/**
+ * Basic IPv6 Router Advertisement Daemon.
+ *
+ * TODO:
+ *
+ * - Rewrite using Handler (and friends) so that AlarmManager can deliver
+ * "kick" messages when it's time to send a multicast RA.
+ *
+ * @hide
+ */
+public class RouterAdvertisementDaemon {
+ private static final String TAG = RouterAdvertisementDaemon.class.getSimpleName();
+ private static final byte ICMPV6_ND_ROUTER_SOLICIT = asByte(133);
+ private static final byte ICMPV6_ND_ROUTER_ADVERT = asByte(134);
+ private static final int MIN_RA_HEADER_SIZE = 16;
+
+ // Summary of various timers and lifetimes.
+ private static final int MIN_RTR_ADV_INTERVAL_SEC = 300;
+ private static final int MAX_RTR_ADV_INTERVAL_SEC = 600;
+ // In general, router, prefix, and DNS lifetimes are all advised to be
+ // greater than or equal to 3 * MAX_RTR_ADV_INTERVAL. Here, we double
+ // that to allow for multicast packet loss.
+ //
+ // This MAX_RTR_ADV_INTERVAL_SEC and DEFAULT_LIFETIME are also consistent
+ // with the https://tools.ietf.org/html/rfc7772#section-4 discussion of
+ // "approximately 7 RAs per hour".
+ private static final int DEFAULT_LIFETIME = 6 * MAX_RTR_ADV_INTERVAL_SEC;
+ // From https://tools.ietf.org/html/rfc4861#section-10 .
+ private static final int MIN_DELAY_BETWEEN_RAS_SEC = 3;
+ // Both initial and final RAs, but also for changes in RA contents.
+ // From https://tools.ietf.org/html/rfc4861#section-10 .
+ private static final int MAX_URGENT_RTR_ADVERTISEMENTS = 5;
+
+ private static final int DAY_IN_SECONDS = 86_400;
+
+ private static final byte[] ALL_NODES = new byte[] {
+ (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
+ };
+
+ private final InterfaceParams mInterface;
+ private final InetSocketAddress mAllNodes;
+
+ // This lock is to protect the RA from being updated while being
+ // transmitted on another thread (multicast or unicast).
+ //
+ // TODO: This should be handled with a more RCU-like approach.
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
+ private final byte[] mRA = new byte[IPV6_MIN_MTU];
+ @GuardedBy("mLock")
+ private int mRaLength;
+ @GuardedBy("mLock")
+ private final DeprecatedInfoTracker mDeprecatedInfoTracker;
+ @GuardedBy("mLock")
+ private RaParams mRaParams;
+
+ private volatile FileDescriptor mSocket;
+ private volatile MulticastTransmitter mMulticastTransmitter;
+ private volatile UnicastResponder mUnicastResponder;
+
+ /** Encapsulate the RA parameters for RouterAdvertisementDaemon.*/
+ public static class RaParams {
+ // Tethered traffic will have the hop limit properly decremented.
+ // Consequently, set the hoplimit greater by one than the upstream
+ // unicast hop limit.
+ //
+ // TODO: Dynamically pass down the IPV6_UNICAST_HOPS value from the
+ // upstream interface for more correct behaviour.
+ static final byte DEFAULT_HOPLIMIT = 65;
+
+ public boolean hasDefaultRoute;
+ public byte hopLimit;
+ public int mtu;
+ public HashSet<IpPrefix> prefixes;
+ public HashSet<Inet6Address> dnses;
+
+ public RaParams() {
+ hasDefaultRoute = false;
+ hopLimit = DEFAULT_HOPLIMIT;
+ mtu = IPV6_MIN_MTU;
+ prefixes = new HashSet<IpPrefix>();
+ dnses = new HashSet<Inet6Address>();
+ }
+
+ public RaParams(RaParams other) {
+ hasDefaultRoute = other.hasDefaultRoute;
+ hopLimit = other.hopLimit;
+ mtu = other.mtu;
+ prefixes = (HashSet) other.prefixes.clone();
+ dnses = (HashSet) other.dnses.clone();
+ }
+
+ /**
+ * Returns the subset of RA parameters that become deprecated when
+ * moving from announcing oldRa to announcing newRa.
+ *
+ * Currently only tracks differences in |prefixes| and |dnses|.
+ */
+ public static RaParams getDeprecatedRaParams(RaParams oldRa, RaParams newRa) {
+ RaParams newlyDeprecated = new RaParams();
+
+ if (oldRa != null) {
+ for (IpPrefix ipp : oldRa.prefixes) {
+ if (newRa == null || !newRa.prefixes.contains(ipp)) {
+ newlyDeprecated.prefixes.add(ipp);
+ }
+ }
+
+ for (Inet6Address dns : oldRa.dnses) {
+ if (newRa == null || !newRa.dnses.contains(dns)) {
+ newlyDeprecated.dnses.add(dns);
+ }
+ }
+ }
+
+ return newlyDeprecated;
+ }
+ }
+
+ private static class DeprecatedInfoTracker {
+ private final HashMap<IpPrefix, Integer> mPrefixes = new HashMap<>();
+ private final HashMap<Inet6Address, Integer> mDnses = new HashMap<>();
+
+ Set<IpPrefix> getPrefixes() {
+ return mPrefixes.keySet();
+ }
+
+ void putPrefixes(Set<IpPrefix> prefixes) {
+ for (IpPrefix ipp : prefixes) {
+ mPrefixes.put(ipp, MAX_URGENT_RTR_ADVERTISEMENTS);
+ }
+ }
+
+ void removePrefixes(Set<IpPrefix> prefixes) {
+ for (IpPrefix ipp : prefixes) {
+ mPrefixes.remove(ipp);
+ }
+ }
+
+ Set<Inet6Address> getDnses() {
+ return mDnses.keySet();
+ }
+
+ void putDnses(Set<Inet6Address> dnses) {
+ for (Inet6Address dns : dnses) {
+ mDnses.put(dns, MAX_URGENT_RTR_ADVERTISEMENTS);
+ }
+ }
+
+ void removeDnses(Set<Inet6Address> dnses) {
+ for (Inet6Address dns : dnses) {
+ mDnses.remove(dns);
+ }
+ }
+
+ boolean isEmpty() {
+ return mPrefixes.isEmpty() && mDnses.isEmpty();
+ }
+
+ private boolean decrementCounters() {
+ boolean removed = decrementCounter(mPrefixes);
+ removed |= decrementCounter(mDnses);
+ return removed;
+ }
+
+ private <T> boolean decrementCounter(HashMap<T, Integer> map) {
+ boolean removed = false;
+
+ for (Iterator<Map.Entry<T, Integer>> it = map.entrySet().iterator();
+ it.hasNext();) {
+ Map.Entry<T, Integer> kv = it.next();
+ if (kv.getValue() == 0) {
+ it.remove();
+ removed = true;
+ } else {
+ kv.setValue(kv.getValue() - 1);
+ }
+ }
+
+ return removed;
+ }
+ }
+
+
+ public RouterAdvertisementDaemon(InterfaceParams ifParams) {
+ mInterface = ifParams;
+ mAllNodes = new InetSocketAddress(getAllNodesForScopeId(mInterface.index), 0);
+ mDeprecatedInfoTracker = new DeprecatedInfoTracker();
+ }
+
+ /** Build new RA.*/
+ public void buildNewRa(RaParams deprecatedParams, RaParams newParams) {
+ synchronized (mLock) {
+ if (deprecatedParams != null) {
+ mDeprecatedInfoTracker.putPrefixes(deprecatedParams.prefixes);
+ mDeprecatedInfoTracker.putDnses(deprecatedParams.dnses);
+ }
+
+ if (newParams != null) {
+ // Process information that is no longer deprecated.
+ mDeprecatedInfoTracker.removePrefixes(newParams.prefixes);
+ mDeprecatedInfoTracker.removeDnses(newParams.dnses);
+ }
+
+ mRaParams = newParams;
+ assembleRaLocked();
+ }
+
+ maybeNotifyMulticastTransmitter();
+ }
+
+ /** Start router advertisement daemon. */
+ public boolean start() {
+ if (!createSocket()) {
+ return false;
+ }
+
+ mMulticastTransmitter = new MulticastTransmitter();
+ mMulticastTransmitter.start();
+
+ mUnicastResponder = new UnicastResponder();
+ mUnicastResponder.start();
+
+ return true;
+ }
+
+ /** Stop router advertisement daemon. */
+ public void stop() {
+ closeSocket();
+ // Wake up mMulticastTransmitter thread to interrupt a potential 1 day sleep before
+ // the thread's termination.
+ maybeNotifyMulticastTransmitter();
+ mMulticastTransmitter = null;
+ mUnicastResponder = null;
+ }
+
+ @GuardedBy("mLock")
+ private void assembleRaLocked() {
+ final ByteBuffer ra = ByteBuffer.wrap(mRA);
+ ra.order(ByteOrder.BIG_ENDIAN);
+
+ final boolean haveRaParams = (mRaParams != null);
+ boolean shouldSendRA = false;
+
+ try {
+ putHeader(ra, haveRaParams && mRaParams.hasDefaultRoute,
+ haveRaParams ? mRaParams.hopLimit : RaParams.DEFAULT_HOPLIMIT);
+ putSlla(ra, mInterface.macAddr.toByteArray());
+ mRaLength = ra.position();
+
+ // https://tools.ietf.org/html/rfc5175#section-4 says:
+ //
+ // "MUST NOT be added to a Router Advertisement message
+ // if no flags in the option are set."
+ //
+ // putExpandedFlagsOption(ra);
+
+ if (haveRaParams) {
+ putMtu(ra, mRaParams.mtu);
+ mRaLength = ra.position();
+
+ for (IpPrefix ipp : mRaParams.prefixes) {
+ putPio(ra, ipp, DEFAULT_LIFETIME, DEFAULT_LIFETIME);
+ mRaLength = ra.position();
+ shouldSendRA = true;
+ }
+
+ if (mRaParams.dnses.size() > 0) {
+ putRdnss(ra, mRaParams.dnses, DEFAULT_LIFETIME);
+ mRaLength = ra.position();
+ shouldSendRA = true;
+ }
+ }
+
+ for (IpPrefix ipp : mDeprecatedInfoTracker.getPrefixes()) {
+ putPio(ra, ipp, 0, 0);
+ mRaLength = ra.position();
+ shouldSendRA = true;
+ }
+
+ final Set<Inet6Address> deprecatedDnses = mDeprecatedInfoTracker.getDnses();
+ if (!deprecatedDnses.isEmpty()) {
+ putRdnss(ra, deprecatedDnses, 0);
+ mRaLength = ra.position();
+ shouldSendRA = true;
+ }
+ } catch (BufferOverflowException e) {
+ // The packet up to mRaLength is valid, since it has been updated
+ // progressively as the RA was built. Log an error, and continue
+ // on as best as possible.
+ Log.e(TAG, "Could not construct new RA: " + e);
+ }
+
+ // We have nothing worth announcing; indicate as much to maybeSendRA().
+ if (!shouldSendRA) {
+ mRaLength = 0;
+ }
+ }
+
+ private void maybeNotifyMulticastTransmitter() {
+ final MulticastTransmitter m = mMulticastTransmitter;
+ if (m != null) {
+ m.hup();
+ }
+ }
+
+ private static Inet6Address getAllNodesForScopeId(int scopeId) {
+ try {
+ return Inet6Address.getByAddress("ff02::1", ALL_NODES, scopeId);
+ } catch (UnknownHostException uhe) {
+ Log.wtf(TAG, "Failed to construct ff02::1 InetAddress: " + uhe);
+ return null;
+ }
+ }
+
+ private static byte asByte(int value) {
+ return (byte) value;
+ }
+ private static short asShort(int value) {
+ return (short) value;
+ }
+
+ private static void putHeader(ByteBuffer ra, boolean hasDefaultRoute, byte hopLimit) {
+ /**
+ Router Advertisement Message Format
+
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Type | Code | Checksum |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Cur Hop Limit |M|O|H|Prf|P|R|R| Router Lifetime |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Reachable Time |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Retrans Timer |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Options ...
+ +-+-+-+-+-+-+-+-+-+-+-+-
+ */
+ ra.put(ICMPV6_ND_ROUTER_ADVERT)
+ .put(asByte(0))
+ .putShort(asShort(0))
+ .put(hopLimit)
+ // RFC 4191 "high" preference, iff. advertising a default route.
+ .put(hasDefaultRoute ? asByte(0x08) : asByte(0))
+ .putShort(hasDefaultRoute ? asShort(DEFAULT_LIFETIME) : asShort(0))
+ .putInt(0)
+ .putInt(0);
+ }
+
+ private static void putSlla(ByteBuffer ra, byte[] slla) {
+ /**
+ Source/Target Link-layer Address
+
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Type | Length | Link-Layer Address ...
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+ if (slla == null || slla.length != 6) {
+ // Only IEEE 802.3 6-byte addresses are supported.
+ return;
+ }
+
+ final byte nd_option_slla = 1;
+ final byte slla_num_8octets = 1;
+ ra.put(nd_option_slla)
+ .put(slla_num_8octets)
+ .put(slla);
+ }
+
+ private static void putExpandedFlagsOption(ByteBuffer ra) {
+ /**
+ Router Advertisement Expanded Flags Option
+
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Type | Length | Bit fields available ..
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ ... for assignment |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+
+ final byte nd_option__efo = 26;
+ final byte efo_num_8octets = 1;
+
+ ra.put(nd_option__efo)
+ .put(efo_num_8octets)
+ .putShort(asShort(0))
+ .putInt(0);
+ }
+
+ private static void putMtu(ByteBuffer ra, int mtu) {
+ /**
+ MTU
+
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Type | Length | Reserved |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | MTU |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+ final byte nd_option_mtu = 5;
+ final byte mtu_num_8octs = 1;
+ ra.put(nd_option_mtu)
+ .put(mtu_num_8octs)
+ .putShort(asShort(0))
+ .putInt((mtu < IPV6_MIN_MTU) ? IPV6_MIN_MTU : mtu);
+ }
+
+ private static void putPio(ByteBuffer ra, IpPrefix ipp,
+ int validTime, int preferredTime) {
+ /**
+ Prefix Information
+
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Type | Length | Prefix Length |L|A| Reserved1 |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Valid Lifetime |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Preferred Lifetime |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Reserved2 |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | |
+ + +
+ | |
+ + Prefix +
+ | |
+ + +
+ | |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+ final int prefixLength = ipp.getPrefixLength();
+ if (prefixLength != 64) {
+ return;
+ }
+ final byte nd_option_pio = 3;
+ final byte pio_num_8octets = 4;
+
+ if (validTime < 0) validTime = 0;
+ if (preferredTime < 0) preferredTime = 0;
+ if (preferredTime > validTime) preferredTime = validTime;
+
+ final byte[] addr = ipp.getAddress().getAddress();
+ ra.put(nd_option_pio)
+ .put(pio_num_8octets)
+ .put(asByte(prefixLength))
+ .put(asByte(0xc0)) /* L & A set */
+ .putInt(validTime)
+ .putInt(preferredTime)
+ .putInt(0)
+ .put(addr);
+ }
+
+ private static void putRio(ByteBuffer ra, IpPrefix ipp) {
+ /**
+ Route Information Option
+
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Type | Length | Prefix Length |Resvd|Prf|Resvd|
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Route Lifetime |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Prefix (Variable Length) |
+ . .
+ . .
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+ final int prefixLength = ipp.getPrefixLength();
+ if (prefixLength > 64) {
+ return;
+ }
+ final byte nd_option_rio = 24;
+ final byte rio_num_8octets = asByte(
+ (prefixLength == 0) ? 1 : (prefixLength <= 8) ? 2 : 3);
+
+ final byte[] addr = ipp.getAddress().getAddress();
+ ra.put(nd_option_rio)
+ .put(rio_num_8octets)
+ .put(asByte(prefixLength))
+ .put(asByte(0x18))
+ .putInt(DEFAULT_LIFETIME);
+
+ // Rely upon an IpPrefix's address being properly zeroed.
+ if (prefixLength > 0) {
+ ra.put(addr, 0, (prefixLength <= 64) ? 8 : 16);
+ }
+ }
+
+ private static void putRdnss(ByteBuffer ra, Set<Inet6Address> dnses, int lifetime) {
+ /**
+ Recursive DNS Server (RDNSS) Option
+
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Type | Length | Reserved |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | Lifetime |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ | |
+ : Addresses of IPv6 Recursive DNS Servers :
+ | |
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+
+ final HashSet<Inet6Address> filteredDnses = new HashSet<>();
+ for (Inet6Address dns : dnses) {
+ if ((new LinkAddress(dns, RFC7421_PREFIX_LENGTH)).isGlobalPreferred()) {
+ filteredDnses.add(dns);
+ }
+ }
+ if (filteredDnses.isEmpty()) return;
+
+ final byte nd_option_rdnss = 25;
+ final byte rdnss_num_8octets = asByte(dnses.size() * 2 + 1);
+ ra.put(nd_option_rdnss)
+ .put(rdnss_num_8octets)
+ .putShort(asShort(0))
+ .putInt(lifetime);
+
+ for (Inet6Address dns : filteredDnses) {
+ // NOTE: If the full of list DNS servers doesn't fit in the packet,
+ // this code will cause a buffer overflow and the RA won't include
+ // this instance of the option at all.
+ //
+ // TODO: Consider looking at ra.remaining() to determine how many
+ // DNS servers will fit, and adding only those.
+ ra.put(dns.getAddress());
+ }
+ }
+
+ private boolean createSocket() {
+ final int send_timout_ms = 300;
+
+ final int oldTag = TrafficStats.getAndSetThreadStatsTag(
+ TrafficStatsConstants.TAG_SYSTEM_NEIGHBOR);
+ try {
+ mSocket = Os.socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
+ // Setting SNDTIMEO is purely for defensive purposes.
+ Os.setsockoptTimeval(
+ mSocket, SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(send_timout_ms));
+ Os.setsockoptIfreq(mSocket, SOL_SOCKET, SO_BINDTODEVICE, mInterface.name);
+ NetworkUtils.protectFromVpn(mSocket);
+ NetworkUtils.setupRaSocket(mSocket, mInterface.index);
+ } catch (ErrnoException | IOException e) {
+ Log.e(TAG, "Failed to create RA daemon socket: " + e);
+ return false;
+ } finally {
+ TrafficStats.setThreadStatsTag(oldTag);
+ }
+
+ return true;
+ }
+
+ private void closeSocket() {
+ if (mSocket != null) {
+ try {
+ IoBridge.closeAndSignalBlockedThreads(mSocket);
+ } catch (IOException ignored) { }
+ }
+ mSocket = null;
+ }
+
+ private boolean isSocketValid() {
+ final FileDescriptor s = mSocket;
+ return (s != null) && s.valid();
+ }
+
+ private boolean isSuitableDestination(InetSocketAddress dest) {
+ if (mAllNodes.equals(dest)) {
+ return true;
+ }
+
+ final InetAddress destip = dest.getAddress();
+ return (destip instanceof Inet6Address)
+ && destip.isLinkLocalAddress()
+ && (((Inet6Address) destip).getScopeId() == mInterface.index);
+ }
+
+ private void maybeSendRA(InetSocketAddress dest) {
+ if (dest == null || !isSuitableDestination(dest)) {
+ dest = mAllNodes;
+ }
+
+ try {
+ synchronized (mLock) {
+ if (mRaLength < MIN_RA_HEADER_SIZE) {
+ // No actual RA to send.
+ return;
+ }
+ Os.sendto(mSocket, mRA, 0, mRaLength, 0, dest);
+ }
+ Log.d(TAG, "RA sendto " + dest.getAddress().getHostAddress());
+ } catch (ErrnoException | SocketException e) {
+ if (isSocketValid()) {
+ Log.e(TAG, "sendto error: " + e);
+ }
+ }
+ }
+
+ private final class UnicastResponder extends Thread {
+ private final InetSocketAddress mSolicitor = new InetSocketAddress();
+ // The recycled buffer for receiving Router Solicitations from clients.
+ // If the RS is larger than IPV6_MIN_MTU the packets are truncated.
+ // This is fine since currently only byte 0 is examined anyway.
+ private final byte[] mSolicitation = new byte[IPV6_MIN_MTU];
+
+ @Override
+ public void run() {
+ while (isSocketValid()) {
+ try {
+ // Blocking receive.
+ final int rval = Os.recvfrom(
+ mSocket, mSolicitation, 0, mSolicitation.length, 0, mSolicitor);
+ // Do the least possible amount of validation.
+ if (rval < 1 || mSolicitation[0] != ICMPV6_ND_ROUTER_SOLICIT) {
+ continue;
+ }
+ } catch (ErrnoException | SocketException e) {
+ if (isSocketValid()) {
+ Log.e(TAG, "recvfrom error: " + e);
+ }
+ continue;
+ }
+
+ maybeSendRA(mSolicitor);
+ }
+ }
+ }
+
+ // TODO: Consider moving this to run on a provided Looper as a Handler,
+ // with WakeupMessage-style messages providing the timer driven input.
+ private final class MulticastTransmitter extends Thread {
+ private final Random mRandom = new Random();
+ private final AtomicInteger mUrgentAnnouncements = new AtomicInteger(0);
+
+ @Override
+ public void run() {
+ while (isSocketValid()) {
+ try {
+ Thread.sleep(getNextMulticastTransmitDelayMs());
+ } catch (InterruptedException ignored) {
+ // Stop sleeping, immediately send an RA, and continue.
+ }
+
+ maybeSendRA(mAllNodes);
+ synchronized (mLock) {
+ if (mDeprecatedInfoTracker.decrementCounters()) {
+ // At least one deprecated PIO has been removed;
+ // reassemble the RA.
+ assembleRaLocked();
+ }
+ }
+ }
+ }
+
+ public void hup() {
+ // Set to one fewer that the desired number, because as soon as
+ // the thread interrupt is processed we immediately send an RA
+ // and mUrgentAnnouncements is not examined until the subsequent
+ // sleep interval computation (i.e. this way we send 3 and not 4).
+ mUrgentAnnouncements.set(MAX_URGENT_RTR_ADVERTISEMENTS - 1);
+ interrupt();
+ }
+
+ private int getNextMulticastTransmitDelaySec() {
+ boolean deprecationInProgress = false;
+ synchronized (mLock) {
+ if (mRaLength < MIN_RA_HEADER_SIZE) {
+ // No actual RA to send; just sleep for 1 day.
+ return DAY_IN_SECONDS;
+ }
+ deprecationInProgress = !mDeprecatedInfoTracker.isEmpty();
+ }
+
+ final int urgentPending = mUrgentAnnouncements.getAndDecrement();
+ if ((urgentPending > 0) || deprecationInProgress) {
+ return MIN_DELAY_BETWEEN_RAS_SEC;
+ }
+
+ return MIN_RTR_ADV_INTERVAL_SEC + mRandom.nextInt(
+ MAX_RTR_ADV_INTERVAL_SEC - MIN_RTR_ADV_INTERVAL_SEC);
+ }
+
+ private long getNextMulticastTransmitDelayMs() {
+ return 1000 * (long) getNextMulticastTransmitDelaySec();
+ }
+ }
+}
diff --git a/Tethering/src/android/net/util/InterfaceSet.java b/Tethering/src/android/net/util/InterfaceSet.java
new file mode 100644
index 0000000..7589787
--- /dev/null
+++ b/Tethering/src/android/net/util/InterfaceSet.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2018 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 android.net.util;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.StringJoiner;
+
+
+/**
+ * @hide
+ */
+public class InterfaceSet {
+ public final Set<String> ifnames;
+
+ public InterfaceSet(String... names) {
+ final Set<String> nameSet = new HashSet<>();
+ for (String name : names) {
+ if (name != null) nameSet.add(name);
+ }
+ ifnames = Collections.unmodifiableSet(nameSet);
+ }
+
+ @Override
+ public String toString() {
+ final StringJoiner sj = new StringJoiner(",", "[", "]");
+ for (String ifname : ifnames) sj.add(ifname);
+ return sj.toString();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj != null
+ && obj instanceof InterfaceSet
+ && ifnames.equals(((InterfaceSet) obj).ifnames);
+ }
+}
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index 089bbd3..da62107 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -43,5 +43,8 @@
name: "tethering-tests-src",
srcs: [
"src/com/android/server/connectivity/tethering/TetheringConfigurationTest.java",
+ "src/android/net/dhcp/DhcpServingParamsParcelExtTest.java",
+ "src/android/net/ip/IpServerTest.java",
+ "src/android/net/util/InterfaceSetTest.java",
],
}
diff --git a/Tethering/tests/unit/src/android/net/dhcp/DhcpServingParamsParcelExtTest.java b/Tethering/tests/unit/src/android/net/dhcp/DhcpServingParamsParcelExtTest.java
new file mode 100644
index 0000000..e01ac7f
--- /dev/null
+++ b/Tethering/tests/unit/src/android/net/dhcp/DhcpServingParamsParcelExtTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2018 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 android.net.dhcp;
+
+import static android.net.InetAddresses.parseNumericAddress;
+
+import static com.google.android.collect.Sets.newHashSet;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.net.LinkAddress;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet4Address;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DhcpServingParamsParcelExtTest {
+ private static final Inet4Address TEST_ADDRESS = inet4Addr("192.168.0.123");
+ private static final int TEST_ADDRESS_PARCELED = 0xc0a8007b;
+ private static final int TEST_PREFIX_LENGTH = 17;
+ private static final int TEST_LEASE_TIME_SECS = 120;
+ private static final int TEST_MTU = 1000;
+ private static final Set<Inet4Address> TEST_ADDRESS_SET =
+ newHashSet(inet4Addr("192.168.1.123"), inet4Addr("192.168.1.124"));
+ private static final Set<Integer> TEST_ADDRESS_SET_PARCELED =
+ newHashSet(0xc0a8017b, 0xc0a8017c);
+
+ private DhcpServingParamsParcelExt mParcel;
+
+ @Before
+ public void setUp() {
+ mParcel = new DhcpServingParamsParcelExt();
+ }
+
+ @Test
+ public void testSetServerAddr() {
+ mParcel.setServerAddr(new LinkAddress(TEST_ADDRESS, TEST_PREFIX_LENGTH));
+
+ assertEquals(TEST_ADDRESS_PARCELED, mParcel.serverAddr);
+ assertEquals(TEST_PREFIX_LENGTH, mParcel.serverAddrPrefixLength);
+ }
+
+ @Test
+ public void testSetDefaultRouters() {
+ mParcel.setDefaultRouters(TEST_ADDRESS_SET);
+ assertEquals(TEST_ADDRESS_SET_PARCELED, asSet(mParcel.defaultRouters));
+ }
+
+ @Test
+ public void testSetDnsServers() {
+ mParcel.setDnsServers(TEST_ADDRESS_SET);
+ assertEquals(TEST_ADDRESS_SET_PARCELED, asSet(mParcel.dnsServers));
+ }
+
+ @Test
+ public void testSetExcludedAddrs() {
+ mParcel.setExcludedAddrs(TEST_ADDRESS_SET);
+ assertEquals(TEST_ADDRESS_SET_PARCELED, asSet(mParcel.excludedAddrs));
+ }
+
+ @Test
+ public void testSetDhcpLeaseTimeSecs() {
+ mParcel.setDhcpLeaseTimeSecs(TEST_LEASE_TIME_SECS);
+ assertEquals(TEST_LEASE_TIME_SECS, mParcel.dhcpLeaseTimeSecs);
+ }
+
+ @Test
+ public void testSetLinkMtu() {
+ mParcel.setLinkMtu(TEST_MTU);
+ assertEquals(TEST_MTU, mParcel.linkMtu);
+ }
+
+ @Test
+ public void testSetMetered() {
+ mParcel.setMetered(true);
+ assertTrue(mParcel.metered);
+ mParcel.setMetered(false);
+ assertFalse(mParcel.metered);
+ }
+
+ private static Inet4Address inet4Addr(String addr) {
+ return (Inet4Address) parseNumericAddress(addr);
+ }
+
+ private static Set<Integer> asSet(int[] ints) {
+ return IntStream.of(ints).boxed().collect(Collectors.toSet());
+ }
+}
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
new file mode 100644
index 0000000..4358cd6
--- /dev/null
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -0,0 +1,526 @@
+/*
+ * Copyright (C) 2016 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 android.net.ip;
+
+import static android.net.ConnectivityManager.TETHERING_BLUETOOTH;
+import static android.net.ConnectivityManager.TETHERING_USB;
+import static android.net.ConnectivityManager.TETHERING_WIFI;
+import static android.net.ConnectivityManager.TETHERING_WIFI_P2P;
+import static android.net.ConnectivityManager.TETHER_ERROR_ENABLE_NAT_ERROR;
+import static android.net.ConnectivityManager.TETHER_ERROR_NO_ERROR;
+import static android.net.ConnectivityManager.TETHER_ERROR_TETHER_IFACE_ERROR;
+import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
+import static android.net.ip.IpServer.STATE_AVAILABLE;
+import static android.net.ip.IpServer.STATE_LOCAL_ONLY;
+import static android.net.ip.IpServer.STATE_TETHERED;
+import static android.net.ip.IpServer.STATE_UNAVAILABLE;
+import static android.net.shared.Inet4AddressUtils.intToInet4AddressHTH;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.timeout;
+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.net.INetd;
+import android.net.INetworkStatsService;
+import android.net.InterfaceConfiguration;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.MacAddress;
+import android.net.RouteInfo;
+import android.net.dhcp.DhcpServingParamsParcel;
+import android.net.dhcp.IDhcpServer;
+import android.net.dhcp.IDhcpServerCallbacks;
+import android.net.util.InterfaceParams;
+import android.net.util.InterfaceSet;
+import android.net.util.SharedLog;
+import android.os.INetworkManagementService;
+import android.os.RemoteException;
+import android.os.test.TestLooper;
+import android.text.TextUtils;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.Inet4Address;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class IpServerTest {
+ private static final String IFACE_NAME = "testnet1";
+ private static final String UPSTREAM_IFACE = "upstream0";
+ private static final String UPSTREAM_IFACE2 = "upstream1";
+ private static final int DHCP_LEASE_TIME_SECS = 3600;
+
+ private static final InterfaceParams TEST_IFACE_PARAMS = new InterfaceParams(
+ IFACE_NAME, 42 /* index */, MacAddress.ALL_ZEROS_ADDRESS, 1500 /* defaultMtu */);
+
+ private static final int MAKE_DHCPSERVER_TIMEOUT_MS = 1000;
+
+ @Mock private INetworkManagementService mNMService;
+ @Mock private INetd mNetd;
+ @Mock private INetworkStatsService mStatsService;
+ @Mock private IpServer.Callback mCallback;
+ @Mock private InterfaceConfiguration mInterfaceConfiguration;
+ @Mock private SharedLog mSharedLog;
+ @Mock private IDhcpServer mDhcpServer;
+ @Mock private RouterAdvertisementDaemon mRaDaemon;
+ @Mock private IpServer.Dependencies mDependencies;
+
+ @Captor private ArgumentCaptor<DhcpServingParamsParcel> mDhcpParamsCaptor;
+
+ private final TestLooper mLooper = new TestLooper();
+ private final ArgumentCaptor<LinkProperties> mLinkPropertiesCaptor =
+ ArgumentCaptor.forClass(LinkProperties.class);
+ private IpServer mIpServer;
+
+ private void initStateMachine(int interfaceType) throws Exception {
+ initStateMachine(interfaceType, false /* usingLegacyDhcp */);
+ }
+
+ private void initStateMachine(int interfaceType, boolean usingLegacyDhcp) throws Exception {
+ doAnswer(inv -> {
+ final IDhcpServerCallbacks cb = inv.getArgument(2);
+ new Thread(() -> {
+ try {
+ cb.onDhcpServerCreated(STATUS_SUCCESS, mDhcpServer);
+ } catch (RemoteException e) {
+ fail(e.getMessage());
+ }
+ }).run();
+ return null;
+ }).when(mDependencies).makeDhcpServer(any(), mDhcpParamsCaptor.capture(), any());
+ when(mDependencies.getRouterAdvertisementDaemon(any())).thenReturn(mRaDaemon);
+ when(mDependencies.getInterfaceParams(IFACE_NAME)).thenReturn(TEST_IFACE_PARAMS);
+ when(mDependencies.getNetdService()).thenReturn(mNetd);
+
+ mIpServer = new IpServer(
+ IFACE_NAME, mLooper.getLooper(), interfaceType, mSharedLog,
+ mNMService, mStatsService, mCallback, usingLegacyDhcp, mDependencies);
+ mIpServer.start();
+ // Starting the state machine always puts us in a consistent state and notifies
+ // the rest of the world that we've changed from an unknown to available state.
+ mLooper.dispatchAll();
+ reset(mNMService, mStatsService, mCallback);
+ when(mNMService.getInterfaceConfig(IFACE_NAME)).thenReturn(mInterfaceConfiguration);
+
+ when(mRaDaemon.start()).thenReturn(true);
+ }
+
+ private void initTetheredStateMachine(int interfaceType, String upstreamIface)
+ throws Exception {
+ initTetheredStateMachine(interfaceType, upstreamIface, false);
+ }
+
+ private void initTetheredStateMachine(int interfaceType, String upstreamIface,
+ boolean usingLegacyDhcp) throws Exception {
+ initStateMachine(interfaceType, usingLegacyDhcp);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ if (upstreamIface != null) {
+ dispatchTetherConnectionChanged(upstreamIface);
+ }
+ reset(mNMService, mStatsService, mCallback);
+ when(mNMService.getInterfaceConfig(IFACE_NAME)).thenReturn(mInterfaceConfiguration);
+ }
+
+ @Before public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ when(mSharedLog.forSubComponent(anyString())).thenReturn(mSharedLog);
+ }
+
+ @Test
+ public void startsOutAvailable() {
+ mIpServer = new IpServer(IFACE_NAME, mLooper.getLooper(),
+ TETHERING_BLUETOOTH, mSharedLog, mNMService, mStatsService, mCallback,
+ false /* usingLegacyDhcp */, mDependencies);
+ mIpServer.start();
+ mLooper.dispatchAll();
+ verify(mCallback).updateInterfaceState(
+ mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
+ verify(mCallback).updateLinkProperties(eq(mIpServer), any(LinkProperties.class));
+ verifyNoMoreInteractions(mCallback, mNMService, mStatsService);
+ }
+
+ @Test
+ public void shouldDoNothingUntilRequested() throws Exception {
+ initStateMachine(TETHERING_BLUETOOTH);
+ final int [] noOp_commands = {
+ IpServer.CMD_TETHER_UNREQUESTED,
+ IpServer.CMD_IP_FORWARDING_ENABLE_ERROR,
+ IpServer.CMD_IP_FORWARDING_DISABLE_ERROR,
+ IpServer.CMD_START_TETHERING_ERROR,
+ IpServer.CMD_STOP_TETHERING_ERROR,
+ IpServer.CMD_SET_DNS_FORWARDERS_ERROR,
+ IpServer.CMD_TETHER_CONNECTION_CHANGED
+ };
+ for (int command : noOp_commands) {
+ // None of these commands should trigger us to request action from
+ // the rest of the system.
+ dispatchCommand(command);
+ verifyNoMoreInteractions(mNMService, mStatsService, mCallback);
+ }
+ }
+
+ @Test
+ public void handlesImmediateInterfaceDown() throws Exception {
+ initStateMachine(TETHERING_BLUETOOTH);
+
+ dispatchCommand(IpServer.CMD_INTERFACE_DOWN);
+ verify(mCallback).updateInterfaceState(
+ mIpServer, STATE_UNAVAILABLE, TETHER_ERROR_NO_ERROR);
+ verify(mCallback).updateLinkProperties(eq(mIpServer), any(LinkProperties.class));
+ verifyNoMoreInteractions(mNMService, mStatsService, mCallback);
+ }
+
+ @Test
+ public void canBeTethered() throws Exception {
+ initStateMachine(TETHERING_BLUETOOTH);
+
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ InOrder inOrder = inOrder(mCallback, mNMService);
+ inOrder.verify(mNMService).tetherInterface(IFACE_NAME);
+ inOrder.verify(mCallback).updateInterfaceState(
+ mIpServer, STATE_TETHERED, TETHER_ERROR_NO_ERROR);
+ inOrder.verify(mCallback).updateLinkProperties(
+ eq(mIpServer), any(LinkProperties.class));
+ verifyNoMoreInteractions(mNMService, mStatsService, mCallback);
+ }
+
+ @Test
+ public void canUnrequestTethering() throws Exception {
+ initTetheredStateMachine(TETHERING_BLUETOOTH, null);
+
+ dispatchCommand(IpServer.CMD_TETHER_UNREQUESTED);
+ InOrder inOrder = inOrder(mNMService, mNetd, mStatsService, mCallback);
+ inOrder.verify(mNMService).untetherInterface(IFACE_NAME);
+ inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
+ inOrder.verify(mCallback).updateInterfaceState(
+ mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
+ inOrder.verify(mCallback).updateLinkProperties(
+ eq(mIpServer), any(LinkProperties.class));
+ verifyNoMoreInteractions(mNMService, mStatsService, mCallback);
+ }
+
+ @Test
+ public void canBeTetheredAsUsb() throws Exception {
+ initStateMachine(TETHERING_USB);
+
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ InOrder inOrder = inOrder(mCallback, mNMService);
+ inOrder.verify(mNMService).getInterfaceConfig(IFACE_NAME);
+ inOrder.verify(mNMService).setInterfaceConfig(IFACE_NAME, mInterfaceConfiguration);
+ inOrder.verify(mNMService).tetherInterface(IFACE_NAME);
+ inOrder.verify(mCallback).updateInterfaceState(
+ mIpServer, STATE_TETHERED, TETHER_ERROR_NO_ERROR);
+ inOrder.verify(mCallback).updateLinkProperties(
+ eq(mIpServer), mLinkPropertiesCaptor.capture());
+ assertIPv4AddressAndDirectlyConnectedRoute(mLinkPropertiesCaptor.getValue());
+ verifyNoMoreInteractions(mNMService, mStatsService, mCallback);
+ }
+
+ @Test
+ public void canBeTetheredAsWifiP2p() throws Exception {
+ initStateMachine(TETHERING_WIFI_P2P);
+
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_LOCAL_ONLY);
+ InOrder inOrder = inOrder(mCallback, mNMService);
+ inOrder.verify(mNMService).getInterfaceConfig(IFACE_NAME);
+ inOrder.verify(mNMService).setInterfaceConfig(IFACE_NAME, mInterfaceConfiguration);
+ inOrder.verify(mNMService).tetherInterface(IFACE_NAME);
+ inOrder.verify(mCallback).updateInterfaceState(
+ mIpServer, STATE_LOCAL_ONLY, TETHER_ERROR_NO_ERROR);
+ inOrder.verify(mCallback).updateLinkProperties(
+ eq(mIpServer), mLinkPropertiesCaptor.capture());
+ assertIPv4AddressAndDirectlyConnectedRoute(mLinkPropertiesCaptor.getValue());
+ verifyNoMoreInteractions(mNMService, mStatsService, mCallback);
+ }
+
+ @Test
+ public void handlesFirstUpstreamChange() throws Exception {
+ initTetheredStateMachine(TETHERING_BLUETOOTH, null);
+
+ // Telling the state machine about its upstream interface triggers
+ // a little more configuration.
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE);
+ InOrder inOrder = inOrder(mNMService);
+ inOrder.verify(mNMService).enableNat(IFACE_NAME, UPSTREAM_IFACE);
+ inOrder.verify(mNMService).startInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE);
+ verifyNoMoreInteractions(mNMService, mStatsService, mCallback);
+ }
+
+ @Test
+ public void handlesChangingUpstream() throws Exception {
+ initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE);
+
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
+ InOrder inOrder = inOrder(mNMService, mStatsService);
+ inOrder.verify(mStatsService).forceUpdate();
+ inOrder.verify(mNMService).stopInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE);
+ inOrder.verify(mNMService).disableNat(IFACE_NAME, UPSTREAM_IFACE);
+ inOrder.verify(mNMService).enableNat(IFACE_NAME, UPSTREAM_IFACE2);
+ inOrder.verify(mNMService).startInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE2);
+ verifyNoMoreInteractions(mNMService, mStatsService, mCallback);
+ }
+
+ @Test
+ public void handlesChangingUpstreamNatFailure() throws Exception {
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
+
+ doThrow(RemoteException.class).when(mNMService).enableNat(IFACE_NAME, UPSTREAM_IFACE2);
+
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
+ InOrder inOrder = inOrder(mNMService, mStatsService);
+ inOrder.verify(mStatsService).forceUpdate();
+ inOrder.verify(mNMService).stopInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE);
+ inOrder.verify(mNMService).disableNat(IFACE_NAME, UPSTREAM_IFACE);
+ inOrder.verify(mNMService).enableNat(IFACE_NAME, UPSTREAM_IFACE2);
+ inOrder.verify(mStatsService).forceUpdate();
+ inOrder.verify(mNMService).stopInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE2);
+ inOrder.verify(mNMService).disableNat(IFACE_NAME, UPSTREAM_IFACE2);
+ }
+
+ @Test
+ public void handlesChangingUpstreamInterfaceForwardingFailure() throws Exception {
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
+
+ doThrow(RemoteException.class).when(mNMService).startInterfaceForwarding(
+ IFACE_NAME, UPSTREAM_IFACE2);
+
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE2);
+ InOrder inOrder = inOrder(mNMService, mStatsService);
+ inOrder.verify(mStatsService).forceUpdate();
+ inOrder.verify(mNMService).stopInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE);
+ inOrder.verify(mNMService).disableNat(IFACE_NAME, UPSTREAM_IFACE);
+ inOrder.verify(mNMService).enableNat(IFACE_NAME, UPSTREAM_IFACE2);
+ inOrder.verify(mNMService).startInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE2);
+ inOrder.verify(mStatsService).forceUpdate();
+ inOrder.verify(mNMService).stopInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE2);
+ inOrder.verify(mNMService).disableNat(IFACE_NAME, UPSTREAM_IFACE2);
+ }
+
+ @Test
+ public void canUnrequestTetheringWithUpstream() throws Exception {
+ initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE);
+
+ dispatchCommand(IpServer.CMD_TETHER_UNREQUESTED);
+ InOrder inOrder = inOrder(mNMService, mNetd, mStatsService, mCallback);
+ inOrder.verify(mStatsService).forceUpdate();
+ inOrder.verify(mNMService).stopInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE);
+ inOrder.verify(mNMService).disableNat(IFACE_NAME, UPSTREAM_IFACE);
+ inOrder.verify(mNMService).untetherInterface(IFACE_NAME);
+ inOrder.verify(mNetd).interfaceSetCfg(argThat(cfg -> IFACE_NAME.equals(cfg.ifName)));
+ inOrder.verify(mCallback).updateInterfaceState(
+ mIpServer, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR);
+ inOrder.verify(mCallback).updateLinkProperties(
+ eq(mIpServer), any(LinkProperties.class));
+ verifyNoMoreInteractions(mNMService, mStatsService, mCallback);
+ }
+
+ @Test
+ public void interfaceDownLeadsToUnavailable() throws Exception {
+ for (boolean shouldThrow : new boolean[]{true, false}) {
+ initTetheredStateMachine(TETHERING_USB, null);
+
+ if (shouldThrow) {
+ doThrow(RemoteException.class).when(mNMService).untetherInterface(IFACE_NAME);
+ }
+ dispatchCommand(IpServer.CMD_INTERFACE_DOWN);
+ InOrder usbTeardownOrder = inOrder(mNMService, mInterfaceConfiguration, mCallback);
+ usbTeardownOrder.verify(mInterfaceConfiguration).setInterfaceDown();
+ usbTeardownOrder.verify(mNMService).setInterfaceConfig(
+ IFACE_NAME, mInterfaceConfiguration);
+ usbTeardownOrder.verify(mCallback).updateInterfaceState(
+ mIpServer, STATE_UNAVAILABLE, TETHER_ERROR_NO_ERROR);
+ usbTeardownOrder.verify(mCallback).updateLinkProperties(
+ eq(mIpServer), mLinkPropertiesCaptor.capture());
+ assertNoAddressesNorRoutes(mLinkPropertiesCaptor.getValue());
+ }
+ }
+
+ @Test
+ public void usbShouldBeTornDownOnTetherError() throws Exception {
+ initStateMachine(TETHERING_USB);
+
+ doThrow(RemoteException.class).when(mNMService).tetherInterface(IFACE_NAME);
+ dispatchCommand(IpServer.CMD_TETHER_REQUESTED, STATE_TETHERED);
+ InOrder usbTeardownOrder = inOrder(mNMService, mInterfaceConfiguration, mCallback);
+ usbTeardownOrder.verify(mInterfaceConfiguration).setInterfaceDown();
+ usbTeardownOrder.verify(mNMService).setInterfaceConfig(
+ IFACE_NAME, mInterfaceConfiguration);
+ usbTeardownOrder.verify(mCallback).updateInterfaceState(
+ mIpServer, STATE_AVAILABLE, TETHER_ERROR_TETHER_IFACE_ERROR);
+ usbTeardownOrder.verify(mCallback).updateLinkProperties(
+ eq(mIpServer), mLinkPropertiesCaptor.capture());
+ assertNoAddressesNorRoutes(mLinkPropertiesCaptor.getValue());
+ }
+
+ @Test
+ public void shouldTearDownUsbOnUpstreamError() throws Exception {
+ initTetheredStateMachine(TETHERING_USB, null);
+
+ doThrow(RemoteException.class).when(mNMService).enableNat(anyString(), anyString());
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE);
+ InOrder usbTeardownOrder = inOrder(mNMService, mInterfaceConfiguration, mCallback);
+ usbTeardownOrder.verify(mInterfaceConfiguration).setInterfaceDown();
+ usbTeardownOrder.verify(mNMService).setInterfaceConfig(IFACE_NAME, mInterfaceConfiguration);
+ usbTeardownOrder.verify(mCallback).updateInterfaceState(
+ mIpServer, STATE_AVAILABLE, TETHER_ERROR_ENABLE_NAT_ERROR);
+ usbTeardownOrder.verify(mCallback).updateLinkProperties(
+ eq(mIpServer), mLinkPropertiesCaptor.capture());
+ assertNoAddressesNorRoutes(mLinkPropertiesCaptor.getValue());
+ }
+
+ @Test
+ public void ignoresDuplicateUpstreamNotifications() throws Exception {
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
+
+ verifyNoMoreInteractions(mNMService, mStatsService, mCallback);
+
+ for (int i = 0; i < 5; i++) {
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE);
+ verifyNoMoreInteractions(mNMService, mStatsService, mCallback);
+ }
+ }
+
+ @Test
+ public void startsDhcpServer() throws Exception {
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE);
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE);
+
+ assertDhcpStarted(new IpPrefix("192.168.43.0/24"));
+ }
+
+ @Test
+ public void startsDhcpServerOnBluetooth() throws Exception {
+ initTetheredStateMachine(TETHERING_BLUETOOTH, UPSTREAM_IFACE);
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE);
+
+ assertDhcpStarted(new IpPrefix("192.168.44.0/24"));
+ }
+
+ @Test
+ public void startsDhcpServerOnWifiP2p() throws Exception {
+ initTetheredStateMachine(TETHERING_WIFI_P2P, UPSTREAM_IFACE);
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE);
+
+ assertDhcpStarted(new IpPrefix("192.168.49.0/24"));
+ }
+
+ @Test
+ public void doesNotStartDhcpServerIfDisabled() throws Exception {
+ initTetheredStateMachine(TETHERING_WIFI, UPSTREAM_IFACE, true /* usingLegacyDhcp */);
+ dispatchTetherConnectionChanged(UPSTREAM_IFACE);
+
+ verify(mDependencies, never()).makeDhcpServer(any(), any(), any());
+ }
+
+ private void assertDhcpStarted(IpPrefix expectedPrefix) throws Exception {
+ verify(mDependencies, times(1)).makeDhcpServer(eq(IFACE_NAME), any(), any());
+ verify(mDhcpServer, timeout(MAKE_DHCPSERVER_TIMEOUT_MS).times(1)).start(any());
+ final DhcpServingParamsParcel params = mDhcpParamsCaptor.getValue();
+ // Last address byte is random
+ assertTrue(expectedPrefix.contains(intToInet4AddressHTH(params.serverAddr)));
+ assertEquals(expectedPrefix.getPrefixLength(), params.serverAddrPrefixLength);
+ assertEquals(1, params.defaultRouters.length);
+ assertEquals(params.serverAddr, params.defaultRouters[0]);
+ assertEquals(1, params.dnsServers.length);
+ assertEquals(params.serverAddr, params.dnsServers[0]);
+ assertEquals(DHCP_LEASE_TIME_SECS, params.dhcpLeaseTimeSecs);
+ }
+
+ /**
+ * Send a command to the state machine under test, and run the event loop to idle.
+ *
+ * @param command One of the IpServer.CMD_* constants.
+ * @param arg1 An additional argument to pass.
+ */
+ private void dispatchCommand(int command, int arg1) {
+ mIpServer.sendMessage(command, arg1);
+ mLooper.dispatchAll();
+ }
+
+ /**
+ * Send a command to the state machine under test, and run the event loop to idle.
+ *
+ * @param command One of the IpServer.CMD_* constants.
+ */
+ private void dispatchCommand(int command) {
+ mIpServer.sendMessage(command);
+ mLooper.dispatchAll();
+ }
+
+ /**
+ * Special override to tell the state machine that the upstream interface has changed.
+ *
+ * @see #dispatchCommand(int)
+ * @param upstreamIface String name of upstream interface (or null)
+ */
+ private void dispatchTetherConnectionChanged(String upstreamIface) {
+ mIpServer.sendMessage(IpServer.CMD_TETHER_CONNECTION_CHANGED,
+ new InterfaceSet(upstreamIface));
+ mLooper.dispatchAll();
+ }
+
+ private void assertIPv4AddressAndDirectlyConnectedRoute(LinkProperties lp) {
+ // Find the first IPv4 LinkAddress.
+ LinkAddress addr4 = null;
+ for (LinkAddress addr : lp.getLinkAddresses()) {
+ if (!(addr.getAddress() instanceof Inet4Address)) continue;
+ addr4 = addr;
+ break;
+ }
+ assertNotNull("missing IPv4 address", addr4);
+
+ // Assert the presence of the associated directly connected route.
+ final RouteInfo directlyConnected = new RouteInfo(addr4, null, lp.getInterfaceName());
+ assertTrue("missing directly connected route: '" + directlyConnected.toString() + "'",
+ lp.getRoutes().contains(directlyConnected));
+ }
+
+ private void assertNoAddressesNorRoutes(LinkProperties lp) {
+ assertTrue(lp.getLinkAddresses().isEmpty());
+ assertTrue(lp.getRoutes().isEmpty());
+ // We also check that interface name is non-empty, because we should
+ // never see an empty interface name in any LinkProperties update.
+ assertFalse(TextUtils.isEmpty(lp.getInterfaceName()));
+ }
+}
diff --git a/Tethering/tests/unit/src/android/net/util/InterfaceSetTest.java b/Tethering/tests/unit/src/android/net/util/InterfaceSetTest.java
new file mode 100644
index 0000000..ea084b6
--- /dev/null
+++ b/Tethering/tests/unit/src/android/net/util/InterfaceSetTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2018 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 android.net.util;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class InterfaceSetTest {
+ @Test
+ public void testNullNamesIgnored() {
+ final InterfaceSet set = new InterfaceSet(null, "if1", null, "if2", null);
+ assertEquals(2, set.ifnames.size());
+ assertTrue(set.ifnames.contains("if1"));
+ assertTrue(set.ifnames.contains("if2"));
+ }
+
+ @Test
+ public void testToString() {
+ final InterfaceSet set = new InterfaceSet("if1", "if2");
+ final String setString = set.toString();
+ assertTrue(setString.equals("[if1,if2]") || setString.equals("[if2,if1]"));
+ }
+
+ @Test
+ public void testToString_Empty() {
+ final InterfaceSet set = new InterfaceSet(null, null);
+ assertEquals("[]", set.toString());
+ }
+
+ @Test
+ public void testEquals() {
+ assertEquals(new InterfaceSet(null, "if1", "if2"), new InterfaceSet("if2", "if1"));
+ assertEquals(new InterfaceSet(null, null), new InterfaceSet());
+ assertFalse(new InterfaceSet("if1", "if3").equals(new InterfaceSet("if1", "if2")));
+ assertFalse(new InterfaceSet("if1", "if2").equals(new InterfaceSet("if1")));
+ assertFalse(new InterfaceSet().equals(null));
+ }
+}