WifiStateMachine expects CMD_START_DHCP to time out after 30-40 seconds. Currently, DhcpClient imposes timeouts on DhcpInitState and on DhcpRequestState, but not on the time it takes to get to from CMD_START_DHCP to DhcpBoundState. So in theory the client could oscillate between DhcpInitState and DhcpRequestState and never time out. Fix this by introducing a new oneshot timer that is set when DHCP starts and is cancelled when it succeeds. CMD_RENEW_DHCP does not need this because it's implemented using only one state, so the oneshot timeout can be implemented using the state timeout. Bug: 19704592 Change-Id: I6a5847a3dee23a2692237b8f3b3b0f8049da5140
898 lines
33 KiB
Java
898 lines
33 KiB
Java
/*
|
|
* Copyright (C) 2015 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 com.android.internal.util.HexDump;
|
|
import com.android.internal.util.Protocol;
|
|
import com.android.internal.util.State;
|
|
import com.android.internal.util.StateMachine;
|
|
|
|
import android.app.AlarmManager;
|
|
import android.app.PendingIntent;
|
|
import android.content.BroadcastReceiver;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.IntentFilter;
|
|
import android.net.DhcpResults;
|
|
import android.net.BaseDhcpStateMachine;
|
|
import android.net.DhcpStateMachine;
|
|
import android.net.InterfaceConfiguration;
|
|
import android.net.LinkAddress;
|
|
import android.net.NetworkUtils;
|
|
import android.os.IBinder;
|
|
import android.os.INetworkManagementService;
|
|
import android.os.Message;
|
|
import android.os.PowerManager;
|
|
import android.os.RemoteException;
|
|
import android.os.ServiceManager;
|
|
import android.os.SystemClock;
|
|
import android.system.ErrnoException;
|
|
import android.system.Os;
|
|
import android.system.PacketSocketAddress;
|
|
import android.util.Log;
|
|
import android.util.TimeUtils;
|
|
|
|
import java.io.FileDescriptor;
|
|
import java.io.IOException;
|
|
import java.lang.Thread;
|
|
import java.net.Inet4Address;
|
|
import java.net.InetSocketAddress;
|
|
import java.net.NetworkInterface;
|
|
import java.net.SocketException;
|
|
import java.nio.BufferUnderflowException;
|
|
import java.nio.ByteBuffer;
|
|
import java.util.Arrays;
|
|
import java.util.Random;
|
|
|
|
import libcore.io.IoBridge;
|
|
|
|
import static android.system.OsConstants.*;
|
|
import static android.net.dhcp.DhcpPacket.*;
|
|
|
|
/**
|
|
* A DHCPv4 client.
|
|
*
|
|
* Written to behave similarly to the DhcpStateMachine + dhcpcd 5.5.6 combination used in Android
|
|
* 5.1 and below, as configured on Nexus 6. The interface is the same as DhcpStateMachine.
|
|
*
|
|
* TODO:
|
|
*
|
|
* - Exponential backoff when receiving NAKs (not specified by the RFC, but current behaviour).
|
|
* - Support persisting lease state and support INIT-REBOOT. Android 5.1 does this, but it does not
|
|
* do so correctly: instead of requesting the lease last obtained on a particular network (e.g., a
|
|
* given SSID), it requests the last-leased IP address on the same interface, causing a delay if
|
|
* the server NAKs or a timeout if it doesn't.
|
|
*
|
|
* Known differences from current behaviour:
|
|
*
|
|
* - Does not request the "static routes" option.
|
|
* - Does not support BOOTP servers. DHCP has been around since 1993, should be everywhere now.
|
|
* - Requests the "broadcast" option, but does nothing with it.
|
|
* - Rejects invalid subnet masks such as 255.255.255.1 (current code treats that as 255.255.255.0).
|
|
*
|
|
* @hide
|
|
*/
|
|
public class DhcpClient extends BaseDhcpStateMachine {
|
|
|
|
private static final String TAG = "DhcpClient";
|
|
private static final boolean DBG = true;
|
|
private static final boolean STATE_DBG = false;
|
|
private static final boolean MSG_DBG = false;
|
|
private static final boolean PACKET_DBG = true;
|
|
|
|
// Timers and timeouts.
|
|
private static final int SECONDS = 1000;
|
|
private static final int FIRST_TIMEOUT_MS = 2 * SECONDS;
|
|
private static final int MAX_TIMEOUT_MS = 128 * SECONDS;
|
|
|
|
// This is not strictly needed, since the client is asynchronous and implements exponential
|
|
// backoff. It's maintained for backwards compatibility with the previous DHCP code, which was
|
|
// a blocking operation with a 30-second timeout. We pick 36 seconds so we can send packets at
|
|
// t=0, t=2, t=6, t=14, t=30, allowing for 10% jitter.
|
|
private static final int DHCP_TIMEOUT_MS = 36 * SECONDS;
|
|
|
|
// Messages.
|
|
private static final int BASE = Protocol.BASE_DHCP + 100;
|
|
private static final int CMD_KICK = BASE + 1;
|
|
private static final int CMD_RECEIVED_PACKET = BASE + 2;
|
|
private static final int CMD_TIMEOUT = BASE + 3;
|
|
private static final int CMD_ONESHOT_TIMEOUT = BASE + 4;
|
|
|
|
// DHCP parameters that we request.
|
|
private static final byte[] REQUESTED_PARAMS = new byte[] {
|
|
DHCP_SUBNET_MASK,
|
|
DHCP_ROUTER,
|
|
DHCP_DNS_SERVER,
|
|
DHCP_DOMAIN_NAME,
|
|
DHCP_MTU,
|
|
DHCP_BROADCAST_ADDRESS, // TODO: currently ignored.
|
|
DHCP_LEASE_TIME,
|
|
DHCP_RENEWAL_TIME,
|
|
DHCP_REBINDING_TIME,
|
|
};
|
|
|
|
// DHCP flag that means "yes, we support unicast."
|
|
private static final boolean DO_UNICAST = false;
|
|
|
|
// System services / libraries we use.
|
|
private final Context mContext;
|
|
private final AlarmManager mAlarmManager;
|
|
private final Random mRandom;
|
|
private final INetworkManagementService mNMService;
|
|
|
|
// Sockets.
|
|
// - We use a packet socket to receive, because servers send us packets bound for IP addresses
|
|
// which we have not yet configured, and the kernel protocol stack drops these.
|
|
// - We use a UDP socket to send, so the kernel handles ARP and routing for us (DHCP servers can
|
|
// be off-link as well as on-link).
|
|
private FileDescriptor mPacketSock;
|
|
private FileDescriptor mUdpSock;
|
|
private ReceiveThread mReceiveThread;
|
|
|
|
// State variables.
|
|
private final StateMachine mController;
|
|
private final PendingIntent mKickIntent;
|
|
private final PendingIntent mTimeoutIntent;
|
|
private final PendingIntent mRenewIntent;
|
|
private final PendingIntent mOneshotTimeoutIntent;
|
|
private final String mIfaceName;
|
|
|
|
private boolean mRegisteredForPreDhcpNotification;
|
|
private NetworkInterface mIface;
|
|
private byte[] mHwAddr;
|
|
private PacketSocketAddress mInterfaceBroadcastAddr;
|
|
private int mTransactionId;
|
|
private long mTransactionStartMillis;
|
|
private DhcpResults mDhcpLease;
|
|
private long mDhcpLeaseExpiry;
|
|
private DhcpResults mOffer;
|
|
|
|
// States.
|
|
private State mStoppedState = new StoppedState();
|
|
private State mDhcpState = new DhcpState();
|
|
private State mDhcpInitState = new DhcpInitState();
|
|
private State mDhcpSelectingState = new DhcpSelectingState();
|
|
private State mDhcpRequestingState = new DhcpRequestingState();
|
|
private State mDhcpHaveAddressState = new DhcpHaveAddressState();
|
|
private State mDhcpBoundState = new DhcpBoundState();
|
|
private State mDhcpRenewingState = new DhcpRenewingState();
|
|
private State mDhcpRebindingState = new DhcpRebindingState();
|
|
private State mDhcpInitRebootState = new DhcpInitRebootState();
|
|
private State mDhcpRebootingState = new DhcpRebootingState();
|
|
private State mWaitBeforeStartState = new WaitBeforeStartState(mDhcpInitState);
|
|
private State mWaitBeforeRenewalState = new WaitBeforeRenewalState(mDhcpRenewingState);
|
|
|
|
private DhcpClient(Context context, StateMachine controller, String iface) {
|
|
super(TAG);
|
|
|
|
mContext = context;
|
|
mController = controller;
|
|
mIfaceName = iface;
|
|
|
|
addState(mStoppedState);
|
|
addState(mDhcpState);
|
|
addState(mDhcpInitState, mDhcpState);
|
|
addState(mWaitBeforeStartState, mDhcpState);
|
|
addState(mDhcpSelectingState, mDhcpState);
|
|
addState(mDhcpRequestingState, mDhcpState);
|
|
addState(mDhcpHaveAddressState, mDhcpState);
|
|
addState(mDhcpBoundState, mDhcpHaveAddressState);
|
|
addState(mWaitBeforeRenewalState, mDhcpHaveAddressState);
|
|
addState(mDhcpRenewingState, mDhcpHaveAddressState);
|
|
addState(mDhcpRebindingState, mDhcpHaveAddressState);
|
|
addState(mDhcpInitRebootState, mDhcpState);
|
|
addState(mDhcpRebootingState, mDhcpState);
|
|
|
|
setInitialState(mStoppedState);
|
|
|
|
mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
|
|
IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE);
|
|
mNMService = INetworkManagementService.Stub.asInterface(b);
|
|
|
|
mRandom = new Random();
|
|
|
|
// Used to schedule packet retransmissions.
|
|
mKickIntent = createStateMachineCommandIntent("KICK", CMD_KICK);
|
|
// Used to time out PacketRetransmittingStates.
|
|
mTimeoutIntent = createStateMachineCommandIntent("TIMEOUT", CMD_TIMEOUT);
|
|
// Used to schedule DHCP renews.
|
|
mRenewIntent = createStateMachineCommandIntent("RENEW", DhcpStateMachine.CMD_RENEW_DHCP);
|
|
// Used to tell the caller when its request (CMD_START_DHCP or CMD_RENEW_DHCP) timed out.
|
|
// TODO: when the legacy DHCP client is gone, make the client fully asynchronous and
|
|
// remove this.
|
|
mOneshotTimeoutIntent = createStateMachineCommandIntent("ONESHOT_TIMEOUT",
|
|
CMD_ONESHOT_TIMEOUT);
|
|
}
|
|
|
|
@Override
|
|
public void registerForPreDhcpNotification() {
|
|
mRegisteredForPreDhcpNotification = true;
|
|
}
|
|
|
|
public static BaseDhcpStateMachine makeDhcpStateMachine(
|
|
Context context, StateMachine controller, String intf) {
|
|
DhcpClient client = new DhcpClient(context, controller, intf);
|
|
client.start();
|
|
return client;
|
|
}
|
|
|
|
/**
|
|
* Constructs a PendingIntent that sends the specified command to the state machine. This is
|
|
* implemented by creating an Intent with the specified parameters, and creating and registering
|
|
* a BroadcastReceiver for it. The broadcast must be sent by a process that holds the
|
|
* {@code CONNECTIVITY_INTERNAL} permission.
|
|
*
|
|
* @param cmdName the name of the command. The intent's action will be
|
|
* {@code android.net.dhcp.DhcpClient.<mIfaceName>.<cmdName>}
|
|
* @param cmd the command to send to the state machine when the PendingIntent is triggered.
|
|
* @return the PendingIntent
|
|
*/
|
|
private PendingIntent createStateMachineCommandIntent(final String cmdName, final int cmd) {
|
|
String action = DhcpClient.class.getName() + "." + mIfaceName + "." + cmdName;
|
|
|
|
Intent intent = new Intent(action, null)
|
|
.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
|
|
// TODO: The intent's package covers the whole of the system server, so it's pretty generic.
|
|
// Consider adding some sort of token as well.
|
|
intent.setPackage(mContext.getPackageName());
|
|
PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, cmd, intent, 0);
|
|
|
|
mContext.registerReceiver(
|
|
new BroadcastReceiver() {
|
|
@Override
|
|
public void onReceive(Context context, Intent intent) {
|
|
sendMessage(cmd);
|
|
}
|
|
},
|
|
new IntentFilter(action),
|
|
android.Manifest.permission.CONNECTIVITY_INTERNAL,
|
|
null);
|
|
|
|
return pendingIntent;
|
|
}
|
|
|
|
private boolean initInterface() {
|
|
try {
|
|
mIface = NetworkInterface.getByName(mIfaceName);
|
|
mHwAddr = mIface.getHardwareAddress();
|
|
mInterfaceBroadcastAddr = new PacketSocketAddress(mIface.getIndex(),
|
|
DhcpPacket.ETHER_BROADCAST);
|
|
return true;
|
|
} catch(SocketException e) {
|
|
Log.wtf(TAG, "Can't determine ifindex or MAC address for " + mIfaceName);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void startNewTransaction() {
|
|
mTransactionId = mRandom.nextInt();
|
|
mTransactionStartMillis = SystemClock.elapsedRealtime();
|
|
}
|
|
|
|
private boolean initSockets() {
|
|
try {
|
|
mPacketSock = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IP);
|
|
PacketSocketAddress addr = new PacketSocketAddress((short) ETH_P_IP, mIface.getIndex());
|
|
Os.bind(mPacketSock, addr);
|
|
NetworkUtils.attachDhcpFilter(mPacketSock);
|
|
} catch(SocketException|ErrnoException e) {
|
|
Log.e(TAG, "Error creating packet socket", e);
|
|
return false;
|
|
}
|
|
try {
|
|
mUdpSock = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
|
Os.setsockoptInt(mUdpSock, SOL_SOCKET, SO_REUSEADDR, 1);
|
|
Os.setsockoptIfreq(mUdpSock, SOL_SOCKET, SO_BINDTODEVICE, mIfaceName);
|
|
Os.setsockoptInt(mUdpSock, SOL_SOCKET, SO_BROADCAST, 1);
|
|
Os.bind(mUdpSock, Inet4Address.ANY, DhcpPacket.DHCP_CLIENT);
|
|
NetworkUtils.protectFromVpn(mUdpSock);
|
|
} catch(SocketException|ErrnoException e) {
|
|
Log.e(TAG, "Error creating UDP socket", e);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private static void closeQuietly(FileDescriptor fd) {
|
|
try {
|
|
IoBridge.closeAndSignalBlockedThreads(fd);
|
|
} catch (IOException ignored) {}
|
|
}
|
|
|
|
private void closeSockets() {
|
|
closeQuietly(mUdpSock);
|
|
closeQuietly(mPacketSock);
|
|
}
|
|
|
|
private boolean setIpAddress(LinkAddress address) {
|
|
InterfaceConfiguration ifcg = new InterfaceConfiguration();
|
|
ifcg.setLinkAddress(address);
|
|
try {
|
|
mNMService.setInterfaceConfig(mIfaceName, ifcg);
|
|
} catch (RemoteException|IllegalStateException e) {
|
|
Log.e(TAG, "Error configuring IP address : " + e);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
class ReceiveThread extends Thread {
|
|
|
|
private final byte[] mPacket = new byte[DhcpPacket.MAX_LENGTH];
|
|
private boolean stopped = false;
|
|
|
|
public void halt() {
|
|
stopped = true;
|
|
closeSockets(); // Interrupts the read() call the thread is blocked in.
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
maybeLog("Receive thread started");
|
|
while (!stopped) {
|
|
try {
|
|
int length = Os.read(mPacketSock, mPacket, 0, mPacket.length);
|
|
DhcpPacket packet = null;
|
|
packet = DhcpPacket.decodeFullPacket(mPacket, length, DhcpPacket.ENCAP_L2);
|
|
if (packet != null) {
|
|
maybeLog("Received packet: " + packet);
|
|
sendMessage(CMD_RECEIVED_PACKET, packet);
|
|
} else if (PACKET_DBG) {
|
|
Log.d(TAG,
|
|
"Can't parse packet" + HexDump.dumpHexString(mPacket, 0, length));
|
|
}
|
|
} catch (IOException|ErrnoException e) {
|
|
if (!stopped) {
|
|
Log.e(TAG, "Read error", e);
|
|
}
|
|
}
|
|
}
|
|
maybeLog("Receive thread stopped");
|
|
}
|
|
}
|
|
|
|
private short getSecs() {
|
|
return (short) ((SystemClock.elapsedRealtime() - mTransactionStartMillis) / 1000);
|
|
}
|
|
|
|
private boolean transmitPacket(ByteBuffer buf, String description, Inet4Address to) {
|
|
try {
|
|
if (to.equals(INADDR_BROADCAST)) {
|
|
maybeLog("Broadcasting " + description);
|
|
Os.sendto(mPacketSock, buf.array(), 0, buf.limit(), 0, mInterfaceBroadcastAddr);
|
|
} else {
|
|
maybeLog("Unicasting " + description + " to " + to.getHostAddress());
|
|
Os.sendto(mUdpSock, buf, 0, to, DhcpPacket.DHCP_SERVER);
|
|
}
|
|
} catch(ErrnoException|IOException e) {
|
|
Log.e(TAG, "Can't send packet: ", e);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private boolean sendDiscoverPacket() {
|
|
ByteBuffer packet = DhcpPacket.buildDiscoverPacket(
|
|
DhcpPacket.ENCAP_L2, mTransactionId, getSecs(), mHwAddr,
|
|
DO_UNICAST, REQUESTED_PARAMS);
|
|
return transmitPacket(packet, "DHCPDISCOVER", INADDR_BROADCAST);
|
|
}
|
|
|
|
private boolean sendRequestPacket(
|
|
Inet4Address clientAddress, Inet4Address requestedAddress,
|
|
Inet4Address serverAddress, Inet4Address to) {
|
|
// TODO: should we use the transaction ID from the server?
|
|
int encap = to.equals(INADDR_BROADCAST) ? DhcpPacket.ENCAP_L2 : DhcpPacket.ENCAP_BOOTP;
|
|
|
|
ByteBuffer packet = DhcpPacket.buildRequestPacket(
|
|
encap, mTransactionId, getSecs(), clientAddress,
|
|
DO_UNICAST, mHwAddr, requestedAddress,
|
|
serverAddress, REQUESTED_PARAMS, null);
|
|
String description = "DHCPREQUEST ciaddr=" + clientAddress.getHostAddress() +
|
|
" request=" + requestedAddress.getHostAddress() +
|
|
" to=" + serverAddress.getHostAddress();
|
|
return transmitPacket(packet, description, to);
|
|
}
|
|
|
|
private void scheduleRenew() {
|
|
mAlarmManager.cancel(mRenewIntent);
|
|
if (mDhcpLeaseExpiry != 0) {
|
|
long now = SystemClock.elapsedRealtime();
|
|
long alarmTime = (now + mDhcpLeaseExpiry) / 2;
|
|
mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, alarmTime, mRenewIntent);
|
|
Log.d(TAG, "Scheduling renewal in " + ((alarmTime - now) / 1000) + "s");
|
|
} else {
|
|
Log.d(TAG, "Infinite lease, no renewal needed");
|
|
}
|
|
}
|
|
|
|
private void notifySuccess() {
|
|
mController.sendMessage(DhcpStateMachine.CMD_POST_DHCP_ACTION,
|
|
DhcpStateMachine.DHCP_SUCCESS, 0, new DhcpResults(mDhcpLease));
|
|
}
|
|
|
|
private void notifyFailure() {
|
|
mController.sendMessage(DhcpStateMachine.CMD_POST_DHCP_ACTION,
|
|
DhcpStateMachine.DHCP_FAILURE, 0, null);
|
|
}
|
|
|
|
private void clearDhcpState() {
|
|
mDhcpLease = null;
|
|
mDhcpLeaseExpiry = 0;
|
|
mOffer = null;
|
|
}
|
|
|
|
/**
|
|
* Quit the DhcpStateMachine.
|
|
*
|
|
* @hide
|
|
*/
|
|
@Override
|
|
public void doQuit() {
|
|
Log.d(TAG, "doQuit");
|
|
quit();
|
|
}
|
|
|
|
protected void onQuitting() {
|
|
Log.d(TAG, "onQuitting");
|
|
mController.sendMessage(DhcpStateMachine.CMD_ON_QUIT);
|
|
}
|
|
|
|
private void maybeLog(String msg) {
|
|
if (DBG) Log.d(TAG, msg);
|
|
}
|
|
|
|
abstract class LoggingState extends State {
|
|
public void enter() {
|
|
if (STATE_DBG) Log.d(TAG, "Entering state " + getName());
|
|
}
|
|
|
|
private String messageName(int what) {
|
|
switch (what) {
|
|
case DhcpStateMachine.CMD_START_DHCP:
|
|
return "CMD_START_DHCP";
|
|
case DhcpStateMachine.CMD_STOP_DHCP:
|
|
return "CMD_STOP_DHCP";
|
|
case DhcpStateMachine.CMD_RENEW_DHCP:
|
|
return "CMD_RENEW_DHCP";
|
|
case DhcpStateMachine.CMD_PRE_DHCP_ACTION:
|
|
return "CMD_PRE_DHCP_ACTION";
|
|
case DhcpStateMachine.CMD_PRE_DHCP_ACTION_COMPLETE:
|
|
return "CMD_PRE_DHCP_ACTION_COMPLETE";
|
|
case DhcpStateMachine.CMD_POST_DHCP_ACTION:
|
|
return "CMD_POST_DHCP_ACTION";
|
|
case CMD_KICK:
|
|
return "CMD_KICK";
|
|
case CMD_RECEIVED_PACKET:
|
|
return "CMD_RECEIVED_PACKET";
|
|
case CMD_TIMEOUT:
|
|
return "CMD_TIMEOUT";
|
|
case CMD_ONESHOT_TIMEOUT:
|
|
return "CMD_ONESHOT_TIMEOUT";
|
|
default:
|
|
return Integer.toString(what);
|
|
}
|
|
}
|
|
|
|
private String messageToString(Message message) {
|
|
long now = SystemClock.uptimeMillis();
|
|
StringBuilder b = new StringBuilder(" ");
|
|
TimeUtils.formatDuration(message.getWhen() - now, b);
|
|
b.append(" ").append(messageName(message.what))
|
|
.append(" ").append(message.arg1)
|
|
.append(" ").append(message.arg2)
|
|
.append(" ").append(message.obj);
|
|
return b.toString();
|
|
}
|
|
|
|
@Override
|
|
public boolean processMessage(Message message) {
|
|
if (MSG_DBG) {
|
|
Log.d(TAG, getName() + messageToString(message));
|
|
}
|
|
return NOT_HANDLED;
|
|
}
|
|
}
|
|
|
|
// Sends CMD_PRE_DHCP_ACTION to the controller, waits for the controller to respond with
|
|
// CMD_PRE_DHCP_ACTION_COMPLETE, and then transitions to mOtherState.
|
|
abstract class WaitBeforeOtherState extends LoggingState {
|
|
protected State mOtherState;
|
|
|
|
@Override
|
|
public void enter() {
|
|
super.enter();
|
|
mController.sendMessage(DhcpStateMachine.CMD_PRE_DHCP_ACTION);
|
|
}
|
|
|
|
@Override
|
|
public boolean processMessage(Message message) {
|
|
super.processMessage(message);
|
|
switch (message.what) {
|
|
case DhcpStateMachine.CMD_PRE_DHCP_ACTION_COMPLETE:
|
|
transitionTo(mOtherState);
|
|
return HANDLED;
|
|
default:
|
|
return NOT_HANDLED;
|
|
}
|
|
}
|
|
}
|
|
|
|
// The one-shot timeout is used to implement the timeout for CMD_START_DHCP. We can't use a
|
|
// state timeout to do this because obtaining an IP address involves passing through more than
|
|
// one state (specifically, it passes at least once through DhcpInitState and once through
|
|
// DhcpRequestingState). The one-shot timeout is created when CMD_START_DHCP is received, and is
|
|
// cancelled when exiting DhcpState (either due to a CMD_STOP_DHCP, or because of an error), or
|
|
// when we get an IP address (when entering DhcpBoundState). If it fires, we send ourselves
|
|
// CMD_ONESHOT_TIMEOUT and notify the caller that DHCP failed, but we take no other action. For
|
|
// example, if we're in DhcpInitState and sending DISCOVERs, we continue to do so.
|
|
//
|
|
// The one-shot timeout is not used for CMD_RENEW_DHCP because that is implemented using only
|
|
// one state, so we can just use the state timeout.
|
|
private void scheduleOneshotTimeout() {
|
|
final long alarmTime = SystemClock.elapsedRealtime() + DHCP_TIMEOUT_MS;
|
|
mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, alarmTime,
|
|
mOneshotTimeoutIntent);
|
|
}
|
|
|
|
private void cancelOneshotTimeout() {
|
|
mAlarmManager.cancel(mOneshotTimeoutIntent);
|
|
}
|
|
|
|
class StoppedState extends LoggingState {
|
|
@Override
|
|
public boolean processMessage(Message message) {
|
|
super.processMessage(message);
|
|
switch (message.what) {
|
|
case DhcpStateMachine.CMD_START_DHCP:
|
|
scheduleOneshotTimeout();
|
|
if (mRegisteredForPreDhcpNotification) {
|
|
transitionTo(mWaitBeforeStartState);
|
|
} else {
|
|
transitionTo(mDhcpInitState);
|
|
}
|
|
return HANDLED;
|
|
default:
|
|
return NOT_HANDLED;
|
|
}
|
|
}
|
|
}
|
|
|
|
class WaitBeforeStartState extends WaitBeforeOtherState {
|
|
public WaitBeforeStartState(State otherState) {
|
|
super();
|
|
mOtherState = otherState;
|
|
}
|
|
}
|
|
|
|
class WaitBeforeRenewalState extends WaitBeforeOtherState {
|
|
public WaitBeforeRenewalState(State otherState) {
|
|
super();
|
|
mOtherState = otherState;
|
|
}
|
|
}
|
|
|
|
class DhcpState extends LoggingState {
|
|
@Override
|
|
public void enter() {
|
|
super.enter();
|
|
clearDhcpState();
|
|
if (initInterface() && initSockets()) {
|
|
mReceiveThread = new ReceiveThread();
|
|
mReceiveThread.start();
|
|
} else {
|
|
notifyFailure();
|
|
transitionTo(mStoppedState);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void exit() {
|
|
cancelOneshotTimeout();
|
|
mReceiveThread.halt(); // Also closes sockets.
|
|
clearDhcpState();
|
|
}
|
|
|
|
@Override
|
|
public boolean processMessage(Message message) {
|
|
super.processMessage(message);
|
|
switch (message.what) {
|
|
case DhcpStateMachine.CMD_STOP_DHCP:
|
|
transitionTo(mStoppedState);
|
|
return HANDLED;
|
|
case CMD_ONESHOT_TIMEOUT:
|
|
maybeLog("Timed out");
|
|
notifyFailure();
|
|
return HANDLED;
|
|
default:
|
|
return NOT_HANDLED;
|
|
}
|
|
}
|
|
}
|
|
|
|
public boolean isValidPacket(DhcpPacket packet) {
|
|
// TODO: check checksum.
|
|
int xid = packet.getTransactionId();
|
|
if (xid != mTransactionId) {
|
|
Log.d(TAG, "Unexpected transaction ID " + xid + ", expected " + mTransactionId);
|
|
return false;
|
|
}
|
|
if (!Arrays.equals(packet.getClientMac(), mHwAddr)) {
|
|
Log.d(TAG, "MAC addr mismatch: got " +
|
|
HexDump.toHexString(packet.getClientMac()) + ", expected " +
|
|
HexDump.toHexString(packet.getClientMac()));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public void setDhcpLeaseExpiry(DhcpPacket packet) {
|
|
long leaseTimeMillis = packet.getLeaseTimeMillis();
|
|
mDhcpLeaseExpiry =
|
|
(leaseTimeMillis > 0) ? SystemClock.elapsedRealtime() + leaseTimeMillis : 0;
|
|
}
|
|
|
|
/**
|
|
* Retransmits packets using jittered exponential backoff with an optional timeout. Packet
|
|
* transmission is triggered by CMD_KICK, which is sent by an AlarmManager alarm. If a subclass
|
|
* sets mTimeout to a positive value, then timeout() is called by an AlarmManager alarm mTimeout
|
|
* milliseconds after entering the state. Kicks and timeouts are cancelled when leaving the
|
|
* state.
|
|
*
|
|
* Concrete subclasses must implement sendPacket, which is called when the alarm fires and a
|
|
* packet needs to be transmitted, and receivePacket, which is triggered by CMD_RECEIVED_PACKET
|
|
* sent by the receive thread. They may also set mTimeout and implement timeout.
|
|
*/
|
|
abstract class PacketRetransmittingState extends LoggingState {
|
|
|
|
private int mTimer;
|
|
protected int mTimeout = 0;
|
|
|
|
@Override
|
|
public void enter() {
|
|
super.enter();
|
|
initTimer();
|
|
maybeInitTimeout();
|
|
sendMessage(CMD_KICK);
|
|
}
|
|
|
|
@Override
|
|
public boolean processMessage(Message message) {
|
|
super.processMessage(message);
|
|
switch (message.what) {
|
|
case CMD_KICK:
|
|
sendPacket();
|
|
scheduleKick();
|
|
return HANDLED;
|
|
case CMD_RECEIVED_PACKET:
|
|
receivePacket((DhcpPacket) message.obj);
|
|
return HANDLED;
|
|
case CMD_TIMEOUT:
|
|
timeout();
|
|
return HANDLED;
|
|
default:
|
|
return NOT_HANDLED;
|
|
}
|
|
}
|
|
|
|
public void exit() {
|
|
mAlarmManager.cancel(mKickIntent);
|
|
mAlarmManager.cancel(mTimeoutIntent);
|
|
}
|
|
|
|
abstract protected boolean sendPacket();
|
|
abstract protected void receivePacket(DhcpPacket packet);
|
|
protected void timeout() {}
|
|
|
|
protected void initTimer() {
|
|
mTimer = FIRST_TIMEOUT_MS;
|
|
}
|
|
|
|
protected int jitterTimer(int baseTimer) {
|
|
int maxJitter = baseTimer / 10;
|
|
int jitter = mRandom.nextInt(2 * maxJitter) - maxJitter;
|
|
return baseTimer + jitter;
|
|
}
|
|
|
|
protected void scheduleKick() {
|
|
long now = SystemClock.elapsedRealtime();
|
|
long timeout = jitterTimer(mTimer);
|
|
long alarmTime = now + timeout;
|
|
mAlarmManager.cancel(mKickIntent);
|
|
mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, alarmTime, mKickIntent);
|
|
mTimer *= 2;
|
|
if (mTimer > MAX_TIMEOUT_MS) {
|
|
mTimer = MAX_TIMEOUT_MS;
|
|
}
|
|
}
|
|
|
|
protected void maybeInitTimeout() {
|
|
if (mTimeout > 0) {
|
|
long alarmTime = SystemClock.elapsedRealtime() + mTimeout;
|
|
mAlarmManager.setExact(
|
|
AlarmManager.ELAPSED_REALTIME_WAKEUP, alarmTime, mTimeoutIntent);
|
|
}
|
|
}
|
|
}
|
|
|
|
class DhcpInitState extends PacketRetransmittingState {
|
|
public DhcpInitState() {
|
|
super();
|
|
}
|
|
|
|
@Override
|
|
public void enter() {
|
|
super.enter();
|
|
startNewTransaction();
|
|
}
|
|
|
|
protected boolean sendPacket() {
|
|
return sendDiscoverPacket();
|
|
}
|
|
|
|
protected void receivePacket(DhcpPacket packet) {
|
|
if (!isValidPacket(packet)) return;
|
|
if (!(packet instanceof DhcpOfferPacket)) return;
|
|
mOffer = packet.toDhcpResults();
|
|
if (mOffer != null) {
|
|
Log.d(TAG, "Got pending lease: " + mOffer);
|
|
transitionTo(mDhcpRequestingState);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Not implemented. We request the first offer we receive.
|
|
class DhcpSelectingState extends LoggingState {
|
|
}
|
|
|
|
class DhcpRequestingState extends PacketRetransmittingState {
|
|
public DhcpRequestingState() {
|
|
super();
|
|
mTimeout = DHCP_TIMEOUT_MS / 2;
|
|
}
|
|
|
|
protected boolean sendPacket() {
|
|
return sendRequestPacket(
|
|
INADDR_ANY, // ciaddr
|
|
(Inet4Address) mOffer.ipAddress.getAddress(), // DHCP_REQUESTED_IP
|
|
(Inet4Address) mOffer.serverAddress, // DHCP_SERVER_IDENTIFIER
|
|
INADDR_BROADCAST); // packet destination address
|
|
}
|
|
|
|
protected void receivePacket(DhcpPacket packet) {
|
|
if (!isValidPacket(packet)) return;
|
|
if ((packet instanceof DhcpAckPacket)) {
|
|
DhcpResults results = packet.toDhcpResults();
|
|
if (results != null) {
|
|
mDhcpLease = results;
|
|
mOffer = null;
|
|
Log.d(TAG, "Confirmed lease: " + mDhcpLease);
|
|
setDhcpLeaseExpiry(packet);
|
|
transitionTo(mDhcpBoundState);
|
|
}
|
|
} else if (packet instanceof DhcpNakPacket) {
|
|
Log.d(TAG, "Received NAK, returning to INIT");
|
|
mOffer = null;
|
|
transitionTo(mDhcpInitState);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void timeout() {
|
|
// After sending REQUESTs unsuccessfully for a while, go back to init.
|
|
transitionTo(mDhcpInitState);
|
|
}
|
|
}
|
|
|
|
class DhcpHaveAddressState extends LoggingState {
|
|
@Override
|
|
public void enter() {
|
|
super.enter();
|
|
if (setIpAddress(mDhcpLease.ipAddress)) {
|
|
maybeLog("Configured IP address " + mDhcpLease.ipAddress);
|
|
} else {
|
|
Log.e(TAG, "Failed to configure IP address " + mDhcpLease.ipAddress);
|
|
notifyFailure();
|
|
// There's likely no point in going into DhcpInitState here, we'll probably just
|
|
// repeat the transaction, get the same IP address as before, and fail.
|
|
transitionTo(mStoppedState);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void exit() {
|
|
maybeLog("Clearing IP address");
|
|
setIpAddress(new LinkAddress("0.0.0.0/0"));
|
|
}
|
|
}
|
|
|
|
class DhcpBoundState extends LoggingState {
|
|
@Override
|
|
public void enter() {
|
|
super.enter();
|
|
cancelOneshotTimeout();
|
|
notifySuccess();
|
|
// TODO: DhcpStateMachine only supports renewing at 50% of the lease time, and does not
|
|
// support rebinding. Once the legacy DHCP client is gone, fix this.
|
|
scheduleRenew();
|
|
}
|
|
|
|
@Override
|
|
public boolean processMessage(Message message) {
|
|
super.processMessage(message);
|
|
switch (message.what) {
|
|
case DhcpStateMachine.CMD_RENEW_DHCP:
|
|
if (mRegisteredForPreDhcpNotification) {
|
|
transitionTo(mWaitBeforeRenewalState);
|
|
} else {
|
|
transitionTo(mDhcpRenewingState);
|
|
}
|
|
return HANDLED;
|
|
default:
|
|
return NOT_HANDLED;
|
|
}
|
|
}
|
|
}
|
|
|
|
class DhcpRenewingState extends PacketRetransmittingState {
|
|
public DhcpRenewingState() {
|
|
super();
|
|
mTimeout = DHCP_TIMEOUT_MS;
|
|
}
|
|
|
|
@Override
|
|
public void enter() {
|
|
super.enter();
|
|
startNewTransaction();
|
|
}
|
|
|
|
protected boolean sendPacket() {
|
|
return sendRequestPacket(
|
|
(Inet4Address) mDhcpLease.ipAddress.getAddress(), // ciaddr
|
|
INADDR_ANY, // DHCP_REQUESTED_IP
|
|
INADDR_ANY, // DHCP_SERVER_IDENTIFIER
|
|
(Inet4Address) mDhcpLease.serverAddress); // packet destination address
|
|
}
|
|
|
|
protected void receivePacket(DhcpPacket packet) {
|
|
if (!isValidPacket(packet)) return;
|
|
if ((packet instanceof DhcpAckPacket)) {
|
|
setDhcpLeaseExpiry(packet);
|
|
transitionTo(mDhcpBoundState);
|
|
} else if (packet instanceof DhcpNakPacket) {
|
|
transitionTo(mDhcpInitState);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void timeout() {
|
|
transitionTo(mDhcpInitState);
|
|
sendMessage(CMD_ONESHOT_TIMEOUT);
|
|
}
|
|
}
|
|
|
|
// Not implemented. DhcpStateMachine does not implement it either.
|
|
class DhcpRebindingState extends LoggingState {
|
|
}
|
|
|
|
class DhcpInitRebootState extends LoggingState {
|
|
}
|
|
|
|
class DhcpRebootingState extends LoggingState {
|
|
}
|
|
}
|