Test: as follows
- built
- flashed
- booted
- runtest frameworks-net passes
- basic wifi usage nominal
Bug: 62476366
Change-Id: I9a74e001bc86972e31b7e0898711628e65d9cf7a
1035 lines
38 KiB
Java
1035 lines
38 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.MessageUtils;
|
|
import com.android.internal.util.StateMachine;
|
|
import com.android.internal.util.WakeupMessage;
|
|
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.IntentFilter;
|
|
import android.net.DhcpResults;
|
|
import android.net.InterfaceConfiguration;
|
|
import android.net.LinkAddress;
|
|
import android.net.NetworkUtils;
|
|
import android.net.TrafficStats;
|
|
import android.net.metrics.IpConnectivityLog;
|
|
import android.net.metrics.DhcpClientEvent;
|
|
import android.net.metrics.DhcpErrorEvent;
|
|
import android.net.util.InterfaceParams;
|
|
import android.os.Message;
|
|
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.EventLog;
|
|
import android.util.Log;
|
|
import android.util.SparseArray;
|
|
import android.util.TimeUtils;
|
|
|
|
import java.io.FileDescriptor;
|
|
import java.io.IOException;
|
|
import java.lang.Thread;
|
|
import java.net.Inet4Address;
|
|
import java.net.SocketException;
|
|
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 StateMachine {
|
|
|
|
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 = false;
|
|
|
|
// 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;
|
|
|
|
private static final int PUBLIC_BASE = Protocol.BASE_DHCP;
|
|
|
|
/* Commands from controller to start/stop DHCP */
|
|
public static final int CMD_START_DHCP = PUBLIC_BASE + 1;
|
|
public static final int CMD_STOP_DHCP = PUBLIC_BASE + 2;
|
|
|
|
/* Notification from DHCP state machine prior to DHCP discovery/renewal */
|
|
public static final int CMD_PRE_DHCP_ACTION = PUBLIC_BASE + 3;
|
|
/* Notification from DHCP state machine post DHCP discovery/renewal. Indicates
|
|
* success/failure */
|
|
public static final int CMD_POST_DHCP_ACTION = PUBLIC_BASE + 4;
|
|
/* Notification from DHCP state machine before quitting */
|
|
public static final int CMD_ON_QUIT = PUBLIC_BASE + 5;
|
|
|
|
/* Command from controller to indicate DHCP discovery/renewal can continue
|
|
* after pre DHCP action is complete */
|
|
public static final int CMD_PRE_DHCP_ACTION_COMPLETE = PUBLIC_BASE + 6;
|
|
|
|
/* Command and event notification to/from IpManager requesting the setting
|
|
* (or clearing) of an IPv4 LinkAddress.
|
|
*/
|
|
public static final int CMD_CLEAR_LINKADDRESS = PUBLIC_BASE + 7;
|
|
public static final int CMD_CONFIGURE_LINKADDRESS = PUBLIC_BASE + 8;
|
|
public static final int EVENT_LINKADDRESS_CONFIGURED = PUBLIC_BASE + 9;
|
|
|
|
/* Message.arg1 arguments to CMD_POST_DHCP_ACTION notification */
|
|
public static final int DHCP_SUCCESS = 1;
|
|
public static final int DHCP_FAILURE = 2;
|
|
|
|
// Internal messages.
|
|
private static final int PRIVATE_BASE = Protocol.BASE_DHCP + 100;
|
|
private static final int CMD_KICK = PRIVATE_BASE + 1;
|
|
private static final int CMD_RECEIVED_PACKET = PRIVATE_BASE + 2;
|
|
private static final int CMD_TIMEOUT = PRIVATE_BASE + 3;
|
|
private static final int CMD_RENEW_DHCP = PRIVATE_BASE + 4;
|
|
private static final int CMD_REBIND_DHCP = PRIVATE_BASE + 5;
|
|
private static final int CMD_EXPIRE_DHCP = PRIVATE_BASE + 6;
|
|
|
|
// For message logging.
|
|
private static final Class[] sMessageClasses = { DhcpClient.class };
|
|
private static final SparseArray<String> sMessageNames =
|
|
MessageUtils.findMessageNames(sMessageClasses);
|
|
|
|
// DHCP parameters that we request.
|
|
/* package */ 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_VENDOR_INFO,
|
|
};
|
|
|
|
// 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 Random mRandom;
|
|
private final IpConnectivityLog mMetricsLog = new IpConnectivityLog();
|
|
|
|
// 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 WakeupMessage mKickAlarm;
|
|
private final WakeupMessage mTimeoutAlarm;
|
|
private final WakeupMessage mRenewAlarm;
|
|
private final WakeupMessage mRebindAlarm;
|
|
private final WakeupMessage mExpiryAlarm;
|
|
private final String mIfaceName;
|
|
|
|
private boolean mRegisteredForPreDhcpNotification;
|
|
private InterfaceParams mIface;
|
|
// TODO: MacAddress-ify more of this class hierarchy.
|
|
private byte[] mHwAddr;
|
|
private PacketSocketAddress mInterfaceBroadcastAddr;
|
|
private int mTransactionId;
|
|
private long mTransactionStartMillis;
|
|
private DhcpResults mDhcpLease;
|
|
private long mDhcpLeaseExpiry;
|
|
private DhcpResults mOffer;
|
|
|
|
// Milliseconds SystemClock timestamps used to record transition times to DhcpBoundState.
|
|
private long mLastInitEnterTime;
|
|
private long mLastBoundExitTime;
|
|
|
|
// 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 mDhcpHaveLeaseState = new DhcpHaveLeaseState();
|
|
private State mConfiguringInterfaceState = new ConfiguringInterfaceState();
|
|
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 WakeupMessage makeWakeupMessage(String cmdName, int cmd) {
|
|
cmdName = DhcpClient.class.getSimpleName() + "." + mIfaceName + "." + cmdName;
|
|
return new WakeupMessage(mContext, getHandler(), cmdName, cmd);
|
|
}
|
|
|
|
// TODO: Take an InterfaceParams instance instead of an interface name String.
|
|
private DhcpClient(Context context, StateMachine controller, String iface) {
|
|
super(TAG, controller.getHandler());
|
|
|
|
mContext = context;
|
|
mController = controller;
|
|
mIfaceName = iface;
|
|
|
|
addState(mStoppedState);
|
|
addState(mDhcpState);
|
|
addState(mDhcpInitState, mDhcpState);
|
|
addState(mWaitBeforeStartState, mDhcpState);
|
|
addState(mDhcpSelectingState, mDhcpState);
|
|
addState(mDhcpRequestingState, mDhcpState);
|
|
addState(mDhcpHaveLeaseState, mDhcpState);
|
|
addState(mConfiguringInterfaceState, mDhcpHaveLeaseState);
|
|
addState(mDhcpBoundState, mDhcpHaveLeaseState);
|
|
addState(mWaitBeforeRenewalState, mDhcpHaveLeaseState);
|
|
addState(mDhcpRenewingState, mDhcpHaveLeaseState);
|
|
addState(mDhcpRebindingState, mDhcpHaveLeaseState);
|
|
addState(mDhcpInitRebootState, mDhcpState);
|
|
addState(mDhcpRebootingState, mDhcpState);
|
|
|
|
setInitialState(mStoppedState);
|
|
|
|
mRandom = new Random();
|
|
|
|
// Used to schedule packet retransmissions.
|
|
mKickAlarm = makeWakeupMessage("KICK", CMD_KICK);
|
|
// Used to time out PacketRetransmittingStates.
|
|
mTimeoutAlarm = makeWakeupMessage("TIMEOUT", CMD_TIMEOUT);
|
|
// Used to schedule DHCP reacquisition.
|
|
mRenewAlarm = makeWakeupMessage("RENEW", CMD_RENEW_DHCP);
|
|
mRebindAlarm = makeWakeupMessage("REBIND", CMD_REBIND_DHCP);
|
|
mExpiryAlarm = makeWakeupMessage("EXPIRY", CMD_EXPIRE_DHCP);
|
|
}
|
|
|
|
public void registerForPreDhcpNotification() {
|
|
mRegisteredForPreDhcpNotification = true;
|
|
}
|
|
|
|
public static DhcpClient makeDhcpClient(
|
|
Context context, StateMachine controller, InterfaceParams ifParams) {
|
|
DhcpClient client = new DhcpClient(context, controller, ifParams.name);
|
|
client.mIface = ifParams;
|
|
client.start();
|
|
return client;
|
|
}
|
|
|
|
private boolean initInterface() {
|
|
if (mIface == null) mIface = InterfaceParams.getByName(mIfaceName);
|
|
if (mIface == null) {
|
|
Log.e(TAG, "Can't determine InterfaceParams for " + mIfaceName);
|
|
return false;
|
|
}
|
|
|
|
mHwAddr = mIface.macAddr.toByteArray();
|
|
mInterfaceBroadcastAddr = new PacketSocketAddress(mIface.index, DhcpPacket.ETHER_BROADCAST);
|
|
return true;
|
|
}
|
|
|
|
private void startNewTransaction() {
|
|
mTransactionId = mRandom.nextInt();
|
|
mTransactionStartMillis = SystemClock.elapsedRealtime();
|
|
}
|
|
|
|
private boolean initSockets() {
|
|
return initPacketSocket() && initUdpSocket();
|
|
}
|
|
|
|
private boolean initPacketSocket() {
|
|
try {
|
|
mPacketSock = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IP);
|
|
PacketSocketAddress addr = new PacketSocketAddress((short) ETH_P_IP, mIface.index);
|
|
Os.bind(mPacketSock, addr);
|
|
NetworkUtils.attachDhcpFilter(mPacketSock);
|
|
} catch(SocketException|ErrnoException e) {
|
|
Log.e(TAG, "Error creating packet socket", e);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private boolean initUdpSocket() {
|
|
final int oldTag = TrafficStats.getAndSetThreadStatsTag(TrafficStats.TAG_SYSTEM_DHCP);
|
|
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.setsockoptInt(mUdpSock, SOL_SOCKET, SO_RCVBUF, 0);
|
|
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;
|
|
} finally {
|
|
TrafficStats.setThreadStatsTag(oldTag);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private boolean connectUdpSock(Inet4Address to) {
|
|
try {
|
|
Os.connect(mUdpSock, to, DhcpPacket.DHCP_SERVER);
|
|
return true;
|
|
} catch (SocketException|ErrnoException e) {
|
|
Log.e(TAG, "Error connecting UDP socket", e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static void closeQuietly(FileDescriptor fd) {
|
|
try {
|
|
IoBridge.closeAndSignalBlockedThreads(fd);
|
|
} catch (IOException ignored) {}
|
|
}
|
|
|
|
private void closeSockets() {
|
|
closeQuietly(mUdpSock);
|
|
closeQuietly(mPacketSock);
|
|
}
|
|
|
|
class ReceiveThread extends Thread {
|
|
|
|
private final byte[] mPacket = new byte[DhcpPacket.MAX_LENGTH];
|
|
private volatile boolean mStopped = false;
|
|
|
|
public void halt() {
|
|
mStopped = true;
|
|
closeSockets(); // Interrupts the read() call the thread is blocked in.
|
|
}
|
|
|
|
@Override
|
|
public void run() {
|
|
if (DBG) Log.d(TAG, "Receive thread started");
|
|
while (!mStopped) {
|
|
int length = 0; // Or compiler can't tell it's initialized if a parse error occurs.
|
|
try {
|
|
length = Os.read(mPacketSock, mPacket, 0, mPacket.length);
|
|
DhcpPacket packet = null;
|
|
packet = DhcpPacket.decodeFullPacket(mPacket, length, DhcpPacket.ENCAP_L2);
|
|
if (DBG) Log.d(TAG, "Received packet: " + packet);
|
|
sendMessage(CMD_RECEIVED_PACKET, packet);
|
|
} catch (IOException|ErrnoException e) {
|
|
if (!mStopped) {
|
|
Log.e(TAG, "Read error", e);
|
|
logError(DhcpErrorEvent.RECEIVE_ERROR);
|
|
}
|
|
} catch (DhcpPacket.ParseException e) {
|
|
Log.e(TAG, "Can't parse packet: " + e.getMessage());
|
|
if (PACKET_DBG) {
|
|
Log.d(TAG, HexDump.dumpHexString(mPacket, 0, length));
|
|
}
|
|
if (e.errorCode == DhcpErrorEvent.DHCP_NO_COOKIE) {
|
|
int snetTagId = 0x534e4554;
|
|
String bugId = "31850211";
|
|
int uid = -1;
|
|
String data = DhcpPacket.ParseException.class.getName();
|
|
EventLog.writeEvent(snetTagId, bugId, uid, data);
|
|
}
|
|
logError(e.errorCode);
|
|
}
|
|
}
|
|
if (DBG) Log.d(TAG, "Receive thread stopped");
|
|
}
|
|
}
|
|
|
|
private short getSecs() {
|
|
return (short) ((SystemClock.elapsedRealtime() - mTransactionStartMillis) / 1000);
|
|
}
|
|
|
|
private boolean transmitPacket(ByteBuffer buf, String description, int encap, Inet4Address to) {
|
|
try {
|
|
if (encap == DhcpPacket.ENCAP_L2) {
|
|
if (DBG) Log.d(TAG, "Broadcasting " + description);
|
|
Os.sendto(mPacketSock, buf.array(), 0, buf.limit(), 0, mInterfaceBroadcastAddr);
|
|
} else if (encap == DhcpPacket.ENCAP_BOOTP && to.equals(INADDR_BROADCAST)) {
|
|
if (DBG) Log.d(TAG, "Broadcasting " + description);
|
|
// We only send L3-encapped broadcasts in DhcpRebindingState,
|
|
// where we have an IP address and an unconnected UDP socket.
|
|
//
|
|
// N.B.: We only need this codepath because DhcpRequestPacket
|
|
// hardcodes the source IP address to 0.0.0.0. We could reuse
|
|
// the packet socket if this ever changes.
|
|
Os.sendto(mUdpSock, buf, 0, to, DhcpPacket.DHCP_SERVER);
|
|
} else {
|
|
// It's safe to call getpeername here, because we only send unicast packets if we
|
|
// have an IP address, and we connect the UDP socket in DhcpBoundState#enter.
|
|
if (DBG) Log.d(TAG, String.format("Unicasting %s to %s",
|
|
description, Os.getpeername(mUdpSock)));
|
|
Os.write(mUdpSock, buf);
|
|
}
|
|
} 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", DhcpPacket.ENCAP_L2, INADDR_BROADCAST);
|
|
}
|
|
|
|
private boolean sendRequestPacket(
|
|
Inet4Address clientAddress, Inet4Address requestedAddress,
|
|
Inet4Address serverAddress, Inet4Address to) {
|
|
// TODO: should we use the transaction ID from the server?
|
|
final int encap = INADDR_ANY.equals(clientAddress)
|
|
? DhcpPacket.ENCAP_L2 : DhcpPacket.ENCAP_BOOTP;
|
|
|
|
ByteBuffer packet = DhcpPacket.buildRequestPacket(
|
|
encap, mTransactionId, getSecs(), clientAddress,
|
|
DO_UNICAST, mHwAddr, requestedAddress,
|
|
serverAddress, REQUESTED_PARAMS, null);
|
|
String serverStr = (serverAddress != null) ? serverAddress.getHostAddress() : null;
|
|
String description = "DHCPREQUEST ciaddr=" + clientAddress.getHostAddress() +
|
|
" request=" + requestedAddress.getHostAddress() +
|
|
" serverid=" + serverStr;
|
|
return transmitPacket(packet, description, encap, to);
|
|
}
|
|
|
|
private void scheduleLeaseTimers() {
|
|
if (mDhcpLeaseExpiry == 0) {
|
|
Log.d(TAG, "Infinite lease, no timer scheduling needed");
|
|
return;
|
|
}
|
|
|
|
final long now = SystemClock.elapsedRealtime();
|
|
|
|
// TODO: consider getting the renew and rebind timers from T1 and T2.
|
|
// See also:
|
|
// https://tools.ietf.org/html/rfc2131#section-4.4.5
|
|
// https://tools.ietf.org/html/rfc1533#section-9.9
|
|
// https://tools.ietf.org/html/rfc1533#section-9.10
|
|
final long remainingDelay = mDhcpLeaseExpiry - now;
|
|
final long renewDelay = remainingDelay / 2;
|
|
final long rebindDelay = remainingDelay * 7 / 8;
|
|
mRenewAlarm.schedule(now + renewDelay);
|
|
mRebindAlarm.schedule(now + rebindDelay);
|
|
mExpiryAlarm.schedule(now + remainingDelay);
|
|
Log.d(TAG, "Scheduling renewal in " + (renewDelay / 1000) + "s");
|
|
Log.d(TAG, "Scheduling rebind in " + (rebindDelay / 1000) + "s");
|
|
Log.d(TAG, "Scheduling expiry in " + (remainingDelay / 1000) + "s");
|
|
}
|
|
|
|
private void notifySuccess() {
|
|
mController.sendMessage(
|
|
CMD_POST_DHCP_ACTION, DHCP_SUCCESS, 0, new DhcpResults(mDhcpLease));
|
|
}
|
|
|
|
private void notifyFailure() {
|
|
mController.sendMessage(CMD_POST_DHCP_ACTION, DHCP_FAILURE, 0, null);
|
|
}
|
|
|
|
private void acceptDhcpResults(DhcpResults results, String msg) {
|
|
mDhcpLease = results;
|
|
mOffer = null;
|
|
Log.d(TAG, msg + " lease: " + mDhcpLease);
|
|
notifySuccess();
|
|
}
|
|
|
|
private void clearDhcpState() {
|
|
mDhcpLease = null;
|
|
mDhcpLeaseExpiry = 0;
|
|
mOffer = null;
|
|
}
|
|
|
|
/**
|
|
* Quit the DhcpStateMachine.
|
|
*
|
|
* @hide
|
|
*/
|
|
public void doQuit() {
|
|
Log.d(TAG, "doQuit");
|
|
quit();
|
|
}
|
|
|
|
@Override
|
|
protected void onQuitting() {
|
|
Log.d(TAG, "onQuitting");
|
|
mController.sendMessage(CMD_ON_QUIT);
|
|
}
|
|
|
|
abstract class LoggingState extends State {
|
|
private long mEnterTimeMs;
|
|
|
|
@Override
|
|
public void enter() {
|
|
if (STATE_DBG) Log.d(TAG, "Entering state " + getName());
|
|
mEnterTimeMs = SystemClock.elapsedRealtime();
|
|
}
|
|
|
|
@Override
|
|
public void exit() {
|
|
long durationMs = SystemClock.elapsedRealtime() - mEnterTimeMs;
|
|
logState(getName(), (int) durationMs);
|
|
}
|
|
|
|
private String messageName(int what) {
|
|
return sMessageNames.get(what, 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;
|
|
}
|
|
|
|
@Override
|
|
public String getName() {
|
|
// All DhcpClient's states are inner classes with a well defined name.
|
|
// Use getSimpleName() and avoid super's getName() creating new String instances.
|
|
return getClass().getSimpleName();
|
|
}
|
|
}
|
|
|
|
// 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(CMD_PRE_DHCP_ACTION);
|
|
}
|
|
|
|
@Override
|
|
public boolean processMessage(Message message) {
|
|
super.processMessage(message);
|
|
switch (message.what) {
|
|
case CMD_PRE_DHCP_ACTION_COMPLETE:
|
|
transitionTo(mOtherState);
|
|
return HANDLED;
|
|
default:
|
|
return NOT_HANDLED;
|
|
}
|
|
}
|
|
}
|
|
|
|
class StoppedState extends State {
|
|
@Override
|
|
public boolean processMessage(Message message) {
|
|
switch (message.what) {
|
|
case CMD_START_DHCP:
|
|
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 State {
|
|
@Override
|
|
public void enter() {
|
|
clearDhcpState();
|
|
if (initInterface() && initSockets()) {
|
|
mReceiveThread = new ReceiveThread();
|
|
mReceiveThread.start();
|
|
} else {
|
|
notifyFailure();
|
|
transitionTo(mStoppedState);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void exit() {
|
|
if (mReceiveThread != null) {
|
|
mReceiveThread.halt(); // Also closes sockets.
|
|
mReceiveThread = null;
|
|
}
|
|
clearDhcpState();
|
|
}
|
|
|
|
@Override
|
|
public boolean processMessage(Message message) {
|
|
super.processMessage(message);
|
|
switch (message.what) {
|
|
case CMD_STOP_DHCP:
|
|
transitionTo(mStoppedState);
|
|
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;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void exit() {
|
|
super.exit();
|
|
mKickAlarm.cancel();
|
|
mTimeoutAlarm.cancel();
|
|
}
|
|
|
|
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;
|
|
mKickAlarm.schedule(alarmTime);
|
|
mTimer *= 2;
|
|
if (mTimer > MAX_TIMEOUT_MS) {
|
|
mTimer = MAX_TIMEOUT_MS;
|
|
}
|
|
}
|
|
|
|
protected void maybeInitTimeout() {
|
|
if (mTimeout > 0) {
|
|
long alarmTime = SystemClock.elapsedRealtime() + mTimeout;
|
|
mTimeoutAlarm.schedule(alarmTime);
|
|
}
|
|
}
|
|
}
|
|
|
|
class DhcpInitState extends PacketRetransmittingState {
|
|
public DhcpInitState() {
|
|
super();
|
|
}
|
|
|
|
@Override
|
|
public void enter() {
|
|
super.enter();
|
|
startNewTransaction();
|
|
mLastInitEnterTime = SystemClock.elapsedRealtime();
|
|
}
|
|
|
|
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() {
|
|
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) {
|
|
setDhcpLeaseExpiry(packet);
|
|
acceptDhcpResults(results, "Confirmed");
|
|
transitionTo(mConfiguringInterfaceState);
|
|
}
|
|
} else if (packet instanceof DhcpNakPacket) {
|
|
// TODO: Wait a while before returning into INIT state.
|
|
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 DhcpHaveLeaseState extends State {
|
|
@Override
|
|
public boolean processMessage(Message message) {
|
|
switch (message.what) {
|
|
case CMD_EXPIRE_DHCP:
|
|
Log.d(TAG, "Lease expired!");
|
|
notifyFailure();
|
|
transitionTo(mDhcpInitState);
|
|
return HANDLED;
|
|
default:
|
|
return NOT_HANDLED;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void exit() {
|
|
// Clear any extant alarms.
|
|
mRenewAlarm.cancel();
|
|
mRebindAlarm.cancel();
|
|
mExpiryAlarm.cancel();
|
|
clearDhcpState();
|
|
// Tell IpManager to clear the IPv4 address. There is no need to
|
|
// wait for confirmation since any subsequent packets are sent from
|
|
// INADDR_ANY anyway (DISCOVER, REQUEST).
|
|
mController.sendMessage(CMD_CLEAR_LINKADDRESS);
|
|
}
|
|
}
|
|
|
|
class ConfiguringInterfaceState extends LoggingState {
|
|
@Override
|
|
public void enter() {
|
|
super.enter();
|
|
mController.sendMessage(CMD_CONFIGURE_LINKADDRESS, mDhcpLease.ipAddress);
|
|
}
|
|
|
|
@Override
|
|
public boolean processMessage(Message message) {
|
|
super.processMessage(message);
|
|
switch (message.what) {
|
|
case EVENT_LINKADDRESS_CONFIGURED:
|
|
transitionTo(mDhcpBoundState);
|
|
return HANDLED;
|
|
default:
|
|
return NOT_HANDLED;
|
|
}
|
|
}
|
|
}
|
|
|
|
class DhcpBoundState extends LoggingState {
|
|
@Override
|
|
public void enter() {
|
|
super.enter();
|
|
if (mDhcpLease.serverAddress != null && !connectUdpSock(mDhcpLease.serverAddress)) {
|
|
// 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.
|
|
//
|
|
// NOTE: It is observed that connectUdpSock() basically never fails, due to
|
|
// SO_BINDTODEVICE. Examining the local socket address shows it will happily
|
|
// return an IPv4 address from another interface, or even return "0.0.0.0".
|
|
//
|
|
// TODO: Consider deleting this check, following testing on several kernels.
|
|
notifyFailure();
|
|
transitionTo(mStoppedState);
|
|
}
|
|
|
|
scheduleLeaseTimers();
|
|
logTimeToBoundState();
|
|
}
|
|
|
|
@Override
|
|
public void exit() {
|
|
super.exit();
|
|
mLastBoundExitTime = SystemClock.elapsedRealtime();
|
|
}
|
|
|
|
@Override
|
|
public boolean processMessage(Message message) {
|
|
super.processMessage(message);
|
|
switch (message.what) {
|
|
case CMD_RENEW_DHCP:
|
|
if (mRegisteredForPreDhcpNotification) {
|
|
transitionTo(mWaitBeforeRenewalState);
|
|
} else {
|
|
transitionTo(mDhcpRenewingState);
|
|
}
|
|
return HANDLED;
|
|
default:
|
|
return NOT_HANDLED;
|
|
}
|
|
}
|
|
|
|
private void logTimeToBoundState() {
|
|
long now = SystemClock.elapsedRealtime();
|
|
if (mLastBoundExitTime > mLastInitEnterTime) {
|
|
logState(DhcpClientEvent.RENEWING_BOUND, (int)(now - mLastBoundExitTime));
|
|
} else {
|
|
logState(DhcpClientEvent.INITIAL_BOUND, (int)(now - mLastInitEnterTime));
|
|
}
|
|
}
|
|
}
|
|
|
|
abstract class DhcpReacquiringState extends PacketRetransmittingState {
|
|
protected String mLeaseMsg;
|
|
|
|
@Override
|
|
public void enter() {
|
|
super.enter();
|
|
startNewTransaction();
|
|
}
|
|
|
|
abstract protected Inet4Address packetDestination();
|
|
|
|
protected boolean sendPacket() {
|
|
return sendRequestPacket(
|
|
(Inet4Address) mDhcpLease.ipAddress.getAddress(), // ciaddr
|
|
INADDR_ANY, // DHCP_REQUESTED_IP
|
|
null, // DHCP_SERVER_IDENTIFIER
|
|
packetDestination()); // packet destination address
|
|
}
|
|
|
|
protected void receivePacket(DhcpPacket packet) {
|
|
if (!isValidPacket(packet)) return;
|
|
if ((packet instanceof DhcpAckPacket)) {
|
|
final DhcpResults results = packet.toDhcpResults();
|
|
if (results != null) {
|
|
if (!mDhcpLease.ipAddress.equals(results.ipAddress)) {
|
|
Log.d(TAG, "Renewed lease not for our current IP address!");
|
|
notifyFailure();
|
|
transitionTo(mDhcpInitState);
|
|
}
|
|
setDhcpLeaseExpiry(packet);
|
|
// Updating our notion of DhcpResults here only causes the
|
|
// DNS servers and routes to be updated in LinkProperties
|
|
// in IpManager and by any overridden relevant handlers of
|
|
// the registered IpManager.Callback. IP address changes
|
|
// are not supported here.
|
|
acceptDhcpResults(results, mLeaseMsg);
|
|
transitionTo(mDhcpBoundState);
|
|
}
|
|
} else if (packet instanceof DhcpNakPacket) {
|
|
Log.d(TAG, "Received NAK, returning to INIT");
|
|
notifyFailure();
|
|
transitionTo(mDhcpInitState);
|
|
}
|
|
}
|
|
}
|
|
|
|
class DhcpRenewingState extends DhcpReacquiringState {
|
|
public DhcpRenewingState() {
|
|
mLeaseMsg = "Renewed";
|
|
}
|
|
|
|
@Override
|
|
public boolean processMessage(Message message) {
|
|
if (super.processMessage(message) == HANDLED) {
|
|
return HANDLED;
|
|
}
|
|
|
|
switch (message.what) {
|
|
case CMD_REBIND_DHCP:
|
|
transitionTo(mDhcpRebindingState);
|
|
return HANDLED;
|
|
default:
|
|
return NOT_HANDLED;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected Inet4Address packetDestination() {
|
|
// Not specifying a SERVER_IDENTIFIER option is a violation of RFC 2131, but...
|
|
// http://b/25343517 . Try to make things work anyway by using broadcast renews.
|
|
return (mDhcpLease.serverAddress != null) ?
|
|
mDhcpLease.serverAddress : INADDR_BROADCAST;
|
|
}
|
|
}
|
|
|
|
class DhcpRebindingState extends DhcpReacquiringState {
|
|
public DhcpRebindingState() {
|
|
mLeaseMsg = "Rebound";
|
|
}
|
|
|
|
@Override
|
|
public void enter() {
|
|
super.enter();
|
|
|
|
// We need to broadcast and possibly reconnect the socket to a
|
|
// completely different server.
|
|
closeQuietly(mUdpSock);
|
|
if (!initUdpSocket()) {
|
|
Log.e(TAG, "Failed to recreate UDP socket");
|
|
transitionTo(mDhcpInitState);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected Inet4Address packetDestination() {
|
|
return INADDR_BROADCAST;
|
|
}
|
|
}
|
|
|
|
class DhcpInitRebootState extends LoggingState {
|
|
}
|
|
|
|
class DhcpRebootingState extends LoggingState {
|
|
}
|
|
|
|
private void logError(int errorCode) {
|
|
mMetricsLog.log(mIfaceName, new DhcpErrorEvent(errorCode));
|
|
}
|
|
|
|
private void logState(String name, int durationMs) {
|
|
mMetricsLog.log(mIfaceName, new DhcpClientEvent(name, durationMs));
|
|
}
|
|
}
|