Merge "Tethering: support Local-only Hotspot mode for downstreams"
This commit is contained in:
@@ -90,7 +90,9 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
|
||||
@@ -122,12 +124,23 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
|
||||
public final TetherInterfaceStateMachine stateMachine;
|
||||
public int lastState;
|
||||
public int lastError;
|
||||
|
||||
public TetherState(TetherInterfaceStateMachine sm) {
|
||||
stateMachine = sm;
|
||||
// Assume all state machines start out available and with no errors.
|
||||
lastState = IControlsTethering.STATE_AVAILABLE;
|
||||
lastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
|
||||
}
|
||||
|
||||
public boolean isCurrentlyServing() {
|
||||
switch (lastState) {
|
||||
case IControlsTethering.STATE_TETHERED:
|
||||
case IControlsTethering.STATE_LOCAL_HOTSPOT:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// used to synchronize public access to members
|
||||
@@ -143,11 +156,13 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
|
||||
private final StateMachine mTetherMasterSM;
|
||||
private final OffloadController mOffloadController;
|
||||
private final UpstreamNetworkMonitor mUpstreamNetworkMonitor;
|
||||
private final HashSet<TetherInterfaceStateMachine> mForwardedDownstreams;
|
||||
|
||||
private volatile TetheringConfiguration mConfig;
|
||||
private String mCurrentUpstreamIface;
|
||||
private Notification.Builder mTetheredNotificationBuilder;
|
||||
private int mLastNotificationId;
|
||||
|
||||
private boolean mRndisEnabled; // track the RNDIS function enabled state
|
||||
private boolean mUsbTetherRequested; // true if USB tethering should be started
|
||||
// when RNDIS is enabled
|
||||
@@ -174,6 +189,7 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
|
||||
mOffloadController = new OffloadController(mTetherMasterSM.getHandler());
|
||||
mUpstreamNetworkMonitor = new UpstreamNetworkMonitor(
|
||||
mContext, mTetherMasterSM, TetherMasterSM.EVENT_UPSTREAM_CALLBACK);
|
||||
mForwardedDownstreams = new HashSet<>();
|
||||
|
||||
mStateReceiver = new StateReceiver();
|
||||
IntentFilter filter = new IntentFilter();
|
||||
@@ -511,6 +527,10 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
|
||||
}
|
||||
|
||||
public int tether(String iface) {
|
||||
return tether(iface, IControlsTethering.STATE_TETHERED);
|
||||
}
|
||||
|
||||
private int tether(String iface, int requestedState) {
|
||||
if (DBG) Log.d(TAG, "Tethering " + iface);
|
||||
synchronized (mPublicSync) {
|
||||
TetherState tetherState = mTetherStates.get(iface);
|
||||
@@ -524,7 +544,13 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
|
||||
Log.e(TAG, "Tried to Tether an unavailable iface: " + iface + ", ignoring");
|
||||
return ConnectivityManager.TETHER_ERROR_UNAVAIL_IFACE;
|
||||
}
|
||||
tetherState.stateMachine.sendMessage(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
|
||||
// NOTE: If a CMD_TETHER_REQUESTED message is already in the TISM's
|
||||
// queue but not yet processed, this will be a no-op and it will not
|
||||
// return an error.
|
||||
//
|
||||
// TODO: reexamine the threading and messaging model.
|
||||
tetherState.stateMachine.sendMessage(
|
||||
TetherInterfaceStateMachine.CMD_TETHER_REQUESTED, requestedState);
|
||||
return ConnectivityManager.TETHER_ERROR_NO_ERROR;
|
||||
}
|
||||
}
|
||||
@@ -537,8 +563,8 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
|
||||
Log.e(TAG, "Tried to Untether an unknown iface :" + iface + ", ignoring");
|
||||
return ConnectivityManager.TETHER_ERROR_UNKNOWN_IFACE;
|
||||
}
|
||||
if (tetherState.lastState != IControlsTethering.STATE_TETHERED) {
|
||||
Log.e(TAG, "Tried to untether an untethered iface :" + iface + ", ignoring");
|
||||
if (!tetherState.isCurrentlyServing()) {
|
||||
Log.e(TAG, "Tried to untether an inactive iface :" + iface + ", ignoring");
|
||||
return ConnectivityManager.TETHER_ERROR_UNAVAIL_IFACE;
|
||||
}
|
||||
tetherState.stateMachine.sendMessage(
|
||||
@@ -565,6 +591,7 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Figure out how to update for local hotspot mode interfaces.
|
||||
private void sendTetherStateChangedBroadcast() {
|
||||
if (!getConnectivityManager().isTetheringSupported()) return;
|
||||
|
||||
@@ -728,7 +755,9 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
|
||||
mRndisEnabled = rndisEnabled;
|
||||
// start tethering if we have a request pending
|
||||
if (usbConnected && mRndisEnabled && mUsbTetherRequested) {
|
||||
tetherMatchingInterfaces(true, ConnectivityManager.TETHERING_USB);
|
||||
tetherMatchingInterfaces(
|
||||
IControlsTethering.STATE_TETHERED,
|
||||
ConnectivityManager.TETHERING_USB);
|
||||
}
|
||||
mUsbTetherRequested = false;
|
||||
}
|
||||
@@ -743,9 +772,11 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
|
||||
break;
|
||||
case WifiManager.WIFI_AP_STATE_ENABLED:
|
||||
// When the AP comes up and we've been requested to tether it, do so.
|
||||
if (mWifiTetherRequested) {
|
||||
tetherMatchingInterfaces(true, ConnectivityManager.TETHERING_WIFI);
|
||||
}
|
||||
// Otherwise, assume it's a local-only hotspot request.
|
||||
final int state = mWifiTetherRequested
|
||||
? IControlsTethering.STATE_TETHERED
|
||||
: IControlsTethering.STATE_LOCAL_HOTSPOT;
|
||||
tetherMatchingInterfaces(state, ConnectivityManager.TETHERING_WIFI);
|
||||
break;
|
||||
case WifiManager.WIFI_AP_STATE_DISABLED:
|
||||
case WifiManager.WIFI_AP_STATE_DISABLING:
|
||||
@@ -775,8 +806,16 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
|
||||
}
|
||||
}
|
||||
|
||||
private void tetherMatchingInterfaces(boolean enable, int interfaceType) {
|
||||
if (VDBG) Log.d(TAG, "tetherMatchingInterfaces(" + enable + ", " + interfaceType + ")");
|
||||
// TODO: Consider renaming to something more accurate in its description.
|
||||
// This method:
|
||||
// - allows requesting either tethering or local hotspot serving states
|
||||
// - handles both enabling and disabling serving states
|
||||
// - only tethers the first matching interface in listInterfaces()
|
||||
// order of a given type
|
||||
private void tetherMatchingInterfaces(int requestedState, int interfaceType) {
|
||||
if (VDBG) {
|
||||
Log.d(TAG, "tetherMatchingInterfaces(" + requestedState + ", " + interfaceType + ")");
|
||||
}
|
||||
|
||||
String[] ifaces = null;
|
||||
try {
|
||||
@@ -799,7 +838,20 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
|
||||
return;
|
||||
}
|
||||
|
||||
int result = (enable ? tether(chosenIface) : untether(chosenIface));
|
||||
final int result;
|
||||
switch (requestedState) {
|
||||
case IControlsTethering.STATE_UNAVAILABLE:
|
||||
case IControlsTethering.STATE_AVAILABLE:
|
||||
result = untether(chosenIface);
|
||||
break;
|
||||
case IControlsTethering.STATE_TETHERED:
|
||||
case IControlsTethering.STATE_LOCAL_HOTSPOT:
|
||||
result = tether(chosenIface, requestedState);
|
||||
break;
|
||||
default:
|
||||
Log.wtf(TAG, "Unknown interface state: " + requestedState);
|
||||
return;
|
||||
}
|
||||
if (result != ConnectivityManager.TETHER_ERROR_NO_ERROR) {
|
||||
Log.e(TAG, "unable start or stop tethering on iface " + chosenIface);
|
||||
return;
|
||||
@@ -844,7 +896,8 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
|
||||
if (mRndisEnabled) {
|
||||
final long ident = Binder.clearCallingIdentity();
|
||||
try {
|
||||
tetherMatchingInterfaces(true, ConnectivityManager.TETHERING_USB);
|
||||
tetherMatchingInterfaces(IControlsTethering.STATE_TETHERED,
|
||||
ConnectivityManager.TETHERING_USB);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(ident);
|
||||
}
|
||||
@@ -855,7 +908,8 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
|
||||
} else {
|
||||
final long ident = Binder.clearCallingIdentity();
|
||||
try {
|
||||
tetherMatchingInterfaces(false, ConnectivityManager.TETHERING_USB);
|
||||
tetherMatchingInterfaces(IControlsTethering.STATE_AVAILABLE,
|
||||
ConnectivityManager.TETHERING_USB);
|
||||
} finally {
|
||||
Binder.restoreCallingIdentity(ident);
|
||||
}
|
||||
@@ -919,6 +973,14 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
|
||||
}
|
||||
}
|
||||
|
||||
private boolean upstreamWanted() {
|
||||
if (!mForwardedDownstreams.isEmpty()) return true;
|
||||
|
||||
synchronized (mPublicSync) {
|
||||
return mUsbTetherRequested || mWifiTetherRequested;
|
||||
}
|
||||
}
|
||||
|
||||
// Needed because the canonical source of upstream truth is just the
|
||||
// upstream interface name, |mCurrentUpstreamIface|. This is ripe for
|
||||
// future simplification, once the upstream Network is canonical.
|
||||
@@ -935,10 +997,10 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
|
||||
|
||||
class TetherMasterSM extends StateMachine {
|
||||
private static final int BASE_MASTER = Protocol.BASE_TETHERING;
|
||||
// an interface SM has requested Tethering
|
||||
static final int CMD_TETHER_MODE_REQUESTED = BASE_MASTER + 1;
|
||||
// an interface SM has unrequested Tethering
|
||||
static final int CMD_TETHER_MODE_UNREQUESTED = BASE_MASTER + 2;
|
||||
// an interface SM has requested Tethering/Local Hotspot
|
||||
static final int EVENT_IFACE_SERVING_STATE_ACTIVE = BASE_MASTER + 1;
|
||||
// an interface SM has unrequested Tethering/Local Hotspot
|
||||
static final int EVENT_IFACE_SERVING_STATE_INACTIVE = BASE_MASTER + 2;
|
||||
// upstream connection change - do the right thing
|
||||
static final int CMD_UPSTREAM_CHANGED = BASE_MASTER + 3;
|
||||
// we don't have a valid upstream conn, check again after a delay
|
||||
@@ -1023,7 +1085,9 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
|
||||
transitionTo(mSetIpForwardingEnabledErrorState);
|
||||
return false;
|
||||
}
|
||||
// TODO: Randomize DHCPv4 ranges, especially in hotspot mode.
|
||||
try {
|
||||
// TODO: Find a more accurate method name (startDHCPv4()?).
|
||||
mNMService.startTethering(cfg.dhcpRanges);
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
@@ -1325,26 +1389,41 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
|
||||
}
|
||||
}
|
||||
|
||||
private void handleInterfaceServingStateActive(int mode, TetherInterfaceStateMachine who) {
|
||||
if (mNotifyList.indexOf(who) < 0) {
|
||||
mNotifyList.add(who);
|
||||
mIPv6TetheringCoordinator.addActiveDownstream(who, mode);
|
||||
}
|
||||
|
||||
if (mode == IControlsTethering.STATE_TETHERED) {
|
||||
mForwardedDownstreams.add(who);
|
||||
} else {
|
||||
mForwardedDownstreams.remove(who);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleInterfaceServingStateInactive(TetherInterfaceStateMachine who) {
|
||||
mNotifyList.remove(who);
|
||||
mIPv6TetheringCoordinator.removeActiveDownstream(who);
|
||||
mForwardedDownstreams.remove(who);
|
||||
}
|
||||
|
||||
class InitialState extends TetherMasterUtilState {
|
||||
@Override
|
||||
public boolean processMessage(Message message) {
|
||||
maybeLogMessage(this, message.what);
|
||||
boolean retValue = true;
|
||||
switch (message.what) {
|
||||
case CMD_TETHER_MODE_REQUESTED:
|
||||
case EVENT_IFACE_SERVING_STATE_ACTIVE:
|
||||
TetherInterfaceStateMachine who = (TetherInterfaceStateMachine)message.obj;
|
||||
if (VDBG) Log.d(TAG, "Tether Mode requested by " + who);
|
||||
if (mNotifyList.indexOf(who) < 0) {
|
||||
mNotifyList.add(who);
|
||||
mIPv6TetheringCoordinator.addActiveDownstream(who);
|
||||
}
|
||||
handleInterfaceServingStateActive(message.arg1, who);
|
||||
transitionTo(mTetherModeAliveState);
|
||||
break;
|
||||
case CMD_TETHER_MODE_UNREQUESTED:
|
||||
case EVENT_IFACE_SERVING_STATE_INACTIVE:
|
||||
who = (TetherInterfaceStateMachine)message.obj;
|
||||
if (VDBG) Log.d(TAG, "Tether Mode unrequested by " + who);
|
||||
mNotifyList.remove(who);
|
||||
mIPv6TetheringCoordinator.removeActiveDownstream(who);
|
||||
handleInterfaceServingStateInactive(who);
|
||||
break;
|
||||
default:
|
||||
retValue = false;
|
||||
@@ -1356,6 +1435,7 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
|
||||
|
||||
class TetherModeAliveState extends TetherMasterUtilState {
|
||||
final SimChangeListener simChange = new SimChangeListener(mContext);
|
||||
boolean mUpstreamWanted = false;
|
||||
boolean mTryCell = true;
|
||||
|
||||
@Override
|
||||
@@ -1366,9 +1446,11 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
|
||||
mUpstreamNetworkMonitor.start();
|
||||
mOffloadController.start();
|
||||
|
||||
// Better try something first pass or crazy tests cases will fail.
|
||||
chooseUpstreamType(true);
|
||||
mTryCell = false;
|
||||
if (upstreamWanted()) {
|
||||
mUpstreamWanted = true;
|
||||
chooseUpstreamType(true);
|
||||
mTryCell = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1381,54 +1463,74 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
|
||||
handleNewUpstreamNetworkState(null);
|
||||
}
|
||||
|
||||
private boolean updateUpstreamWanted() {
|
||||
final boolean previousUpstreamWanted = mUpstreamWanted;
|
||||
mUpstreamWanted = upstreamWanted();
|
||||
return previousUpstreamWanted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean processMessage(Message message) {
|
||||
maybeLogMessage(this, message.what);
|
||||
boolean retValue = true;
|
||||
switch (message.what) {
|
||||
case CMD_TETHER_MODE_REQUESTED: {
|
||||
case EVENT_IFACE_SERVING_STATE_ACTIVE: {
|
||||
TetherInterfaceStateMachine who = (TetherInterfaceStateMachine)message.obj;
|
||||
if (VDBG) Log.d(TAG, "Tether Mode requested by " + who);
|
||||
if (mNotifyList.indexOf(who) < 0) {
|
||||
mNotifyList.add(who);
|
||||
mIPv6TetheringCoordinator.addActiveDownstream(who);
|
||||
}
|
||||
handleInterfaceServingStateActive(message.arg1, who);
|
||||
who.sendMessage(TetherInterfaceStateMachine.CMD_TETHER_CONNECTION_CHANGED,
|
||||
mCurrentUpstreamIface);
|
||||
// If there has been a change and an upstream is now
|
||||
// desired, kick off the selection process.
|
||||
final boolean previousUpstreamWanted = updateUpstreamWanted();
|
||||
if (!previousUpstreamWanted && mUpstreamWanted) {
|
||||
chooseUpstreamType(true);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CMD_TETHER_MODE_UNREQUESTED: {
|
||||
case EVENT_IFACE_SERVING_STATE_INACTIVE: {
|
||||
TetherInterfaceStateMachine who = (TetherInterfaceStateMachine)message.obj;
|
||||
if (VDBG) Log.d(TAG, "Tether Mode unrequested by " + who);
|
||||
if (mNotifyList.remove(who)) {
|
||||
if (DBG) Log.d(TAG, "TetherModeAlive removing notifyee " + who);
|
||||
if (mNotifyList.isEmpty()) {
|
||||
turnOffMasterTetherSettings(); // transitions appropriately
|
||||
} else {
|
||||
if (DBG) {
|
||||
Log.d(TAG, "TetherModeAlive still has " + mNotifyList.size() +
|
||||
" live requests:");
|
||||
for (TetherInterfaceStateMachine o : mNotifyList) {
|
||||
Log.d(TAG, " " + o);
|
||||
}
|
||||
handleInterfaceServingStateInactive(who);
|
||||
|
||||
if (mNotifyList.isEmpty()) {
|
||||
turnOffMasterTetherSettings(); // transitions appropriately
|
||||
} else {
|
||||
if (DBG) {
|
||||
Log.d(TAG, "TetherModeAlive still has " + mNotifyList.size() +
|
||||
" live requests:");
|
||||
for (TetherInterfaceStateMachine o : mNotifyList) {
|
||||
Log.d(TAG, " " + o);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "TetherModeAliveState UNREQUESTED has unknown who: " + who);
|
||||
}
|
||||
mIPv6TetheringCoordinator.removeActiveDownstream(who);
|
||||
// If there has been a change and an upstream is no
|
||||
// longer desired, release any mobile requests.
|
||||
final boolean previousUpstreamWanted = updateUpstreamWanted();
|
||||
if (previousUpstreamWanted && !mUpstreamWanted) {
|
||||
mUpstreamNetworkMonitor.releaseMobileNetworkRequest();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CMD_UPSTREAM_CHANGED:
|
||||
updateUpstreamWanted();
|
||||
if (!mUpstreamWanted) break;
|
||||
|
||||
// Need to try DUN immediately if Wi-Fi goes down.
|
||||
chooseUpstreamType(true);
|
||||
mTryCell = false;
|
||||
break;
|
||||
case CMD_RETRY_UPSTREAM:
|
||||
updateUpstreamWanted();
|
||||
if (!mUpstreamWanted) break;
|
||||
|
||||
chooseUpstreamType(mTryCell);
|
||||
mTryCell = !mTryCell;
|
||||
break;
|
||||
case EVENT_UPSTREAM_CALLBACK: {
|
||||
updateUpstreamWanted();
|
||||
if (!mUpstreamWanted) break;
|
||||
|
||||
final NetworkState ns = (NetworkState) message.obj;
|
||||
|
||||
if (ns == null || !pertainsToCurrentUpstream(ns)) {
|
||||
@@ -1490,7 +1592,7 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
|
||||
public boolean processMessage(Message message) {
|
||||
boolean retValue = true;
|
||||
switch (message.what) {
|
||||
case CMD_TETHER_MODE_REQUESTED:
|
||||
case EVENT_IFACE_SERVING_STATE_ACTIVE:
|
||||
TetherInterfaceStateMachine who = (TetherInterfaceStateMachine)message.obj;
|
||||
who.sendMessage(mErrorNotification);
|
||||
break;
|
||||
@@ -1604,12 +1706,16 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
|
||||
case IControlsTethering.STATE_TETHERED:
|
||||
pw.print("TetheredState");
|
||||
break;
|
||||
case IControlsTethering.STATE_LOCAL_HOTSPOT:
|
||||
pw.print("LocalHotspotState");
|
||||
break;
|
||||
default:
|
||||
pw.print("UnknownState");
|
||||
break;
|
||||
}
|
||||
pw.println(" - lastError = " + tetherState.lastError);
|
||||
}
|
||||
pw.println("Upstream wanted: " + upstreamWanted());
|
||||
pw.decreaseIndent();
|
||||
}
|
||||
pw.decreaseIndent();
|
||||
@@ -1648,15 +1754,21 @@ public class Tethering extends BaseNetworkObserver implements IControlsTethering
|
||||
if (error == ConnectivityManager.TETHER_ERROR_MASTER_ERROR) {
|
||||
mTetherMasterSM.sendMessage(TetherMasterSM.CMD_CLEAR_ERROR, who);
|
||||
}
|
||||
int which;
|
||||
switch (state) {
|
||||
case IControlsTethering.STATE_UNAVAILABLE:
|
||||
case IControlsTethering.STATE_AVAILABLE:
|
||||
mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_UNREQUESTED, who);
|
||||
which = TetherMasterSM.EVENT_IFACE_SERVING_STATE_INACTIVE;
|
||||
break;
|
||||
case IControlsTethering.STATE_TETHERED:
|
||||
mTetherMasterSM.sendMessage(TetherMasterSM.CMD_TETHER_MODE_REQUESTED, who);
|
||||
case IControlsTethering.STATE_LOCAL_HOTSPOT:
|
||||
which = TetherMasterSM.EVENT_IFACE_SERVING_STATE_ACTIVE;
|
||||
break;
|
||||
default:
|
||||
Log.wtf(TAG, "Unknown interface state: " + state);
|
||||
return;
|
||||
}
|
||||
mTetherMasterSM.sendMessage(which, state, 0, who);
|
||||
sendTetherStateChangedBroadcast();
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ public interface IControlsTethering {
|
||||
public final int STATE_UNAVAILABLE = 0;
|
||||
public final int STATE_AVAILABLE = 1;
|
||||
public final int STATE_TETHERED = 2;
|
||||
public final int STATE_LOCAL_HOTSPOT = 3;
|
||||
|
||||
/**
|
||||
* Notify that |who| has changed its tethering state. This may be called from any thread.
|
||||
|
||||
@@ -24,12 +24,17 @@ import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.net.NetworkState;
|
||||
import android.net.RouteInfo;
|
||||
import android.net.util.NetworkConstants;
|
||||
import android.util.Log;
|
||||
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Random;
|
||||
|
||||
|
||||
/**
|
||||
@@ -45,29 +50,65 @@ public class IPv6TetheringCoordinator {
|
||||
private static final boolean DBG = false;
|
||||
private static final boolean VDBG = false;
|
||||
|
||||
private static class Downstream {
|
||||
public final TetherInterfaceStateMachine tism;
|
||||
public final int mode; // IControlsTethering.STATE_*
|
||||
// Used to append to a ULA /48, constructing a ULA /64 for local use.
|
||||
public final short subnetId;
|
||||
|
||||
Downstream(TetherInterfaceStateMachine tism, int mode, short subnetId) {
|
||||
this.tism = tism;
|
||||
this.mode = mode;
|
||||
this.subnetId = subnetId;
|
||||
}
|
||||
}
|
||||
|
||||
private final ArrayList<TetherInterfaceStateMachine> mNotifyList;
|
||||
private final LinkedList<TetherInterfaceStateMachine> mActiveDownstreams;
|
||||
// NOTE: mActiveDownstreams is a list and not a hash data structure because
|
||||
// we keep active downstreams in arrival order. This is done so /64s can
|
||||
// be parceled out on a "first come, first served" basis and a /64 used by
|
||||
// a downstream that is no longer active can be redistributed to any next
|
||||
// waiting active downstream (again, in arrival order).
|
||||
private final LinkedList<Downstream> mActiveDownstreams;
|
||||
private final byte[] mUniqueLocalPrefix;
|
||||
private short mNextSubnetId;
|
||||
private NetworkState mUpstreamNetworkState;
|
||||
|
||||
public IPv6TetheringCoordinator(ArrayList<TetherInterfaceStateMachine> notifyList) {
|
||||
mNotifyList = notifyList;
|
||||
mActiveDownstreams = new LinkedList<>();
|
||||
mUniqueLocalPrefix = generateUniqueLocalPrefix();
|
||||
mNextSubnetId = 0;
|
||||
}
|
||||
|
||||
public void addActiveDownstream(TetherInterfaceStateMachine downstream) {
|
||||
if (mActiveDownstreams.indexOf(downstream) == -1) {
|
||||
public void addActiveDownstream(TetherInterfaceStateMachine downstream, int mode) {
|
||||
if (findDownstream(downstream) == null) {
|
||||
// Adding a new downstream appends it to the list. Adding a
|
||||
// downstream a second time without first removing it has no effect.
|
||||
mActiveDownstreams.offer(downstream);
|
||||
// We never change the mode of a downstream except by first removing
|
||||
// it and then re-adding it (with its new mode specified);
|
||||
if (mActiveDownstreams.offer(new Downstream(downstream, mode, mNextSubnetId))) {
|
||||
// Make sure subnet IDs are always positive. They are appended
|
||||
// to a ULA /48 to make a ULA /64 for local use.
|
||||
mNextSubnetId = (short) Math.max(0, mNextSubnetId + 1);
|
||||
}
|
||||
updateIPv6TetheringInterfaces();
|
||||
}
|
||||
}
|
||||
|
||||
public void removeActiveDownstream(TetherInterfaceStateMachine downstream) {
|
||||
stopIPv6TetheringOn(downstream);
|
||||
if (mActiveDownstreams.remove(downstream)) {
|
||||
if (mActiveDownstreams.remove(findDownstream(downstream))) {
|
||||
updateIPv6TetheringInterfaces();
|
||||
}
|
||||
|
||||
// When tethering is stopping we can reset the subnet counter.
|
||||
if (mNotifyList.isEmpty()) {
|
||||
if (!mActiveDownstreams.isEmpty()) {
|
||||
Log.wtf(TAG, "Tethering notify list empty, IPv6 downstreams non-empty.");
|
||||
}
|
||||
mNextSubnetId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void updateUpstreamNetworkState(NetworkState ns) {
|
||||
@@ -123,20 +164,31 @@ public class IPv6TetheringCoordinator {
|
||||
}
|
||||
|
||||
private LinkProperties getInterfaceIPv6LinkProperties(TetherInterfaceStateMachine sm) {
|
||||
if (mUpstreamNetworkState == null) return null;
|
||||
|
||||
if (sm.interfaceType() == ConnectivityManager.TETHERING_BLUETOOTH) {
|
||||
// TODO: Figure out IPv6 support on PAN interfaces.
|
||||
return null;
|
||||
}
|
||||
|
||||
final Downstream ds = findDownstream(sm);
|
||||
if (ds == null) return null;
|
||||
|
||||
if (ds.mode == IControlsTethering.STATE_LOCAL_HOTSPOT) {
|
||||
// Build a Unique Locally-assigned Prefix configuration.
|
||||
return getUniqueLocalConfig(mUniqueLocalPrefix, ds.subnetId);
|
||||
}
|
||||
|
||||
// This downstream is in IControlsTethering.STATE_TETHERED mode.
|
||||
if (mUpstreamNetworkState == null || mUpstreamNetworkState.linkProperties == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// NOTE: Here, in future, we would have policies to decide how to divvy
|
||||
// up the available dedicated prefixes among downstream interfaces.
|
||||
// At this time we have no such mechanism--we only support tethering
|
||||
// IPv6 toward the oldest (first requested) active downstream.
|
||||
|
||||
final TetherInterfaceStateMachine currentActive = mActiveDownstreams.peek();
|
||||
if (currentActive != null && currentActive == sm) {
|
||||
final Downstream currentActive = mActiveDownstreams.peek();
|
||||
if (currentActive != null && currentActive.tism == sm) {
|
||||
final LinkProperties lp = getIPv6OnlyLinkProperties(
|
||||
mUpstreamNetworkState.linkProperties);
|
||||
if (lp.hasIPv6DefaultRoute() && lp.hasGlobalIPv6Address()) {
|
||||
@@ -147,6 +199,13 @@ public class IPv6TetheringCoordinator {
|
||||
return null;
|
||||
}
|
||||
|
||||
Downstream findDownstream(TetherInterfaceStateMachine tism) {
|
||||
for (Downstream ds : mActiveDownstreams) {
|
||||
if (ds.tism == tism) return ds;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean canTetherIPv6(NetworkState ns) {
|
||||
// Broadly speaking:
|
||||
//
|
||||
@@ -263,6 +322,44 @@ public class IPv6TetheringCoordinator {
|
||||
!ip.isMulticastAddress();
|
||||
}
|
||||
|
||||
private static LinkProperties getUniqueLocalConfig(byte[] ulp, short subnetId) {
|
||||
final LinkProperties lp = new LinkProperties();
|
||||
|
||||
final IpPrefix local48 = makeUniqueLocalPrefix(ulp, (short) 0, 48);
|
||||
lp.addRoute(new RouteInfo(local48, null, null));
|
||||
|
||||
final IpPrefix local64 = makeUniqueLocalPrefix(ulp, subnetId, 64);
|
||||
// Because this is a locally-generated ULA, we don't have an upstream
|
||||
// address. But because the downstream IP address management code gets
|
||||
// its prefix from the upstream's IP address, we create a fake one here.
|
||||
lp.addLinkAddress(new LinkAddress(local64.getAddress(), 64));
|
||||
|
||||
lp.setMtu(NetworkConstants.ETHER_MTU);
|
||||
return lp;
|
||||
}
|
||||
|
||||
private static IpPrefix makeUniqueLocalPrefix(byte[] in6addr, short subnetId, int prefixlen) {
|
||||
final byte[] bytes = Arrays.copyOf(in6addr, in6addr.length);
|
||||
bytes[7] = (byte) (subnetId >> 8);
|
||||
bytes[8] = (byte) subnetId;
|
||||
return new IpPrefix(bytes, prefixlen);
|
||||
}
|
||||
|
||||
// Generates a Unique Locally-assigned Prefix:
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc4193#section-3.1
|
||||
//
|
||||
// The result is a /48 that can be used for local-only communications.
|
||||
private static byte[] generateUniqueLocalPrefix() {
|
||||
final byte[] ulp = new byte[6]; // 6 = 48bits / 8bits/byte
|
||||
(new Random()).nextBytes(ulp);
|
||||
|
||||
final byte[] in6addr = Arrays.copyOf(ulp, NetworkConstants.IPV6_ADDR_LEN);
|
||||
in6addr[0] = (byte) 0xfd; // fc00::/7 and L=1
|
||||
|
||||
return in6addr;
|
||||
}
|
||||
|
||||
private static String toDebugString(NetworkState ns) {
|
||||
if (ns == null) {
|
||||
return "NetworkState{null}";
|
||||
|
||||
@@ -42,6 +42,7 @@ import java.net.UnknownHostException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
|
||||
|
||||
/**
|
||||
@@ -66,10 +67,17 @@ public class IPv6TetheringInterfaceServices {
|
||||
}
|
||||
|
||||
public boolean start() {
|
||||
// TODO: Refactor for testability (perhaps passing an android.system.Os
|
||||
// instance and calling getifaddrs() directly).
|
||||
try {
|
||||
mNetworkInterface = NetworkInterface.getByName(mIfName);
|
||||
} catch (SocketException e) {
|
||||
Log.e(TAG, "Failed to find NetworkInterface for " + mIfName, e);
|
||||
Log.e(TAG, "Error looking up NetworkInterfaces for " + mIfName, e);
|
||||
stop();
|
||||
return false;
|
||||
}
|
||||
if (mNetworkInterface == null) {
|
||||
Log.e(TAG, "Failed to find NetworkInterface for " + mIfName);
|
||||
stop();
|
||||
return false;
|
||||
}
|
||||
@@ -267,10 +275,10 @@ public class IPv6TetheringInterfaceServices {
|
||||
return localRoutes;
|
||||
}
|
||||
|
||||
// Given a prefix like 2001:db8::/64 return 2001:db8::1.
|
||||
// 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] = 0x1;
|
||||
dnsBytes[dnsBytes.length - 1] = getRandomNonZeroByte();
|
||||
try {
|
||||
return Inet6Address.getByAddress(null, dnsBytes, 0);
|
||||
} catch (UnknownHostException e) {
|
||||
@@ -278,4 +286,11 @@ public class IPv6TetheringInterfaceServices {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte getRandomNonZeroByte() {
|
||||
final byte random = (byte) (new Random()).nextInt();
|
||||
// Don't pick the subnet-router anycast address, since that might be
|
||||
// in use on the upstream already.
|
||||
return (random != 0) ? random : 0x1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,8 @@ public class TetherInterfaceStateMachine extends StateMachine {
|
||||
public static final int CMD_IPV6_TETHER_UPDATE = BASE_IFACE + 13;
|
||||
|
||||
private final State mInitialState;
|
||||
private final State mServingState;
|
||||
private final State mLocalHotspotState;
|
||||
private final State mTetheredState;
|
||||
private final State mUnavailableState;
|
||||
|
||||
@@ -105,10 +107,14 @@ public class TetherInterfaceStateMachine extends StateMachine {
|
||||
mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
|
||||
|
||||
mInitialState = new InitialState();
|
||||
addState(mInitialState);
|
||||
mServingState = new ServingState();
|
||||
mLocalHotspotState = new LocalHotspotState();
|
||||
mTetheredState = new TetheredState();
|
||||
addState(mTetheredState);
|
||||
mUnavailableState = new UnavailableState();
|
||||
addState(mInitialState);
|
||||
addState(mServingState);
|
||||
addState(mLocalHotspotState, mServingState);
|
||||
addState(mTetheredState, mServingState);
|
||||
addState(mUnavailableState);
|
||||
|
||||
setInitialState(mInitialState);
|
||||
@@ -172,12 +178,15 @@ public class TetherInterfaceStateMachine extends StateMachine {
|
||||
}
|
||||
}
|
||||
|
||||
private void sendInterfaceState(int newInterfaceState) {
|
||||
mTetherController.notifyInterfaceStateChange(
|
||||
mIfaceName, TetherInterfaceStateMachine.this, newInterfaceState, mLastError);
|
||||
}
|
||||
|
||||
class InitialState extends State {
|
||||
@Override
|
||||
public void enter() {
|
||||
mTetherController.notifyInterfaceStateChange(
|
||||
mIfaceName, TetherInterfaceStateMachine.this,
|
||||
IControlsTethering.STATE_AVAILABLE, mLastError);
|
||||
sendInterfaceState(IControlsTethering.STATE_AVAILABLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -187,7 +196,16 @@ public class TetherInterfaceStateMachine extends StateMachine {
|
||||
switch (message.what) {
|
||||
case CMD_TETHER_REQUESTED:
|
||||
mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
|
||||
transitionTo(mTetheredState);
|
||||
switch (message.arg1) {
|
||||
case IControlsTethering.STATE_LOCAL_HOTSPOT:
|
||||
transitionTo(mLocalHotspotState);
|
||||
break;
|
||||
case IControlsTethering.STATE_TETHERED:
|
||||
transitionTo(mTetheredState);
|
||||
break;
|
||||
default:
|
||||
Log.e(TAG, "Invalid tethering interface serving state specified.");
|
||||
}
|
||||
break;
|
||||
case CMD_INTERFACE_DOWN:
|
||||
transitionTo(mUnavailableState);
|
||||
@@ -204,7 +222,7 @@ public class TetherInterfaceStateMachine extends StateMachine {
|
||||
}
|
||||
}
|
||||
|
||||
class TetheredState extends State {
|
||||
class ServingState extends State {
|
||||
@Override
|
||||
public void enter() {
|
||||
if (!configureIfaceIp(true)) {
|
||||
@@ -225,11 +243,6 @@ public class TetherInterfaceStateMachine extends StateMachine {
|
||||
if (!mIPv6TetherSvc.start()) {
|
||||
Log.e(TAG, "Failed to start IPv6TetheringInterfaceServices");
|
||||
}
|
||||
|
||||
if (DBG) Log.d(TAG, "Tethered " + mIfaceName);
|
||||
mTetherController.notifyInterfaceStateChange(
|
||||
mIfaceName, TetherInterfaceStateMachine.this,
|
||||
IControlsTethering.STATE_TETHERED, mLastError);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -238,7 +251,6 @@ public class TetherInterfaceStateMachine extends StateMachine {
|
||||
// of these operations, but it doesn't really change that we have to try them
|
||||
// all in sequence.
|
||||
mIPv6TetherSvc.stop();
|
||||
cleanupUpstream();
|
||||
|
||||
try {
|
||||
mNMService.untetherInterface(mIfaceName);
|
||||
@@ -250,6 +262,73 @@ public class TetherInterfaceStateMachine extends StateMachine {
|
||||
configureIfaceIp(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean processMessage(Message message) {
|
||||
maybeLogMessage(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:
|
||||
mIPv6TetherSvc.updateUpstreamIPv6LinkProperties(
|
||||
(LinkProperties) message.obj);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
class LocalHotspotState extends State {
|
||||
@Override
|
||||
public void enter() {
|
||||
if (DBG) Log.d(TAG, "Local hotspot " + mIfaceName);
|
||||
sendInterfaceState(IControlsTethering.STATE_LOCAL_HOTSPOT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean processMessage(Message message) {
|
||||
maybeLogMessage(this, message.what);
|
||||
switch (message.what) {
|
||||
case CMD_TETHER_REQUESTED:
|
||||
Log.e(TAG, "CMD_TETHER_REQUESTED while in local hotspot mode.");
|
||||
break;
|
||||
case CMD_TETHER_CONNECTION_CHANGED:
|
||||
// Ignored in local hotspot state.
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class TetheredState extends State {
|
||||
@Override
|
||||
public void enter() {
|
||||
if (DBG) Log.d(TAG, "Tethered " + mIfaceName);
|
||||
sendInterfaceState(IControlsTethering.STATE_TETHERED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exit() {
|
||||
cleanupUpstream();
|
||||
}
|
||||
|
||||
private void cleanupUpstream() {
|
||||
if (mMyUpstreamIfaceName == null) return;
|
||||
|
||||
@@ -285,13 +364,8 @@ public class TetherInterfaceStateMachine extends StateMachine {
|
||||
maybeLogMessage(this, message.what);
|
||||
boolean retValue = true;
|
||||
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);
|
||||
case CMD_TETHER_REQUESTED:
|
||||
Log.e(TAG, "CMD_TETHER_REQUESTED while already tethering.");
|
||||
break;
|
||||
case CMD_TETHER_CONNECTION_CHANGED:
|
||||
String newUpstreamIfaceName = (String)(message.obj);
|
||||
@@ -317,18 +391,6 @@ public class TetherInterfaceStateMachine extends StateMachine {
|
||||
}
|
||||
mMyUpstreamIfaceName = newUpstreamIfaceName;
|
||||
break;
|
||||
case CMD_IPV6_TETHER_UPDATE:
|
||||
mIPv6TetherSvc.updateUpstreamIPv6LinkProperties(
|
||||
(LinkProperties) message.obj);
|
||||
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:
|
||||
retValue = false;
|
||||
break;
|
||||
@@ -348,9 +410,7 @@ public class TetherInterfaceStateMachine extends StateMachine {
|
||||
@Override
|
||||
public void enter() {
|
||||
mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
|
||||
mTetherController.notifyInterfaceStateChange(
|
||||
mIfaceName, TetherInterfaceStateMachine.this,
|
||||
IControlsTethering.STATE_UNAVAILABLE, mLastError);
|
||||
sendInterfaceState(IControlsTethering.STATE_UNAVAILABLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +308,8 @@ public class UpstreamNetworkMonitor {
|
||||
// Fetch (and cache) a ConnectivityManager only if and when we need one.
|
||||
private ConnectivityManager cm() {
|
||||
if (mCM == null) {
|
||||
mCM = mContext.getSystemService(ConnectivityManager.class);
|
||||
// MUST call the String variant to be able to write unittests.
|
||||
mCM = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
}
|
||||
return mCM;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
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.*;
|
||||
|
||||
import android.net.IpPrefix;
|
||||
@@ -68,7 +70,6 @@ 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 IPV6_MIN_MTU = 1280;
|
||||
private static final int MIN_RA_HEADER_SIZE = 16;
|
||||
|
||||
// Summary of various timers and lifetimes.
|
||||
@@ -542,6 +543,14 @@ public class RouterAdvertisementDaemon {
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
*/
|
||||
|
||||
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)
|
||||
@@ -549,7 +558,7 @@ public class RouterAdvertisementDaemon {
|
||||
.putShort(asShort(0))
|
||||
.putInt(lifetime);
|
||||
|
||||
for (Inet6Address dns : dnses) {
|
||||
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.
|
||||
|
||||
@@ -36,6 +36,7 @@ public final class NetworkConstants {
|
||||
*
|
||||
* See also:
|
||||
* - https://tools.ietf.org/html/rfc894
|
||||
* - https://tools.ietf.org/html/rfc2464
|
||||
* - https://tools.ietf.org/html/rfc7042
|
||||
* - http://www.iana.org/assignments/ethernet-numbers/ethernet-numbers.xhtml
|
||||
* - http://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml
|
||||
@@ -57,6 +58,8 @@ public final class NetworkConstants {
|
||||
FF, FF, FF, FF, FF, FF
|
||||
};
|
||||
|
||||
public static final int ETHER_MTU = 1500;
|
||||
|
||||
/**
|
||||
* ARP constants.
|
||||
*
|
||||
@@ -97,6 +100,7 @@ public final class NetworkConstants {
|
||||
public static final int IPV6_SRC_ADDR_OFFSET = 8;
|
||||
public static final int IPV6_DST_ADDR_OFFSET = 24;
|
||||
public static final int IPV6_ADDR_LEN = 16;
|
||||
public static final int IPV6_MIN_MTU = 1280;
|
||||
public static final int RFC7421_PREFIX_LENGTH = 64;
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,22 +16,43 @@
|
||||
|
||||
package com.android.server.connectivity;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Matchers.anyBoolean;
|
||||
import static org.mockito.Matchers.anyInt;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.hardware.usb.UsbManager;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.ConnectivityManager.NetworkCallback;
|
||||
import android.net.INetworkPolicyManager;
|
||||
import android.net.INetworkStatsService;
|
||||
import android.net.InterfaceConfiguration;
|
||||
import android.net.NetworkRequest;
|
||||
import android.net.wifi.WifiConfiguration;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.Handler;
|
||||
import android.os.INetworkManagementService;
|
||||
import android.os.PersistableBundle;
|
||||
import android.os.test.TestLooper;
|
||||
import android.os.UserHandle;
|
||||
import android.support.test.filters.SmallTest;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
import android.telephony.CarrierConfigManager;
|
||||
|
||||
import com.android.internal.util.test.BroadcastInterceptingContext;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -44,34 +65,60 @@ public class TetheringTest {
|
||||
private static final String[] PROVISIONING_APP_NAME = {"some", "app"};
|
||||
|
||||
@Mock private Context mContext;
|
||||
@Mock private ConnectivityManager mConnectivityManager;
|
||||
@Mock private INetworkManagementService mNMService;
|
||||
@Mock private INetworkStatsService mStatsService;
|
||||
@Mock private INetworkPolicyManager mPolicyManager;
|
||||
@Mock private MockableSystemProperties mSystemProperties;
|
||||
@Mock private Resources mResources;
|
||||
@Mock private UsbManager mUsbManager;
|
||||
@Mock private WifiManager mWifiManager;
|
||||
@Mock private CarrierConfigManager mCarrierConfigManager;
|
||||
|
||||
// Like so many Android system APIs, these cannot be mocked because it is marked final.
|
||||
// We have to use the real versions.
|
||||
private final PersistableBundle mCarrierConfig = new PersistableBundle();
|
||||
private final TestLooper mLooper = new TestLooper();
|
||||
private final String mTestIfname = "test_wlan0";
|
||||
|
||||
private BroadcastInterceptingContext mServiceContext;
|
||||
private Tethering mTethering;
|
||||
|
||||
private class MockContext extends BroadcastInterceptingContext {
|
||||
MockContext(Context base) {
|
||||
super(base);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resources getResources() { return mResources; }
|
||||
|
||||
@Override
|
||||
public Object getSystemService(String name) {
|
||||
if (Context.CONNECTIVITY_SERVICE.equals(name)) return mConnectivityManager;
|
||||
if (Context.WIFI_SERVICE.equals(name)) return mWifiManager;
|
||||
return super.getSystemService(name);
|
||||
}
|
||||
}
|
||||
|
||||
@Before public void setUp() throws Exception {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
when(mContext.getResources()).thenReturn(mResources);
|
||||
when(mResources.getStringArray(com.android.internal.R.array.config_tether_dhcp_range))
|
||||
.thenReturn(new String[0]);
|
||||
when(mResources.getStringArray(com.android.internal.R.array.config_tether_usb_regexs))
|
||||
.thenReturn(new String[0]);
|
||||
when(mResources.getStringArray(com.android.internal.R.array.config_tether_wifi_regexs))
|
||||
.thenReturn(new String[0]);
|
||||
.thenReturn(new String[]{ "test_wlan\\d" });
|
||||
when(mResources.getStringArray(com.android.internal.R.array.config_tether_bluetooth_regexs))
|
||||
.thenReturn(new String[0]);
|
||||
when(mResources.getIntArray(com.android.internal.R.array.config_tether_upstream_types))
|
||||
.thenReturn(new int[0]);
|
||||
mTethering = new Tethering(mContext, mNMService, mStatsService, mPolicyManager,
|
||||
when(mNMService.listInterfaces())
|
||||
.thenReturn(new String[]{ "test_rmnet_data0", mTestIfname });
|
||||
when(mNMService.getInterfaceConfig(anyString()))
|
||||
.thenReturn(new InterfaceConfiguration());
|
||||
|
||||
mServiceContext = new MockContext(mContext);
|
||||
mTethering = new Tethering(mServiceContext, mNMService, mStatsService, mPolicyManager,
|
||||
mLooper.getLooper(), mSystemProperties);
|
||||
}
|
||||
|
||||
@@ -126,4 +173,144 @@ public class TetheringTest {
|
||||
.thenReturn(new String[] {"malformedApp"});
|
||||
assertTrue(!mTethering.isTetherProvisioningRequired());
|
||||
}
|
||||
|
||||
private void sendWifiApStateChanged(int state) {
|
||||
final Intent intent = new Intent(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
|
||||
intent.putExtra(WifiManager.EXTRA_WIFI_AP_STATE, state);
|
||||
mServiceContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void workingLocalOnlyHotspot() throws Exception {
|
||||
when(mConnectivityManager.isTetheringSupported()).thenReturn(true);
|
||||
when(mWifiManager.setWifiApEnabled(any(WifiConfiguration.class), anyBoolean()))
|
||||
.thenReturn(true);
|
||||
|
||||
// Emulate externally-visible WifiManager effects, causing the
|
||||
// per-interface state machine to start up, and telling us that
|
||||
// hotspot mode is to be started.
|
||||
mTethering.interfaceStatusChanged(mTestIfname, true);
|
||||
sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_ENABLED);
|
||||
mLooper.dispatchAll();
|
||||
|
||||
verify(mNMService, times(1)).listInterfaces();
|
||||
verify(mNMService, times(1)).getInterfaceConfig(mTestIfname);
|
||||
verify(mNMService, times(1))
|
||||
.setInterfaceConfig(eq(mTestIfname), any(InterfaceConfiguration.class));
|
||||
verify(mNMService, times(1)).tetherInterface(mTestIfname);
|
||||
verify(mNMService, times(1)).setIpForwardingEnabled(true);
|
||||
verify(mNMService, times(1)).startTethering(any(String[].class));
|
||||
verifyNoMoreInteractions(mNMService);
|
||||
// UpstreamNetworkMonitor will be started, and will register two callbacks:
|
||||
// a "listen all" and a "track default".
|
||||
verify(mConnectivityManager, times(1)).registerNetworkCallback(
|
||||
any(NetworkRequest.class), any(NetworkCallback.class), any(Handler.class));
|
||||
verify(mConnectivityManager, times(1)).registerDefaultNetworkCallback(
|
||||
any(NetworkCallback.class), any(Handler.class));
|
||||
// TODO: Figure out why this isn't exactly once, for sendTetherStateChangedBroadcast().
|
||||
verify(mConnectivityManager, atLeastOnce()).isTetheringSupported();
|
||||
verifyNoMoreInteractions(mConnectivityManager);
|
||||
|
||||
// Emulate externally-visible WifiManager effects, when hotspot mode
|
||||
// is being torn down.
|
||||
sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
|
||||
mTethering.interfaceRemoved(mTestIfname);
|
||||
mLooper.dispatchAll();
|
||||
|
||||
verify(mNMService, times(1)).untetherInterface(mTestIfname);
|
||||
// TODO: Why is {g,s}etInterfaceConfig() called more than once?
|
||||
verify(mNMService, atLeastOnce()).getInterfaceConfig(mTestIfname);
|
||||
verify(mNMService, atLeastOnce())
|
||||
.setInterfaceConfig(eq(mTestIfname), any(InterfaceConfiguration.class));
|
||||
verify(mNMService, times(1)).stopTethering();
|
||||
verify(mNMService, times(1)).setIpForwardingEnabled(false);
|
||||
verifyNoMoreInteractions(mNMService);
|
||||
// Asking for the last error after the per-interface state machine
|
||||
// has been reaped yields an unknown interface error.
|
||||
assertEquals(ConnectivityManager.TETHER_ERROR_UNKNOWN_IFACE,
|
||||
mTethering.getLastTetherError(mTestIfname));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void workingWifiTethering() throws Exception {
|
||||
when(mConnectivityManager.isTetheringSupported()).thenReturn(true);
|
||||
when(mWifiManager.setWifiApEnabled(any(WifiConfiguration.class), anyBoolean()))
|
||||
.thenReturn(true);
|
||||
|
||||
// Emulate pressing the WiFi tethering button.
|
||||
mTethering.startTethering(ConnectivityManager.TETHERING_WIFI, null, false);
|
||||
mLooper.dispatchAll();
|
||||
verify(mWifiManager, times(1)).setWifiApEnabled(null, true);
|
||||
verifyNoMoreInteractions(mWifiManager);
|
||||
verifyNoMoreInteractions(mConnectivityManager);
|
||||
verifyNoMoreInteractions(mNMService);
|
||||
|
||||
// Emulate externally-visible WifiManager effects, causing the
|
||||
// per-interface state machine to start up, and telling us that
|
||||
// tethering mode is to be started.
|
||||
mTethering.interfaceStatusChanged(mTestIfname, true);
|
||||
sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_ENABLED);
|
||||
mLooper.dispatchAll();
|
||||
|
||||
verify(mNMService, times(1)).listInterfaces();
|
||||
verify(mNMService, times(1)).getInterfaceConfig(mTestIfname);
|
||||
verify(mNMService, times(1))
|
||||
.setInterfaceConfig(eq(mTestIfname), any(InterfaceConfiguration.class));
|
||||
verify(mNMService, times(1)).tetherInterface(mTestIfname);
|
||||
verify(mNMService, times(1)).setIpForwardingEnabled(true);
|
||||
verify(mNMService, times(1)).startTethering(any(String[].class));
|
||||
verifyNoMoreInteractions(mNMService);
|
||||
// UpstreamNetworkMonitor will be started, and will register two callbacks:
|
||||
// a "listen all" and a "track default".
|
||||
verify(mConnectivityManager, times(1)).registerNetworkCallback(
|
||||
any(NetworkRequest.class), any(NetworkCallback.class), any(Handler.class));
|
||||
verify(mConnectivityManager, times(1)).registerDefaultNetworkCallback(
|
||||
any(NetworkCallback.class), any(Handler.class));
|
||||
// In tethering mode, in the default configuration, an explicit request
|
||||
// for a mobile network is also made.
|
||||
verify(mConnectivityManager, atLeastOnce()).getNetworkInfo(anyInt());
|
||||
verify(mConnectivityManager, times(1)).requestNetwork(
|
||||
any(NetworkRequest.class), any(NetworkCallback.class), eq(0), anyInt(),
|
||||
any(Handler.class));
|
||||
// TODO: Figure out why this isn't exactly once, for sendTetherStateChangedBroadcast().
|
||||
verify(mConnectivityManager, atLeastOnce()).isTetheringSupported();
|
||||
verifyNoMoreInteractions(mConnectivityManager);
|
||||
|
||||
/////
|
||||
// We do not currently emulate any upstream being found.
|
||||
//
|
||||
// This is why there are no calls to verify mNMService.enableNat() or
|
||||
// mNMService.startInterfaceForwarding().
|
||||
/////
|
||||
|
||||
// Emulate pressing the WiFi tethering button.
|
||||
mTethering.stopTethering(ConnectivityManager.TETHERING_WIFI);
|
||||
mLooper.dispatchAll();
|
||||
verify(mWifiManager, times(1)).setWifiApEnabled(null, false);
|
||||
verifyNoMoreInteractions(mWifiManager);
|
||||
verifyNoMoreInteractions(mConnectivityManager);
|
||||
verifyNoMoreInteractions(mNMService);
|
||||
|
||||
// Emulate externally-visible WifiManager effects, when tethering mode
|
||||
// is being torn down.
|
||||
sendWifiApStateChanged(WifiManager.WIFI_AP_STATE_DISABLED);
|
||||
mTethering.interfaceRemoved(mTestIfname);
|
||||
mLooper.dispatchAll();
|
||||
|
||||
verify(mNMService, times(1)).untetherInterface(mTestIfname);
|
||||
// TODO: Why is {g,s}etInterfaceConfig() called more than once?
|
||||
verify(mNMService, atLeastOnce()).getInterfaceConfig(mTestIfname);
|
||||
verify(mNMService, atLeastOnce())
|
||||
.setInterfaceConfig(eq(mTestIfname), any(InterfaceConfiguration.class));
|
||||
verify(mNMService, times(1)).stopTethering();
|
||||
verify(mNMService, times(1)).setIpForwardingEnabled(false);
|
||||
verifyNoMoreInteractions(mNMService);
|
||||
// Asking for the last error after the per-interface state machine
|
||||
// has been reaped yields an unknown interface error.
|
||||
assertEquals(ConnectivityManager.TETHER_ERROR_UNKNOWN_IFACE,
|
||||
mTethering.getLastTetherError(mTestIfname));
|
||||
}
|
||||
|
||||
// TODO: Test that a request for hotspot mode doesn't interface with an
|
||||
// already operating tethering mode interface.
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import static android.net.ConnectivityManager.TETHERING_BLUETOOTH;
|
||||
import static android.net.ConnectivityManager.TETHERING_USB;
|
||||
import static android.net.ConnectivityManager.TETHERING_WIFI;
|
||||
import static com.android.server.connectivity.tethering.IControlsTethering.STATE_AVAILABLE;
|
||||
import static com.android.server.connectivity.tethering.IControlsTethering.STATE_LOCAL_HOTSPOT;
|
||||
import static com.android.server.connectivity.tethering.IControlsTethering.STATE_TETHERED;
|
||||
import static com.android.server.connectivity.tethering.IControlsTethering.STATE_UNAVAILABLE;
|
||||
|
||||
@@ -80,7 +81,7 @@ public class TetherInterfaceStateMachineTest {
|
||||
|
||||
private void initTetheredStateMachine(int interfaceType, String upstreamIface) throws Exception {
|
||||
initStateMachine(interfaceType);
|
||||
dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
|
||||
dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED, STATE_TETHERED);
|
||||
if (upstreamIface != null) {
|
||||
dispatchTetherConnectionChanged(upstreamIface);
|
||||
}
|
||||
@@ -138,7 +139,7 @@ public class TetherInterfaceStateMachineTest {
|
||||
public void canBeTethered() throws Exception {
|
||||
initStateMachine(TETHERING_BLUETOOTH);
|
||||
|
||||
dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
|
||||
dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED, STATE_TETHERED);
|
||||
InOrder inOrder = inOrder(mTetherHelper, mNMService);
|
||||
inOrder.verify(mNMService).tetherInterface(IFACE_NAME);
|
||||
inOrder.verify(mTetherHelper).notifyInterfaceStateChange(
|
||||
@@ -162,7 +163,7 @@ public class TetherInterfaceStateMachineTest {
|
||||
public void canBeTetheredAsUsb() throws Exception {
|
||||
initStateMachine(TETHERING_USB);
|
||||
|
||||
dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
|
||||
dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED, STATE_TETHERED);
|
||||
InOrder inOrder = inOrder(mTetherHelper, mNMService);
|
||||
inOrder.verify(mNMService).getInterfaceConfig(IFACE_NAME);
|
||||
inOrder.verify(mNMService).setInterfaceConfig(IFACE_NAME, mInterfaceConfiguration);
|
||||
@@ -272,7 +273,7 @@ public class TetherInterfaceStateMachineTest {
|
||||
initStateMachine(TETHERING_USB);
|
||||
|
||||
doThrow(RemoteException.class).when(mNMService).tetherInterface(IFACE_NAME);
|
||||
dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED);
|
||||
dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED, STATE_TETHERED);
|
||||
InOrder usbTeardownOrder = inOrder(mNMService, mInterfaceConfiguration, mTetherHelper);
|
||||
usbTeardownOrder.verify(mInterfaceConfiguration).setInterfaceDown();
|
||||
usbTeardownOrder.verify(mNMService).setInterfaceConfig(
|
||||
@@ -306,6 +307,17 @@ public class TetherInterfaceStateMachineTest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a command to the state machine under test, and run the event loop to idle.
|
||||
*
|
||||
* @param command One of the TetherInterfaceStateMachine.CMD_* constants.
|
||||
* @param obj An additional argument to pass.
|
||||
*/
|
||||
private void dispatchCommand(int command, int arg1) {
|
||||
mTestedSm.sendMessage(command, arg1);
|
||||
mLooper.dispatchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a command to the state machine under test, and run the event loop to idle.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user