blob: d20e769d4e687593cf5e87a2172cfefccc61cf85 [file] [log] [blame]
/*
* 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 static android.net.dhcp.DhcpPacket.DHCP_BROADCAST_ADDRESS;
import static android.net.dhcp.DhcpPacket.DHCP_CAPTIVE_PORTAL;
import static android.net.dhcp.DhcpPacket.DHCP_DNS_SERVER;
import static android.net.dhcp.DhcpPacket.DHCP_DOMAIN_NAME;
import static android.net.dhcp.DhcpPacket.DHCP_IPV6_ONLY_PREFERRED;
import static android.net.dhcp.DhcpPacket.DHCP_LEASE_TIME;
import static android.net.dhcp.DhcpPacket.DHCP_MTU;
import static android.net.dhcp.DhcpPacket.DHCP_REBINDING_TIME;
import static android.net.dhcp.DhcpPacket.DHCP_RENEWAL_TIME;
import static android.net.dhcp.DhcpPacket.DHCP_ROUTER;
import static android.net.dhcp.DhcpPacket.DHCP_SUBNET_MASK;
import static android.net.dhcp.DhcpPacket.DHCP_VENDOR_INFO;
import static android.net.dhcp.DhcpPacket.INADDR_ANY;
import static android.net.dhcp.DhcpPacket.INADDR_BROADCAST;
import static android.net.dhcp.DhcpPacket.INFINITE_LEASE;
import static android.net.util.NetworkStackUtils.DHCP_INIT_REBOOT_VERSION;
import static android.net.util.NetworkStackUtils.DHCP_IPV6_ONLY_PREFERRED_VERSION;
import static android.net.util.NetworkStackUtils.DHCP_IP_CONFLICT_DETECT_VERSION;
import static android.net.util.NetworkStackUtils.DHCP_RAPID_COMMIT_VERSION;
import static android.net.util.NetworkStackUtils.closeSocketQuietly;
import static android.net.util.SocketUtils.makePacketSocketAddress;
import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
import static android.system.OsConstants.AF_INET;
import static android.system.OsConstants.AF_PACKET;
import static android.system.OsConstants.ETH_P_ARP;
import static android.system.OsConstants.ETH_P_IP;
import static android.system.OsConstants.IPPROTO_UDP;
import static android.system.OsConstants.SOCK_DGRAM;
import static android.system.OsConstants.SOCK_NONBLOCK;
import static android.system.OsConstants.SOCK_RAW;
import static android.system.OsConstants.SOL_SOCKET;
import static android.system.OsConstants.SO_BROADCAST;
import static android.system.OsConstants.SO_RCVBUF;
import static android.system.OsConstants.SO_REUSEADDR;
import static com.android.net.module.util.NetworkStackConstants.ARP_REQUEST;
import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY;
import static com.android.net.module.util.NetworkStackConstants.IPV4_CONFLICT_ANNOUNCE_NUM;
import static com.android.net.module.util.NetworkStackConstants.IPV4_CONFLICT_PROBE_NUM;
import android.content.Context;
import android.net.DhcpResults;
import android.net.InetAddresses;
import android.net.Layer2PacketParcelable;
import android.net.MacAddress;
import android.net.NetworkStackIpMemoryStore;
import android.net.TrafficStats;
import android.net.ip.IpClient;
import android.net.ipmemorystore.NetworkAttributes;
import android.net.ipmemorystore.OnNetworkAttributesRetrievedListener;
import android.net.ipmemorystore.OnStatusListener;
import android.net.metrics.DhcpClientEvent;
import android.net.metrics.DhcpErrorEvent;
import android.net.metrics.IpConnectivityLog;
import android.net.networkstack.aidl.dhcp.DhcpOption;
import android.net.util.HostnameTransliterator;
import android.net.util.InterfaceParams;
import android.net.util.NetworkStackUtils;
import android.net.util.SocketUtils;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.os.PowerManager;
import android.os.SystemClock;
import android.provider.Settings;
import android.stats.connectivity.DhcpFeature;
import android.system.ErrnoException;
import android.system.Os;
import android.util.EventLog;
import android.util.Log;
import android.util.SparseArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.HexDump;
import com.android.internal.util.MessageUtils;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.internal.util.WakeupMessage;
import com.android.net.module.util.DeviceConfigUtils;
import com.android.net.module.util.NetworkStackConstants;
import com.android.net.module.util.PacketReader;
import com.android.networkstack.R;
import com.android.networkstack.apishim.CaptivePortalDataShimImpl;
import com.android.networkstack.apishim.SocketUtilsShimImpl;
import com.android.networkstack.apishim.common.ShimUtils;
import com.android.networkstack.arp.ArpPacket;
import com.android.networkstack.metrics.IpProvisioningMetrics;
import java.io.ByteArrayOutputStream;
import java.io.FileDescriptor;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.SocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
/**
* 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 VDBG = Log.isLoggable(TAG, Log.VERBOSE);
private static final boolean STATE_DBG = Log.isLoggable(TAG, Log.DEBUG);
private static final boolean MSG_DBG = Log.isLoggable(TAG, Log.DEBUG);
private static final boolean PACKET_DBG = Log.isLoggable(TAG, Log.DEBUG);
// Metrics events: must be kept in sync with server-side aggregation code.
/** Represents transitions from DhcpInitState to DhcpBoundState */
private static final String EVENT_INITIAL_BOUND = "InitialBoundState";
/** Represents transitions from and to DhcpBoundState via DhcpRenewingState */
private static final String EVENT_RENEWING_BOUND = "RenewingBoundState";
// Timers and timeouts.
private static final int SECONDS = 1000;
private static final int FIRST_TIMEOUT_MS = 1 * SECONDS;
private static final int MAX_TIMEOUT_MS = 512 * SECONDS;
private static final int IPMEMORYSTORE_TIMEOUT_MS = 1 * SECONDS;
private static final int DHCP_INITREBOOT_TIMEOUT_MS = 5 * SECONDS;
// The waiting time to restart the DHCP configuration process after broadcasting a
// DHCPDECLINE message, (RFC2131 3.1.5 describes client SHOULD wait a minimum of 10
// seconds to avoid excessive traffic, but it's too long).
@VisibleForTesting
public static final String DHCP_RESTART_CONFIG_DELAY = "dhcp_restart_configuration_delay";
private static final int DEFAULT_DHCP_RESTART_CONFIG_DELAY_MS = 1 * SECONDS;
private static final int MAX_DHCP_CLIENT_RESTART_CONFIG_DELAY_MS = 10 * SECONDS;
// Initial random delay before sending first ARP probe.
@VisibleForTesting
public static final String ARP_FIRST_PROBE_DELAY_MS = "arp_first_probe_delay";
private static final int DEFAULT_ARP_FIRST_PROBE_DELAY_MS = 100;
private static final int MAX_ARP_FIRST_PROBE_DELAY_MS = 1 * SECONDS;
// Minimum delay until retransmitting the probe. The probe will be retransmitted after a
// random number of milliseconds in the range ARP_PROBE_MIN_MS and ARP_PROBE_MAX_MS.
@VisibleForTesting
public static final String ARP_PROBE_MIN_MS = "arp_probe_min";
private static final int DEFAULT_ARP_PROBE_MIN_MS = 100;
private static final int MAX_ARP_PROBE_MIN_MS = 1 * SECONDS;
// Maximum delay until retransmitting the probe.
@VisibleForTesting
public static final String ARP_PROBE_MAX_MS = "arp_probe_max";
private static final int DEFAULT_ARP_PROBE_MAX_MS = 300;
private static final int MAX_ARP_PROBE_MAX_MS = 2 * SECONDS;
// Initial random delay before sending first ARP Announcement after completing Probe packet
// transmission.
@VisibleForTesting
public static final String ARP_FIRST_ANNOUNCE_DELAY_MS = "arp_first_announce_delay";
private static final int DEFAULT_ARP_FIRST_ANNOUNCE_DELAY_MS = 100;
private static final int MAX_ARP_FIRST_ANNOUNCE_DELAY_MS = 2 * SECONDS;
// Time between retransmitting ARP Announcement packets.
@VisibleForTesting
public static final String ARP_ANNOUNCE_INTERVAL_MS = "arp_announce_interval";
private static final int DEFAULT_ARP_ANNOUNCE_INTERVAL_MS = 100;
private static final int MAX_ARP_ANNOUNCE_INTERVAL_MS = 2 * SECONDS;
// Max conflict count before configuring interface with declined IP address anyway.
private static final int MAX_CONFLICTS_COUNT = 2;
// 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 18 seconds so we can send packets at
// t=0, t=1, t=3, t=7, t=16, allowing for 10% jitter.
private static final int DHCP_TIMEOUT_MS = 36 * SECONDS;
// DhcpClient uses IpClient's handler.
private static final int PUBLIC_BASE = IpClient.DHCPCLIENT_CMD_BASE;
// Below constants are picked up by MessageUtils and exempt from ProGuard optimization.
/* 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;
// Command to IpClient starting/aborting preconnection process.
public static final int CMD_START_PRECONNECTION = PUBLIC_BASE + 10;
public static final int CMD_ABORT_PRECONNECTION = PUBLIC_BASE + 11;
// Command to rebind the leased IPv4 address on L2 roaming happened.
public static final int CMD_REFRESH_LINKADDRESS = PUBLIC_BASE + 12;
/* Message.arg1 arguments to CMD_POST_DHCP_ACTION notification */
public static final int DHCP_SUCCESS = 1;
public static final int DHCP_FAILURE = 2;
public static final int DHCP_IPV6_ONLY = 3;
// Internal messages.
private static final int PRIVATE_BASE = IpClient.DHCPCLIENT_CMD_BASE + 100;
private static final int CMD_KICK = PRIVATE_BASE + 1;
private static final int CMD_RECEIVED_PACKET = PRIVATE_BASE + 2;
@VisibleForTesting
public 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;
private static final int EVENT_CONFIGURATION_TIMEOUT = PRIVATE_BASE + 7;
private static final int EVENT_CONFIGURATION_OBTAINED = PRIVATE_BASE + 8;
private static final int EVENT_CONFIGURATION_INVALID = PRIVATE_BASE + 9;
private static final int EVENT_IP_CONFLICT = PRIVATE_BASE + 10;
private static final int CMD_ARP_PROBE = PRIVATE_BASE + 11;
private static final int CMD_ARP_ANNOUNCEMENT = PRIVATE_BASE + 12;
// constant to represent this DHCP lease has been expired.
@VisibleForTesting
public static final long EXPIRED_LEASE = 1L;
// For message logging.
private static final Class[] sMessageClasses = { DhcpClient.class };
private static final SparseArray<String> sMessageNames =
MessageUtils.findMessageNames(sMessageClasses);
// DHCP parameters that we request by default.
@VisibleForTesting
/* package */ static final byte[] DEFAULT_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,
};
@NonNull
private byte[] getRequestedParams() {
// Set an initial size large enough for all optional parameters that we might request.
final int numOptionalParams = 2;
final ByteArrayOutputStream params =
new ByteArrayOutputStream(DEFAULT_REQUESTED_PARAMS.length + numOptionalParams);
params.write(DEFAULT_REQUESTED_PARAMS, 0, DEFAULT_REQUESTED_PARAMS.length);
if (isCapportApiEnabled()) {
params.write(DHCP_CAPTIVE_PORTAL);
}
if (isIPv6OnlyPreferredModeEnabled()) {
params.write(DHCP_IPV6_ONLY_PREFERRED);
}
// Customized DHCP options to be put in PRL.
for (DhcpOption option : mConfiguration.options) {
if (option.value == null) params.write(option.type);
}
return params.toByteArray();
}
private static boolean isCapportApiEnabled() {
return CaptivePortalDataShimImpl.isSupported();
}
// 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();
@NonNull
private final IpProvisioningMetrics mMetrics;
// 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 mUdpSock;
// 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 SocketAddress mInterfaceBroadcastAddr;
private int mTransactionId;
private long mTransactionStartMillis;
private DhcpResults mDhcpLease;
private long mDhcpLeaseExpiry;
private DhcpResults mOffer;
private Configuration mConfiguration;
private Inet4Address mLastAssignedIpv4Address;
private int mConflictCount;
private long mLastAssignedIpv4AddressExpiry;
private Dependencies mDependencies;
@Nullable
private DhcpPacketHandler mDhcpPacketHandler;
@NonNull
private final NetworkStackIpMemoryStore mIpMemoryStore;
@Nullable
private final String mHostname;
// Milliseconds SystemClock timestamps used to record transition times to DhcpBoundState.
private long mLastInitEnterTime;
private long mLastBoundExitTime;
// 32-bit unsigned integer used to indicate the number of milliseconds the DHCP client should
// disable DHCPv4.
private long mIpv6OnlyWaitTimeMs;
// States.
private State mStoppedState = new StoppedState();
private State mDhcpState = new DhcpState();
private State mDhcpInitState = new DhcpInitState();
private State mDhcpPreconnectingState = new DhcpPreconnectingState();
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 mObtainingConfigurationState = new ObtainingConfigurationState();
private State mWaitBeforeStartState = new WaitBeforeStartState(mDhcpInitState);
private State mWaitBeforeRenewalState = new WaitBeforeRenewalState(mDhcpRenewingState);
private State mWaitBeforeObtainingConfigurationState =
new WaitBeforeObtainingConfigurationState(mObtainingConfigurationState);
private State mIpAddressConflictDetectingState = new IpAddressConflictDetectingState();
private State mDhcpDecliningState = new DhcpDecliningState();
private State mIpv6OnlyWaitState = new Ipv6OnlyWaitState();
private WakeupMessage makeWakeupMessage(String cmdName, int cmd) {
cmdName = DhcpClient.class.getSimpleName() + "." + mIfaceName + "." + cmdName;
return new WakeupMessage(mContext, getHandler(), cmdName, cmd);
}
/**
* Encapsulates DhcpClient depencencies that's used for unit testing and
* integration testing.
*/
public static class Dependencies {
private final NetworkStackIpMemoryStore mNetworkStackIpMemoryStore;
private final IpProvisioningMetrics mMetrics;
public Dependencies(NetworkStackIpMemoryStore store, IpProvisioningMetrics metrics) {
mNetworkStackIpMemoryStore = store;
mMetrics = metrics;
}
/**
* Get the configuration from RRO to check whether or not to send hostname option in
* DHCPDISCOVER/DHCPREQUEST message.
*/
public boolean getSendHostnameOption(final Context context) {
return context.getResources().getBoolean(R.bool.config_dhcp_client_hostname);
}
/**
* Get the device name from system settings.
*/
public String getDeviceName(final Context context) {
return Settings.Global.getString(context.getContentResolver(),
Settings.Global.DEVICE_NAME);
}
/**
* Get a IpMemoryStore instance.
*/
public NetworkStackIpMemoryStore getIpMemoryStore() {
return mNetworkStackIpMemoryStore;
}
/**
* Get a IpProvisioningMetrics instance.
*/
public IpProvisioningMetrics getIpProvisioningMetrics() {
return mMetrics;
}
/**
* Return whether a feature guarded by a feature flag is enabled.
* @see DeviceConfigUtils#isFeatureEnabled(Context, String, String)
*/
public boolean isFeatureEnabled(final Context context, final String name,
boolean defaultEnabled) {
return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name,
defaultEnabled);
}
/**
* Get the Integer value of relevant DeviceConfig properties of Connectivity namespace.
*/
public int getIntDeviceConfig(final String name, int minimumValue, int maximumValue,
int defaultValue) {
return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY,
name, minimumValue, maximumValue, defaultValue);
}
/**
* Get a new wake lock to force CPU keeping awake when transmitting packets or waiting
* for timeout.
*/
public PowerManager.WakeLock getWakeLock(final PowerManager powerManager) {
return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
}
}
// TODO: Take an InterfaceParams instance instead of an interface name String.
private DhcpClient(Context context, StateMachine controller, String iface,
Dependencies deps) {
super(TAG, controller.getHandler());
mDependencies = deps;
mContext = context;
mController = controller;
mIfaceName = iface;
mIpMemoryStore = deps.getIpMemoryStore();
mMetrics = deps.getIpProvisioningMetrics();
// CHECKSTYLE:OFF IndentationCheck
addState(mStoppedState);
addState(mDhcpState);
addState(mDhcpInitState, mDhcpState);
addState(mWaitBeforeStartState, mDhcpState);
addState(mWaitBeforeObtainingConfigurationState, mDhcpState);
addState(mDhcpPreconnectingState, mDhcpState);
addState(mObtainingConfigurationState, mDhcpState);
addState(mDhcpSelectingState, mDhcpState);
addState(mDhcpRequestingState, mDhcpState);
addState(mIpAddressConflictDetectingState, mDhcpState);
addState(mIpv6OnlyWaitState, mDhcpState);
addState(mDhcpHaveLeaseState, mDhcpState);
addState(mConfiguringInterfaceState, mDhcpHaveLeaseState);
addState(mDhcpBoundState, mDhcpHaveLeaseState);
addState(mWaitBeforeRenewalState, mDhcpHaveLeaseState);
addState(mDhcpRenewingState, mDhcpHaveLeaseState);
addState(mDhcpRebindingState, mDhcpHaveLeaseState);
addState(mDhcpDecliningState, mDhcpHaveLeaseState);
addState(mDhcpInitRebootState, mDhcpState);
addState(mDhcpRebootingState, mDhcpState);
// CHECKSTYLE:ON IndentationCheck
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);
// Transliterate hostname read from system settings if RRO option is enabled.
final boolean sendHostname = deps.getSendHostnameOption(context);
mHostname = sendHostname ? new HostnameTransliterator().transliterate(
deps.getDeviceName(mContext)) : null;
mMetrics.setHostnameTransinfo(sendHostname, mHostname != null);
}
public void registerForPreDhcpNotification() {
mRegisteredForPreDhcpNotification = true;
}
public static DhcpClient makeDhcpClient(
Context context, StateMachine controller, InterfaceParams ifParams,
Dependencies deps) {
DhcpClient client = new DhcpClient(context, controller, ifParams.name, deps);
client.mIface = ifParams;
client.start();
return client;
}
/**
* check whether or not to support caching the last lease info and INIT-REBOOT state.
*
* INIT-REBOOT state is supported on Android R by default if there is no experiment flag set to
* disable this feature explicitly, meanwhile turning this feature on/off by pushing experiment
* flag makes it possible to do A/B test and metrics collection on both of Android Q and R, but
* it's disabled on Android Q by default.
*/
public boolean isDhcpLeaseCacheEnabled() {
final boolean defaultEnabled =
ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q);
return mDependencies.isFeatureEnabled(mContext, DHCP_INIT_REBOOT_VERSION, defaultEnabled);
}
/**
* check whether or not to support DHCP Rapid Commit option.
*/
public boolean isDhcpRapidCommitEnabled() {
return mDependencies.isFeatureEnabled(mContext, DHCP_RAPID_COMMIT_VERSION,
false /* defaultEnabled */);
}
/**
* check whether or not to support IP address conflict detection and DHCPDECLINE.
*/
public boolean isDhcpIpConflictDetectEnabled() {
return mDependencies.isFeatureEnabled(mContext, DHCP_IP_CONFLICT_DETECT_VERSION,
false /* defaultEnabled */);
}
/**
* check whether or not to support IPv6-only preferred option.
*
* IPv6-only preferred option is enabled by default if there is no experiment flag set to
* disable this feature explicitly.
*/
public boolean isIPv6OnlyPreferredModeEnabled() {
return mDependencies.isFeatureEnabled(mContext, DHCP_IPV6_ONLY_PREFERRED_VERSION,
true /* defaultEnabled */);
}
private void recordMetricEnabledFeatures() {
if (isDhcpLeaseCacheEnabled()) mMetrics.setDhcpEnabledFeature(DhcpFeature.DF_INITREBOOT);
if (isDhcpRapidCommitEnabled()) mMetrics.setDhcpEnabledFeature(DhcpFeature.DF_RAPIDCOMMIT);
if (isDhcpIpConflictDetectEnabled()) mMetrics.setDhcpEnabledFeature(DhcpFeature.DF_DAD);
if (mConfiguration.isPreconnectionEnabled) {
mMetrics.setDhcpEnabledFeature(DhcpFeature.DF_FILS);
}
}
private void confirmDhcpLease(DhcpPacket packet, DhcpResults results) {
setDhcpLeaseExpiry(packet);
acceptDhcpResults(results, "Confirmed");
}
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 = SocketUtilsShimImpl.newInstance().makePacketSocketAddress(
ETH_P_IP, mIface.index, DhcpPacket.ETHER_BROADCAST);
return true;
}
private void startNewTransaction() {
mTransactionId = mRandom.nextInt();
mTransactionStartMillis = SystemClock.elapsedRealtime();
}
private boolean initUdpSocket() {
final int oldTag = TrafficStats.getAndSetThreadStatsTag(
NetworkStackConstants.TAG_SYSTEM_DHCP);
try {
mUdpSock = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
SocketUtils.bindSocketToInterface(mUdpSock, mIfaceName);
Os.setsockoptInt(mUdpSock, SOL_SOCKET, SO_REUSEADDR, 1);
Os.setsockoptInt(mUdpSock, SOL_SOCKET, SO_BROADCAST, 1);
Os.setsockoptInt(mUdpSock, SOL_SOCKET, SO_RCVBUF, 0);
Os.bind(mUdpSock, IPV4_ADDR_ANY, DhcpPacket.DHCP_CLIENT);
} 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 byte[] getOptionsToSkip() {
final ByteArrayOutputStream optionsToSkip = new ByteArrayOutputStream(2);
if (!isCapportApiEnabled()) optionsToSkip.write(DHCP_CAPTIVE_PORTAL);
if (!isIPv6OnlyPreferredModeEnabled()) optionsToSkip.write(DHCP_IPV6_ONLY_PREFERRED);
return optionsToSkip.toByteArray();
}
private class DhcpPacketHandler extends PacketReader {
private FileDescriptor mPacketSock;
DhcpPacketHandler(Handler handler) {
super(handler);
}
@Override
protected void handlePacket(byte[] recvbuf, int length) {
try {
final DhcpPacket packet = DhcpPacket.decodeFullPacket(recvbuf, length,
DhcpPacket.ENCAP_L2, getOptionsToSkip());
if (DBG) Log.d(TAG, "Received packet: " + packet);
sendMessage(CMD_RECEIVED_PACKET, packet);
} catch (DhcpPacket.ParseException e) {
Log.e(TAG, "Can't parse packet: " + e.getMessage());
if (PACKET_DBG) {
Log.d(TAG, HexDump.dumpHexString(recvbuf, 0, length));
}
if (e.errorCode == DhcpErrorEvent.DHCP_NO_COOKIE) {
final int snetTagId = 0x534e4554;
final String bugId = "31850211";
final int uid = -1;
final String data = DhcpPacket.ParseException.class.getName();
EventLog.writeEvent(snetTagId, bugId, uid, data);
}
mMetricsLog.log(mIfaceName, new DhcpErrorEvent(e.errorCode));
mMetrics.addDhcpErrorCode(e.errorCode);
}
}
@Override
protected FileDescriptor createFd() {
try {
mPacketSock = Os.socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, 0 /* protocol */);
NetworkStackUtils.attachDhcpFilter(mPacketSock);
final SocketAddress addr = makePacketSocketAddress(ETH_P_IP, mIface.index);
Os.bind(mPacketSock, addr);
} catch (SocketException | ErrnoException e) {
logError("Error creating packet socket", e);
closeFd(mPacketSock);
mPacketSock = null;
return null;
}
return mPacketSock;
}
@Override
protected int readPacket(FileDescriptor fd, byte[] packetBuffer) throws Exception {
try {
return Os.read(fd, packetBuffer, 0, packetBuffer.length);
} catch (IOException | ErrnoException e) {
mMetricsLog.log(mIfaceName, new DhcpErrorEvent(DhcpErrorEvent.RECEIVE_ERROR));
throw e;
}
}
@Override
protected void logError(@NonNull String msg, @Nullable Exception e) {
Log.e(TAG, msg, e);
}
public int transmitPacket(final ByteBuffer buf, final SocketAddress socketAddress)
throws ErrnoException, SocketException {
return Os.sendto(mPacketSock, buf.array(), 0 /* byteOffset */,
buf.limit() /* byteCount */, 0 /* flags */, socketAddress);
}
}
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);
mDhcpPacketHandler.transmitPacket(buf, 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() {
// When Rapid Commit option is enabled, limit only the first 3 DHCPDISCOVER packets
// taking Rapid Commit option, in order to prevent the potential interoperability issue
// and be able to rollback later. See {@link DHCP_TIMEOUT_MS} for the (re)transmission
// schedule with 10% jitter.
final boolean requestRapidCommit = isDhcpRapidCommitEnabled() && (getSecs() <= 4);
final ByteBuffer packet = DhcpPacket.buildDiscoverPacket(
DhcpPacket.ENCAP_L2, mTransactionId, getSecs(), mHwAddr,
DO_UNICAST, getRequestedParams(), requestRapidCommit, mHostname,
mConfiguration.options);
mMetrics.incrementCountForDiscover();
return transmitPacket(packet, "DHCPDISCOVER", DhcpPacket.ENCAP_L2, INADDR_BROADCAST);
}
private boolean sendRequestPacket(
final Inet4Address clientAddress, final Inet4Address requestedAddress,
final Inet4Address serverAddress, final 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;
final ByteBuffer packet = DhcpPacket.buildRequestPacket(
encap, mTransactionId, getSecs(), clientAddress, DO_UNICAST, mHwAddr,
requestedAddress, serverAddress, getRequestedParams(), mHostname,
mConfiguration.options);
String serverStr = (serverAddress != null) ? serverAddress.getHostAddress() : null;
String description = "DHCPREQUEST ciaddr=" + clientAddress.getHostAddress() +
" request=" + requestedAddress.getHostAddress() +
" serverid=" + serverStr;
mMetrics.incrementCountForRequest();
return transmitPacket(packet, description, encap, to);
}
private boolean sendDeclinePacket(final Inet4Address requestedAddress,
final Inet4Address serverIdentifier) {
// Requested IP address and Server Identifier options are mandatory for DHCPDECLINE.
final ByteBuffer packet = DhcpPacket.buildDeclinePacket(DhcpPacket.ENCAP_L2,
mTransactionId, mHwAddr, requestedAddress, serverIdentifier);
return transmitPacket(packet, "DHCPDECLINE", DhcpPacket.ENCAP_L2, INADDR_BROADCAST);
}
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 setLeaseExpiredToIpMemoryStore() {
final String l2Key = mConfiguration.l2Key;
if (l2Key == null) return;
final NetworkAttributes.Builder na = new NetworkAttributes.Builder();
// TODO: clear out the address and lease instead of storing an expired lease
na.setAssignedV4AddressExpiry(EXPIRED_LEASE);
final OnStatusListener listener = status -> {
if (!status.isSuccess()) Log.e(TAG, "Failed to set lease expiry, status: " + status);
};
mIpMemoryStore.storeNetworkAttributes(l2Key, na.build(), listener);
}
private void maybeSaveLeaseToIpMemoryStore() {
final String l2Key = mConfiguration.l2Key;
if (l2Key == null || mDhcpLease == null || mDhcpLease.ipAddress == null) return;
final NetworkAttributes.Builder na = new NetworkAttributes.Builder();
na.setAssignedV4Address((Inet4Address) mDhcpLease.ipAddress.getAddress());
na.setAssignedV4AddressExpiry((mDhcpLease.leaseDuration == INFINITE_LEASE)
? Long.MAX_VALUE
: mDhcpLease.leaseDuration * 1000 + System.currentTimeMillis());
na.setDnsAddresses(mDhcpLease.dnsServers);
na.setMtu(mDhcpLease.mtu);
final OnStatusListener listener = status -> {
if (!status.isSuccess()) Log.e(TAG, "Failed to store network attrs, status: " + status);
};
mIpMemoryStore.storeNetworkAttributes(l2Key, na.build(), listener);
}
private void notifySuccess() {
if (isDhcpLeaseCacheEnabled()) {
maybeSaveLeaseToIpMemoryStore();
}
mController.sendMessage(
CMD_POST_DHCP_ACTION, DHCP_SUCCESS, 0, new DhcpResults(mDhcpLease));
}
private void notifyFailure() {
if (isDhcpLeaseCacheEnabled()) {
setLeaseExpiredToIpMemoryStore();
}
mController.sendMessage(CMD_POST_DHCP_ACTION, DHCP_FAILURE, 0, null);
}
private void acceptDhcpResults(DhcpResults results, String msg) {
mDhcpLease = results;
if (mDhcpLease.dnsServers.isEmpty()) {
// supplement customized dns servers
final String[] dnsServersList =
mContext.getResources().getStringArray(R.array.config_default_dns_servers);
for (final String dnsServer : dnsServersList) {
try {
mDhcpLease.dnsServers.add(InetAddresses.parseNumericAddress(dnsServer));
} catch (IllegalArgumentException e) {
Log.e(TAG, "Invalid default DNS server: " + dnsServer, e);
}
}
}
mOffer = null;
Log.d(TAG, msg + " lease: " + mDhcpLease);
}
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();
return new StringBuilder(" ")
.append(message.getWhen() - now)
.append(messageName(message.what))
.append(" ").append(message.arg1)
.append(" ").append(message.arg2)
.append(" ").append(message.obj)
.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 {
private final State mOtherState;
WaitBeforeOtherState(State otherState) {
mOtherState = otherState;
}
@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;
}
}
}
/**
* Helper method to transition to the appropriate state according to whether the pre dhcp
* action (e.g. turn off power optimization while doing DHCP) is required to execute.
* waitStateForPreDhcpAction is used to wait the pre dhcp action completed before moving to
* other state. If the pre dhcp action is unnecessary, transition to the target state directly.
*/
private void preDhcpTransitionTo(final State waitStateForPreDhcpAction,
final State targetState) {
transitionTo(mRegisteredForPreDhcpNotification ? waitStateForPreDhcpAction : targetState);
}
/**
* This class is used to convey initial configuration to DhcpClient when starting DHCP.
*/
public static class Configuration {
// This is part of the initial configuration because it is passed in on startup and
// never updated.
// TODO: decide what to do about L2 key changes while the client is connected.
@Nullable
public final String l2Key;
public final boolean isPreconnectionEnabled;
@NonNull
public final List<DhcpOption> options;
public Configuration(@Nullable final String l2Key, final boolean isPreconnectionEnabled,
@NonNull final List<DhcpOption> options) {
this.l2Key = l2Key;
this.isPreconnectionEnabled = isPreconnectionEnabled;
this.options = options;
}
}
class StoppedState extends State {
@Override
public boolean processMessage(Message message) {
switch (message.what) {
case CMD_START_DHCP:
mConfiguration = (Configuration) message.obj;
if (mConfiguration.isPreconnectionEnabled) {
transitionTo(mDhcpPreconnectingState);
} else {
startInitRebootOrInit();
}
recordMetricEnabledFeatures();
return HANDLED;
default:
return NOT_HANDLED;
}
}
}
class WaitBeforeStartState extends WaitBeforeOtherState {
WaitBeforeStartState(State otherState) {
super(otherState);
}
}
class WaitBeforeRenewalState extends WaitBeforeOtherState {
WaitBeforeRenewalState(State otherState) {
super(otherState);
}
}
class WaitBeforeObtainingConfigurationState extends WaitBeforeOtherState {
WaitBeforeObtainingConfigurationState(State otherState) {
super(otherState);
}
}
class DhcpState extends State {
@Override
public void enter() {
clearDhcpState();
mConflictCount = 0;
if (initInterface() && initUdpSocket()) {
mDhcpPacketHandler = new DhcpPacketHandler(getHandler());
if (mDhcpPacketHandler.start()) return;
Log.e(TAG, "Fail to start DHCP Packet Handler");
}
notifyFailure();
// We cannot call transitionTo because a transition is still in progress.
// Instead, ensure that we process CMD_STOP_DHCP as soon as the transition is complete.
deferMessage(obtainMessage(CMD_STOP_DHCP));
}
@Override
public void exit() {
if (mDhcpPacketHandler != null) {
mDhcpPacketHandler.stop();
if (DBG) Log.d(TAG, "DHCP Packet Handler stopped");
}
closeSocketQuietly(mUdpSock);
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;
}
abstract class TimeoutState extends LoggingState {
protected long mTimeout = 0;
@Override
public void enter() {
super.enter();
maybeInitTimeout();
}
@Override
public boolean processMessage(Message message) {
super.processMessage(message);
switch (message.what) {
case CMD_TIMEOUT:
timeout();
return HANDLED;
default:
return NOT_HANDLED;
}
}
@Override
public void exit() {
super.exit();
mTimeoutAlarm.cancel();
}
protected abstract void timeout();
private void maybeInitTimeout() {
if (mTimeout > 0) {
long alarmTime = SystemClock.elapsedRealtime() + mTimeout;
mTimeoutAlarm.schedule(alarmTime);
}
}
}
/**
* 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 TimeoutState {
private int mTimer;
@Override
public void enter() {
super.enter();
initTimer();
sendMessage(CMD_KICK);
}
@Override
public boolean processMessage(Message message) {
if (super.processMessage(message) == HANDLED) {
return HANDLED;
}
switch (message.what) {
case CMD_KICK:
sendPacket();
scheduleKick();
return HANDLED;
case CMD_RECEIVED_PACKET:
receivePacket((DhcpPacket) message.obj);
return HANDLED;
default:
return NOT_HANDLED;
}
}
@Override
public void exit() {
super.exit();
mKickAlarm.cancel();
}
protected abstract boolean sendPacket();
protected abstract 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;
}
}
}
class ObtainingConfigurationState extends LoggingState {
@Override
public void enter() {
super.enter();
// Set a timeout for retrieving network attributes operation
sendMessageDelayed(EVENT_CONFIGURATION_TIMEOUT, IPMEMORYSTORE_TIMEOUT_MS);
final OnNetworkAttributesRetrievedListener listener = (status, l2Key, attributes) -> {
if (null == attributes || null == attributes.assignedV4Address) {
if (!status.isSuccess()) {
Log.e(TAG, "Error retrieving network attributes: " + status);
}
sendMessage(EVENT_CONFIGURATION_INVALID);
return;
}
sendMessage(EVENT_CONFIGURATION_OBTAINED, attributes);
};
mIpMemoryStore.retrieveNetworkAttributes(mConfiguration.l2Key, listener);
}
@Override
public boolean processMessage(Message message) {
super.processMessage(message);
switch (message.what) {
case EVENT_CONFIGURATION_INVALID:
case EVENT_CONFIGURATION_TIMEOUT:
transitionTo(mDhcpInitState);
return HANDLED;
case EVENT_CONFIGURATION_OBTAINED:
final long currentTime = System.currentTimeMillis();
NetworkAttributes attributes = (NetworkAttributes) message.obj;
if (DBG) {
Log.d(TAG, "l2key: " + mConfiguration.l2Key
+ " lease address: " + attributes.assignedV4Address
+ " lease expiry: " + attributes.assignedV4AddressExpiry
+ " current time: " + currentTime);
}
if (currentTime >= attributes.assignedV4AddressExpiry) {
// Lease has expired.
transitionTo(mDhcpInitState);
return HANDLED;
}
mLastAssignedIpv4Address = attributes.assignedV4Address;
mLastAssignedIpv4AddressExpiry = attributes.assignedV4AddressExpiry;
transitionTo(mDhcpInitRebootState);
return HANDLED;
default:
deferMessage(message);
return HANDLED;
}
}
@Override
public void exit() {
super.exit();
removeMessages(EVENT_CONFIGURATION_INVALID);
removeMessages(EVENT_CONFIGURATION_TIMEOUT);
removeMessages(EVENT_CONFIGURATION_OBTAINED);
}
}
private boolean maybeTransitionToIpv6OnlyWaitState(@NonNull final DhcpPacket packet) {
if (!isIPv6OnlyPreferredModeEnabled()) return false;
if (packet.getIpv6OnlyWaitTimeMillis() == DhcpPacket.V6ONLY_PREFERRED_ABSENCE) return false;
mIpv6OnlyWaitTimeMs = packet.getIpv6OnlyWaitTimeMillis();
transitionTo(mIpv6OnlyWaitState);
return true;
}
private void receiveOfferOrAckPacket(final DhcpPacket packet, final boolean acceptRapidCommit) {
if (!isValidPacket(packet)) return;
// 1. received the DHCPOFFER packet, process it by following RFC2131.
// 2. received the DHCPACK packet from DHCP Servers that support Rapid
// Commit option, process it by following RFC4039.
if (packet instanceof DhcpOfferPacket) {
if (maybeTransitionToIpv6OnlyWaitState(packet)) {
return;
}
mOffer = packet.toDhcpResults();
if (mOffer != null) {
Log.d(TAG, "Got pending lease: " + mOffer);
transitionTo(mDhcpRequestingState);
}
} else if (packet instanceof DhcpAckPacket) {
// If rapid commit is not enabled in DhcpInitState, or enablePreconnection is
// not enabled in DhcpPreconnectingState, ignore DHCPACK packet. Only DHCPACK
// with the rapid commit option are valid.
if (!acceptRapidCommit || !packet.mRapidCommit) return;
final DhcpResults results = packet.toDhcpResults();
if (results != null) {
confirmDhcpLease(packet, results);
transitionTo(isDhcpIpConflictDetectEnabled()
? mIpAddressConflictDetectingState : mConfiguringInterfaceState);
}
}
}
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) {
receiveOfferOrAckPacket(packet, isDhcpRapidCommitEnabled());
}
}
private void startInitRebootOrInit() {
if (isDhcpLeaseCacheEnabled()) {
preDhcpTransitionTo(mWaitBeforeObtainingConfigurationState,
mObtainingConfigurationState);
} else {
preDhcpTransitionTo(mWaitBeforeStartState, mDhcpInitState);
}
}
class DhcpPreconnectingState extends TimeoutState {
// This state is used to support Fast Initial Link Setup (FILS) IP Address Setup
// procedure defined in the IEEE802.11ai (2016) currently. However, this state could
// be extended to support other intended useage as well in the future, e.g. pre-actions
// should be completed in advance before the normal DHCP solicit process starts.
DhcpPreconnectingState() {
mTimeout = FIRST_TIMEOUT_MS;
}
@Override
public void enter() {
super.enter();
startNewTransaction();
mLastInitEnterTime = SystemClock.elapsedRealtime();
sendPreconnectionPacket();
}
@Override
public boolean processMessage(Message message) {
if (super.processMessage(message) == HANDLED) {
return HANDLED;
}
switch (message.what) {
case CMD_RECEIVED_PACKET:
receiveOfferOrAckPacket((DhcpPacket) message.obj,
mConfiguration.isPreconnectionEnabled);
return HANDLED;
case CMD_ABORT_PRECONNECTION:
startInitRebootOrInit();
return HANDLED;
default:
return NOT_HANDLED;
}
}
// This timeout is necessary and cannot just be replaced with a notification that
// preconnection is complete. This is because:
// - The preconnection complete notification could arrive before the ACK with rapid
// commit arrives. In this case we would go back to init state, pick a new transaction
// ID, and when the ACK with rapid commit arrives, we would ignore it because the
// transaction ID doesn't match.
// - We cannot just wait in this state until the ACK with rapid commit arrives, because
// if that ACK never arrives (e.g., dropped by the network), we'll never go back to init
// and send a DISCOVER.
@Override
public void timeout() {
startInitRebootOrInit();
}
private void sendPreconnectionPacket() {
final Layer2PacketParcelable l2Packet = new Layer2PacketParcelable();
final ByteBuffer packet = DhcpPacket.buildDiscoverPacket(
DhcpPacket.ENCAP_L2, mTransactionId, getSecs(), mHwAddr,
DO_UNICAST, getRequestedParams(), true /* rapid commit */, mHostname,
mConfiguration.options);
l2Packet.dstMacAddress = MacAddress.fromBytes(DhcpPacket.ETHER_BROADCAST);
l2Packet.payload = Arrays.copyOf(packet.array(), packet.limit());
mController.sendMessage(CMD_START_PRECONNECTION, l2Packet);
}
}
// 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)) {
if (maybeTransitionToIpv6OnlyWaitState(packet)) {
return;
}
final DhcpResults results = packet.toDhcpResults();
if (results != null) {
confirmDhcpLease(packet, results);
transitionTo(isDhcpIpConflictDetectEnabled()
? mIpAddressConflictDetectingState : 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(mStoppedState);
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();
// We must call notifySuccess to apply the rest of the DHCP configuration (e.g., DNS
// servers) before adding the IP address to the interface. Otherwise, as soon as
// IpClient sees the IP address appear, it will enter provisioned state without any
// configuration information from DHCP. http://b/146850745.
notifySuccess();
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;
}
}
}
private class IpConflictDetector extends PacketReader {
private FileDescriptor mArpSock;
private final Inet4Address mTargetIp;
IpConflictDetector(@NonNull Handler handler, @NonNull Inet4Address ipAddress) {
super(handler);
mTargetIp = ipAddress;
}
@Override
protected void handlePacket(byte[] recvbuf, int length) {
try {
final ArpPacket packet = ArpPacket.parseArpPacket(recvbuf, length);
if (hasIpAddressConflict(packet, mTargetIp)) {
mMetrics.incrementCountForIpConflict();
sendMessage(EVENT_IP_CONFLICT);
}
} catch (ArpPacket.ParseException e) {
logError("Can't parse ARP packet", e);
}
}
@Override
protected FileDescriptor createFd() {
try {
// TODO: attach ARP packet only filter.
mArpSock = Os.socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, 0 /* protocol */);
SocketAddress addr = makePacketSocketAddress(ETH_P_ARP, mIface.index);
Os.bind(mArpSock, addr);
return mArpSock;
} catch (SocketException | ErrnoException e) {
logError("Error creating ARP socket", e);
closeFd(mArpSock);
mArpSock = null;
return null;
}
}
@Override
protected void logError(@NonNull String msg, @NonNull Exception e) {
Log.e(TAG, msg, e);
}
public boolean transmitPacket(@NonNull Inet4Address targetIp,
@NonNull Inet4Address senderIp, final byte[] hwAddr,
@NonNull SocketAddress sockAddr) {
// RFC5227 3. describes both ARP Probes and Announcements use ARP Request packet.
final ByteBuffer packet = ArpPacket.buildArpPacket(DhcpPacket.ETHER_BROADCAST, hwAddr,
targetIp.getAddress(), new byte[ETHER_ADDR_LEN], senderIp.getAddress(),
(short) ARP_REQUEST);
try {
Os.sendto(mArpSock, packet.array(), 0 /* byteOffset */,
packet.limit() /* byteCount */, 0 /* flags */, sockAddr);
return true;
} catch (ErrnoException | SocketException e) {
logError("Can't send ARP packet", e);
return false;
}
}
}
private boolean isArpProbe(@NonNull ArpPacket packet) {
return (packet.opCode == ARP_REQUEST && packet.senderIp.equals(INADDR_ANY)
&& !packet.targetIp.equals(INADDR_ANY));
}
// RFC5227 2.1.1 says, during probing period:
// 1. the host receives any ARP packet (Request *or* Reply) on the interface where the
// probe is being performed, where the packet's 'sender IP address' is the address
// being probed for, then the host MUST treat this address as conflict.
// 2. the host receives any ARP Probe where the packet's 'target IP address' is the
// address being probed for, and the packet's 'sender hardware address' is not the
// hardware address of any of the host's interfaces, then the host SHOULD similarly
// treat this as an address conflict.
private boolean packetHasIpAddressConflict(@NonNull ArpPacket packet,
@NonNull Inet4Address targetIp) {
return (((!packet.senderIp.equals(INADDR_ANY) && packet.senderIp.equals(targetIp))
|| (isArpProbe(packet) && packet.targetIp.equals(targetIp)))
&& !Arrays.equals(packet.senderHwAddress.toByteArray(), mHwAddr));
}
private boolean hasIpAddressConflict(@NonNull ArpPacket packet,
@NonNull Inet4Address targetIp) {
if (!packetHasIpAddressConflict(packet, targetIp)) return false;
if (DBG) {
final String senderIpString = packet.senderIp.getHostAddress();
final String targetIpString = packet.targetIp.getHostAddress();
final MacAddress senderMacAddress = packet.senderHwAddress;
final MacAddress hostMacAddress = MacAddress.fromBytes(mHwAddr);
Log.d(TAG, "IP address conflict detected:"
+ (packet.opCode == ARP_REQUEST ? "ARP Request" : "ARP Reply")
+ " ARP sender MAC: " + senderMacAddress.toString()
+ " host MAC: " + hostMacAddress.toString()
+ " ARP sender IP: " + senderIpString
+ " ARP target IP: " + targetIpString
+ " host target IP: " + targetIp.getHostAddress());
}
return true;
}
class IpAddressConflictDetectingState extends LoggingState {
private int mArpProbeCount;
private int mArpAnnounceCount;
private Inet4Address mTargetIp;
private IpConflictDetector mIpConflictDetector;
private PowerManager.WakeLock mTimeoutWakeLock;
private int mArpFirstProbeDelayMs;
private int mArpProbeMaxDelayMs;
private int mArpProbeMinDelayMs;
private int mArpFirstAnnounceDelayMs;
private int mArpAnnounceIntervalMs;
@Override
public void enter() {
super.enter();
mArpProbeCount = 0;
mArpAnnounceCount = 0;
// IP address conflict detection occurs after receiving DHCPACK
// message every time, i.e. we already get an available lease from
// DHCP server, that ensures mDhcpLease should be NonNull, see
// {@link DhcpRequestingState#receivePacket} for details.
mTargetIp = (Inet4Address) mDhcpLease.ipAddress.getAddress();
mIpConflictDetector = new IpConflictDetector(getHandler(), mTargetIp);
// IpConflictDetector might fail to create the raw socket.
if (!mIpConflictDetector.start()) {
Log.e(TAG, "Fail to start IP Conflict Detector");
transitionTo(mConfiguringInterfaceState);
return;
}
// Read the customized parameters from DeviceConfig.
readIpConflictParametersFromDeviceConfig();
if (VDBG) {
Log.d(TAG, "ARP First Probe delay: " + mArpFirstProbeDelayMs
+ " ARP Probe Max delay: " + mArpProbeMaxDelayMs
+ " ARP Probe Min delay: " + mArpProbeMinDelayMs
+ " ARP First Announce delay: " + mArpFirstAnnounceDelayMs
+ " ARP Announce interval: " + mArpAnnounceIntervalMs);
}
// Note that when we get here, we're still processing the WakeupMessage that caused
// us to transition into this state, and thus the AlarmManager is still holding its
// wakelock. That wakelock might expire as soon as this method returns.
final PowerManager powerManager = mContext.getSystemService(PowerManager.class);
mTimeoutWakeLock = mDependencies.getWakeLock(powerManager);
mTimeoutWakeLock.acquire();
// RFC5227 2.1.1 describes waiting for a random time interval between 0 and
// PROBE_WAIT seconds before sending probe packets PROBE_NUM times, this delay
// helps avoid hosts send initial probe packet simultaneously upon power on.
// Probe packet transmission interval spaces randomly and uniformly between
// PROBE_MIN and PROBE_MAX.
sendMessageDelayed(CMD_ARP_PROBE, mRandom.nextInt(mArpFirstProbeDelayMs));
}
@Override
public boolean processMessage(Message message) {
super.processMessage(message);
switch (message.what) {
case CMD_ARP_PROBE:
// According to RFC5227, wait ANNOUNCE_WAIT seconds after
// the last ARP Probe, and no conflicting ARP Reply or ARP
// Probe has been received within this period, then host can
// determine the desired IP address may be used safely.
sendArpProbe();
if (++mArpProbeCount < IPV4_CONFLICT_PROBE_NUM) {
scheduleProbe();
} else {
scheduleAnnounce(mArpFirstAnnounceDelayMs);
}
return HANDLED;
case CMD_ARP_ANNOUNCEMENT:
sendArpAnnounce();
if (++mArpAnnounceCount < IPV4_CONFLICT_ANNOUNCE_NUM) {
scheduleAnnounce(mArpAnnounceIntervalMs);
} else {
transitionTo(mConfiguringInterfaceState);
}
return HANDLED;
case EVENT_IP_CONFLICT:
transitionTo(mDhcpDecliningState);
return HANDLED;
default:
return NOT_HANDLED;
}
}
// Because the timing parameters used in IP Address detection mechanism are in
// milliseconds, WakeupMessage would be too imprecise for small timeouts.
private void scheduleProbe() {
long timeout = mRandom.nextInt(mArpProbeMaxDelayMs - mArpProbeMinDelayMs)
+ mArpProbeMinDelayMs;
sendMessageDelayed(CMD_ARP_PROBE, timeout);
}
private void scheduleAnnounce(final int timeout) {
sendMessageDelayed(CMD_ARP_ANNOUNCEMENT, timeout);
}
@Override
public void exit() {
super.exit();
mTimeoutWakeLock.release();
mIpConflictDetector.stop();
if (DBG) Log.d(TAG, "IP Conflict Detector stopped");
removeMessages(CMD_ARP_PROBE);
removeMessages(CMD_ARP_ANNOUNCEMENT);
removeMessages(EVENT_IP_CONFLICT);
}
// The following timing parameters are defined in RFC5227 as fixed constants, which
// are too long to adopt in the mobile network scenario, however more appropriate to
// reference these fixed value as maximumValue argument to restrict the upper bound,
// the minimum values of 10/20ms are used to avoid tight loops due to misconfiguration.
private void readIpConflictParametersFromDeviceConfig() {
// PROBE_WAIT
mArpFirstProbeDelayMs = mDependencies.getIntDeviceConfig(ARP_FIRST_PROBE_DELAY_MS,
10, MAX_ARP_FIRST_PROBE_DELAY_MS, DEFAULT_ARP_FIRST_PROBE_DELAY_MS);
// PROBE_MIN
mArpProbeMinDelayMs = mDependencies.getIntDeviceConfig(ARP_PROBE_MIN_MS, 10,
MAX_ARP_PROBE_MIN_MS, DEFAULT_ARP_PROBE_MIN_MS);
// PROBE_MAX
mArpProbeMaxDelayMs = Math.max(mArpProbeMinDelayMs + 1,
mDependencies.getIntDeviceConfig(ARP_PROBE_MAX_MS, 20, MAX_ARP_PROBE_MAX_MS,
DEFAULT_ARP_PROBE_MAX_MS));
// ANNOUNCE_WAIT
mArpFirstAnnounceDelayMs = mDependencies.getIntDeviceConfig(ARP_FIRST_ANNOUNCE_DELAY_MS,
20, MAX_ARP_FIRST_ANNOUNCE_DELAY_MS, DEFAULT_ARP_FIRST_ANNOUNCE_DELAY_MS);
// ANNOUNCE_INTERVAL
mArpAnnounceIntervalMs = mDependencies.getIntDeviceConfig(ARP_ANNOUNCE_INTERVAL_MS, 20,
MAX_ARP_ANNOUNCE_INTERVAL_MS, DEFAULT_ARP_ANNOUNCE_INTERVAL_MS);
}
private boolean sendArpProbe() {
return mIpConflictDetector.transmitPacket(mTargetIp /* target IP */,
INADDR_ANY /* sender IP */, mHwAddr, mInterfaceBroadcastAddr);
}
private boolean sendArpAnnounce() {
return mIpConflictDetector.transmitPacket(mTargetIp /* target IP */,
mTargetIp /* sender IP */, mHwAddr, mInterfaceBroadcastAddr);
}
}
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:
preDhcpTransitionTo(mWaitBeforeRenewalState, mDhcpRenewingState);
return HANDLED;
case CMD_REFRESH_LINKADDRESS:
transitionTo(mDhcpRebindingState);
return HANDLED;
default:
return NOT_HANDLED;
}
}
private void logTimeToBoundState() {
long now = SystemClock.elapsedRealtime();
if (mLastBoundExitTime > mLastInitEnterTime) {
logState(EVENT_RENEWING_BOUND, (int) (now - mLastBoundExitTime));
} else {
logState(EVENT_INITIAL_BOUND, (int) (now - mLastInitEnterTime));
}
}
}
abstract class DhcpReacquiringState extends PacketRetransmittingState {
protected String mLeaseMsg;
@Override
public void enter() {
super.enter();
startNewTransaction();
}
protected abstract 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)) {
if (maybeTransitionToIpv6OnlyWaitState(packet)) {
return;
}
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(mStoppedState);
return;
}
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);
notifySuccess();
transitionTo(mDhcpBoundState);
}
} else if (packet instanceof DhcpNakPacket) {
Log.d(TAG, "Received NAK, returning to StoppedState");
notifyFailure();
transitionTo(mStoppedState);
}
}
}
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.
closeSocketQuietly(mUdpSock);
if (!initUdpSocket()) {
Log.e(TAG, "Failed to recreate UDP socket");
transitionTo(mDhcpInitState);
}
}
@Override
protected Inet4Address packetDestination() {
return INADDR_BROADCAST;
}
}
class DhcpInitRebootState extends DhcpRequestingState {
@Override
public void enter() {
mTimeout = DHCP_INITREBOOT_TIMEOUT_MS;
super.enter();
startNewTransaction();
}
// RFC 2131 4.3.2 describes generated DHCPREQUEST message during
// INIT-REBOOT state:
// 'server identifier' MUST NOT be filled in, 'requested IP address'
// option MUST be filled in with client's notion of its previously
// assigned address. 'ciaddr' MUST be zero. The client is seeking to
// verify a previously allocated, cached configuration. Server SHOULD
// send a DHCPNAK message to the client if the 'requested IP address'
// is incorrect, or is on the wrong network.
@Override
protected boolean sendPacket() {
return sendRequestPacket(
INADDR_ANY, // ciaddr
mLastAssignedIpv4Address, // DHCP_REQUESTED_IP
null, // DHCP_SERVER_IDENTIFIER
INADDR_BROADCAST); // packet destination address
}
@Override
public void exit() {
mLastAssignedIpv4Address = null;
mLastAssignedIpv4AddressExpiry = 0;
}
}
class DhcpRebootingState extends LoggingState {
}
class DhcpDecliningState extends TimeoutState {
@Override
public void enter() {
// If the host experiences MAX_CONFLICTS or more address conflicts on the
// interface, configure interface with this IP address anyway.
if (++mConflictCount > MAX_CONFLICTS_COUNT) {
transitionTo(mConfiguringInterfaceState);
return;
}
mTimeout = mDependencies.getIntDeviceConfig(DHCP_RESTART_CONFIG_DELAY, 100,
MAX_DHCP_CLIENT_RESTART_CONFIG_DELAY_MS, DEFAULT_DHCP_RESTART_CONFIG_DELAY_MS);
super.enter();
sendPacket();
}
// No need to override processMessage here since this state is
// functionally identical to its superclass TimeoutState.
protected void timeout() {
transitionTo(mDhcpInitState);
}
private boolean sendPacket() {
return sendDeclinePacket(
(Inet4Address) mDhcpLease.ipAddress.getAddress(), // requested IP
(Inet4Address) mDhcpLease.serverAddress); // serverIdentifier
}
}
// This state is used for IPv6-only preferred mode defined in the draft-ietf-dhc-v6only.
// For IPv6-only capable host, it will forgo obtaining an IPv4 address for V6ONLY_WAIT
// period if the network indicates that it can provide IPv6 connectivity by replying
// with a valid IPv6-only preferred option in the DHCPOFFER or DHCPACK.
class Ipv6OnlyWaitState extends TimeoutState {
@Override
public void enter() {
mTimeout = mIpv6OnlyWaitTimeMs;
super.enter();
// Restore power save and suspend optimization if it was disabled before.
if (mRegisteredForPreDhcpNotification) {
mController.sendMessage(CMD_POST_DHCP_ACTION, DHCP_IPV6_ONLY, 0, null);
}
}
@Override
public void exit() {
mIpv6OnlyWaitTimeMs = 0;
}
protected void timeout() {
startInitRebootOrInit();
}
}
private void logState(String name, int durationMs) {
final DhcpClientEvent event = new DhcpClientEvent.Builder()
.setMsg(name)
.setDurationMs(durationMs)
.build();
mMetricsLog.log(mIfaceName, event);
}
}