blob: 3acd76eefa1da43dcc03cbb62718fbd7e859f5ad [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.net.dhcp;
import static android.net.dhcp.DhcpPacket.DHCP_CLIENT;
import static android.net.dhcp.DhcpPacket.DHCP_HOST_NAME;
import static android.net.dhcp.DhcpPacket.DHCP_SERVER;
import static android.net.dhcp.DhcpPacket.ENCAP_BOOTP;
import static android.net.dhcp.IDhcpServer.STATUS_INVALID_ARGUMENT;
import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
import static android.net.dhcp.IDhcpServer.STATUS_UNKNOWN_ERROR;
import static android.net.util.NetworkStackUtils.DHCP_RAPID_COMMIT_VERSION;
import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
import static android.system.OsConstants.AF_INET;
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.SOL_SOCKET;
import static android.system.OsConstants.SO_BROADCAST;
import static android.system.OsConstants.SO_REUSEADDR;
import static com.android.internal.util.TrafficStatsConstants.TAG_SYSTEM_DHCP_SERVER;
import static com.android.net.module.util.Inet4AddressUtils.getBroadcastAddress;
import static com.android.net.module.util.Inet4AddressUtils.getPrefixMaskAsInet4Address;
import static com.android.net.module.util.NetworkStackConstants.INFINITE_LEASE;
import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ALL;
import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY;
import static com.android.server.util.PermissionUtil.enforceNetworkStackCallingPermission;
import static java.lang.Integer.toUnsignedLong;
import android.content.Context;
import android.net.INetworkStackStatusCallback;
import android.net.IpPrefix;
import android.net.MacAddress;
import android.net.TrafficStats;
import android.net.util.NetworkStackUtils;
import android.net.util.SharedLog;
import android.net.util.SocketUtils;
import android.os.Handler;
import android.os.Message;
import android.os.RemoteException;
import android.os.SystemClock;
import android.system.ErrnoException;
import android.system.Os;
import android.text.TextUtils;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.internal.util.HexDump;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.net.module.util.DeviceConfigUtils;
import java.io.FileDescriptor;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.util.ArrayList;
/**
* A DHCPv4 server.
*
* <p>This server listens for and responds to packets on a single interface. It considers itself
* authoritative for all leases on the subnet, which means that DHCP requests for unknown leases of
* unknown hosts receive a reply instead of being ignored.
*
* <p>The server relies on StateMachine's handler (including send/receive operations): all internal
* operations are done in StateMachine's looper. Public methods are thread-safe and will schedule
* operations on that looper asynchronously.
* @hide
*/
public class DhcpServer extends StateMachine {
private static final String REPO_TAG = "Repository";
// Lease time to transmit to client instead of a negative time in case a lease expired before
// the server could send it (if the server process is suspended for example).
private static final int EXPIRED_FALLBACK_LEASE_TIME_SECS = 120;
private static final int CMD_START_DHCP_SERVER = 1;
private static final int CMD_STOP_DHCP_SERVER = 2;
private static final int CMD_UPDATE_PARAMS = 3;
@VisibleForTesting
protected static final int CMD_RECEIVE_PACKET = 4;
private static final int CMD_TERMINATE_AFTER_STOP = 5;
@NonNull
private final Context mContext;
@NonNull
private final String mIfName;
@NonNull
private final DhcpLeaseRepository mLeaseRepo;
@NonNull
private final SharedLog mLog;
@NonNull
private final Dependencies mDeps;
@NonNull
private final Clock mClock;
@NonNull
private DhcpServingParams mServingParams;
@Nullable
private DhcpPacketListener mPacketListener;
@Nullable
private FileDescriptor mSocket;
@Nullable
private IDhcpEventCallbacks mEventCallbacks;
private final boolean mDhcpRapidCommitEnabled;
// States.
private final StoppedState mStoppedState = new StoppedState();
private final StartedState mStartedState = new StartedState();
private final RunningState mRunningState = new RunningState();
private final WaitBeforeRetrievalState mWaitBeforeRetrievalState =
new WaitBeforeRetrievalState();
/**
* Clock to be used by DhcpServer to track time for lease expiration.
*
* <p>The clock should track time as may be measured by clients obtaining a lease. It does not
* need to be monotonous across restarts of the server as long as leases are cleared when the
* server is stopped.
*/
public static class Clock {
/**
* @see SystemClock#elapsedRealtime()
*/
public long elapsedRealtime() {
return SystemClock.elapsedRealtime();
}
}
/**
* Dependencies for the DhcpServer. Useful to be mocked in tests.
*/
public interface Dependencies {
/**
* Send a packet to the specified datagram socket.
*
* @param fd File descriptor of the socket.
* @param buffer Data to be sent.
* @param dst Destination address of the packet.
*/
void sendPacket(@NonNull FileDescriptor fd, @NonNull ByteBuffer buffer,
@NonNull InetAddress dst) throws ErrnoException, IOException;
/**
* Create a DhcpLeaseRepository for the server.
* @param servingParams Parameters used to serve DHCP requests.
* @param log Log to be used by the repository.
* @param clock Clock that the repository must use to track time.
*/
DhcpLeaseRepository makeLeaseRepository(@NonNull DhcpServingParams servingParams,
@NonNull SharedLog log, @NonNull Clock clock);
/**
* Create a packet listener that will send packets to be processed.
*/
DhcpPacketListener makePacketListener(@NonNull Handler handler);
/**
* Create a clock that the server will use to track time.
*/
Clock makeClock();
/**
* Add an entry to the ARP cache table.
* @param fd Datagram socket file descriptor that must use the new entry.
*/
void addArpEntry(@NonNull Inet4Address ipv4Addr, @NonNull MacAddress ethAddr,
@NonNull String ifname, @NonNull FileDescriptor fd) throws IOException;
/**
* Check whether or not one specific experimental feature for connectivity namespace is
* enabled.
* @param context The global context information about an app environment.
* @param name Specific experimental flag name.
*/
boolean isFeatureEnabled(@NonNull Context context, @NonNull String name);
}
private class DependenciesImpl implements Dependencies {
@Override
public void sendPacket(@NonNull FileDescriptor fd, @NonNull ByteBuffer buffer,
@NonNull InetAddress dst) throws ErrnoException, IOException {
Os.sendto(fd, buffer, 0, dst, DhcpPacket.DHCP_CLIENT);
}
@Override
public DhcpLeaseRepository makeLeaseRepository(@NonNull DhcpServingParams servingParams,
@NonNull SharedLog log, @NonNull Clock clock) {
return new DhcpLeaseRepository(
DhcpServingParams.makeIpPrefix(servingParams.serverAddr),
servingParams.excludedAddrs, servingParams.dhcpLeaseTimeSecs * 1000,
servingParams.singleClientAddr, log.forSubComponent(REPO_TAG), clock);
}
@Override
public DhcpPacketListener makePacketListener(@NonNull Handler handler) {
return new PacketListener(handler);
}
@Override
public Clock makeClock() {
return new Clock();
}
@Override
public void addArpEntry(@NonNull Inet4Address ipv4Addr, @NonNull MacAddress ethAddr,
@NonNull String ifname, @NonNull FileDescriptor fd) throws IOException {
NetworkStackUtils.addArpEntry(ipv4Addr, ethAddr, ifname, fd);
}
@Override
public boolean isFeatureEnabled(@NonNull Context context, @NonNull String name) {
return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY, name);
}
}
private static class MalformedPacketException extends Exception {
MalformedPacketException(String message, Throwable t) {
super(message, t);
}
}
public DhcpServer(@NonNull Context context, @NonNull String ifName,
@NonNull DhcpServingParams params, @NonNull SharedLog log) {
this(context, ifName, params, log, null);
}
@VisibleForTesting
DhcpServer(@NonNull Context context, @NonNull String ifName, @NonNull DhcpServingParams params,
@NonNull SharedLog log, @Nullable Dependencies deps) {
super(DhcpServer.class.getSimpleName() + "." + ifName);
if (deps == null) {
deps = new DependenciesImpl();
}
mContext = context;
mIfName = ifName;
mServingParams = params;
mLog = log;
mDeps = deps;
mClock = deps.makeClock();
mLeaseRepo = deps.makeLeaseRepository(mServingParams, mLog, mClock);
mDhcpRapidCommitEnabled = deps.isFeatureEnabled(context, DHCP_RAPID_COMMIT_VERSION);
// CHECKSTYLE:OFF IndentationCheck
addState(mStoppedState);
addState(mStartedState);
addState(mRunningState, mStartedState);
addState(mWaitBeforeRetrievalState, mStartedState);
// CHECKSTYLE:ON IndentationCheck
setInitialState(mStoppedState);
super.start();
}
/**
* Make a IDhcpServer connector to communicate with this DhcpServer.
*/
public IDhcpServer makeConnector() {
return new DhcpServerConnector();
}
private class DhcpServerConnector extends IDhcpServer.Stub {
@Override
public void start(@Nullable INetworkStackStatusCallback cb) {
enforceNetworkStackCallingPermission();
DhcpServer.this.start(cb);
}
@Override
public void startWithCallbacks(@Nullable INetworkStackStatusCallback statusCb,
@Nullable IDhcpEventCallbacks eventCb) {
enforceNetworkStackCallingPermission();
DhcpServer.this.start(statusCb, eventCb);
}
@Override
public void updateParams(@Nullable DhcpServingParamsParcel params,
@Nullable INetworkStackStatusCallback cb) {
enforceNetworkStackCallingPermission();
DhcpServer.this.updateParams(params, cb);
}
@Override
public void stop(@Nullable INetworkStackStatusCallback cb) {
enforceNetworkStackCallingPermission();
DhcpServer.this.stop(cb);
}
@Override
public int getInterfaceVersion() {
return this.VERSION;
}
@Override
public String getInterfaceHash() {
return this.HASH;
}
}
/**
* Start listening for and responding to packets.
*
* <p>It is not legal to call this method more than once; in particular the server cannot be
* restarted after being stopped.
*/
void start(@Nullable INetworkStackStatusCallback cb) {
start(cb, null);
}
/**
* Start listening for and responding to packets, with optional callbacks for lease events.
*
* <p>It is not legal to call this method more than once; in particular the server cannot be
* restarted after being stopped.
*/
void start(@Nullable INetworkStackStatusCallback statusCb,
@Nullable IDhcpEventCallbacks eventCb) {
sendMessage(CMD_START_DHCP_SERVER, new Pair<>(statusCb, eventCb));
}
/**
* Update serving parameters. All subsequently received requests will be handled with the new
* parameters, and current leases that are incompatible with the new parameters are dropped.
*/
void updateParams(@Nullable DhcpServingParamsParcel params,
@Nullable INetworkStackStatusCallback cb) {
final DhcpServingParams parsedParams;
try {
// throws InvalidParameterException with null params
parsedParams = DhcpServingParams.fromParcelableObject(params);
} catch (DhcpServingParams.InvalidParameterException e) {
mLog.e("Invalid parameters sent to DhcpServer", e);
maybeNotifyStatus(cb, STATUS_INVALID_ARGUMENT);
return;
}
sendMessage(CMD_UPDATE_PARAMS, new Pair<>(parsedParams, cb));
}
/**
* Stop listening for packets.
*
* <p>As the server is stopped asynchronously, some packets may still be processed shortly after
* calling this method. The server will also be cleaned up and can't be started again, even if
* it was already stopped.
*/
void stop(@Nullable INetworkStackStatusCallback cb) {
sendMessage(CMD_STOP_DHCP_SERVER, cb);
sendMessage(CMD_TERMINATE_AFTER_STOP);
}
private void maybeNotifyStatus(@Nullable INetworkStackStatusCallback cb, int statusCode) {
if (cb == null) return;
try {
cb.onStatusAvailable(statusCode);
} catch (RemoteException e) {
mLog.e("Could not send status back to caller", e);
}
}
private void handleUpdateServingParams(@NonNull DhcpServingParams params,
@Nullable INetworkStackStatusCallback cb) {
mServingParams = params;
mLeaseRepo.updateParams(
DhcpServingParams.makeIpPrefix(params.serverAddr),
params.excludedAddrs,
params.dhcpLeaseTimeSecs * 1000,
params.singleClientAddr);
maybeNotifyStatus(cb, STATUS_SUCCESS);
}
class StoppedState extends State {
private INetworkStackStatusCallback mOnStopCallback;
@Override
public void enter() {
maybeNotifyStatus(mOnStopCallback, STATUS_SUCCESS);
mOnStopCallback = null;
}
@Override
public boolean processMessage(Message msg) {
switch (msg.what) {
case CMD_START_DHCP_SERVER:
final Pair<INetworkStackStatusCallback, IDhcpEventCallbacks> obj =
(Pair<INetworkStackStatusCallback, IDhcpEventCallbacks>) msg.obj;
mStartedState.mOnStartCallback = obj.first;
mEventCallbacks = obj.second;
transitionTo(mRunningState);
return HANDLED;
case CMD_TERMINATE_AFTER_STOP:
quit();
return HANDLED;
default:
return NOT_HANDLED;
}
}
}
class StartedState extends State {
private INetworkStackStatusCallback mOnStartCallback;
@Override
public void enter() {
if (mPacketListener != null) {
mLog.e("Starting DHCP server more than once is not supported.");
maybeNotifyStatus(mOnStartCallback, STATUS_UNKNOWN_ERROR);
mOnStartCallback = null;
return;
}
mPacketListener = mDeps.makePacketListener(getHandler());
if (!mPacketListener.start()) {
mLog.e("Fail to start DHCP Packet Listener, rollback to StoppedState");
deferMessage(obtainMessage(CMD_STOP_DHCP_SERVER, null));
maybeNotifyStatus(mOnStartCallback, STATUS_UNKNOWN_ERROR);
mOnStartCallback = null;
return;
}
if (mEventCallbacks != null) {
mLeaseRepo.addLeaseCallbacks(mEventCallbacks);
}
maybeNotifyStatus(mOnStartCallback, STATUS_SUCCESS);
// Clear INetworkStackStatusCallback binder token, so that it's freed
// on the other side.
mOnStartCallback = null;
}
@Override
public boolean processMessage(Message msg) {
switch (msg.what) {
case CMD_UPDATE_PARAMS:
final Pair<DhcpServingParams, INetworkStackStatusCallback> pair =
(Pair<DhcpServingParams, INetworkStackStatusCallback>) msg.obj;
handleUpdateServingParams(pair.first, pair.second);
return HANDLED;
case CMD_START_DHCP_SERVER:
mLog.e("ALERT: START received in StartedState. Please fix caller.");
return HANDLED;
case CMD_STOP_DHCP_SERVER:
mStoppedState.mOnStopCallback = (INetworkStackStatusCallback) msg.obj;
transitionTo(mStoppedState);
return HANDLED;
default:
return NOT_HANDLED;
}
}
@Override
public void exit() {
mPacketListener.stop();
mLog.logf("DHCP Packet Listener stopped");
}
}
class RunningState extends State {
@Override
public boolean processMessage(Message msg) {
switch (msg.what) {
case CMD_RECEIVE_PACKET:
processPacket((DhcpPacket) msg.obj);
return HANDLED;
default:
// Fall through to StartedState.
return NOT_HANDLED;
}
}
private void processPacket(@NonNull DhcpPacket packet) {
mLog.log("Received packet of type " + packet.getClass().getSimpleName());
final Inet4Address sid = packet.mServerIdentifier;
if (sid != null && !sid.equals(mServingParams.serverAddr.getAddress())) {
mLog.log("Packet ignored due to wrong server identifier: " + sid);
return;
}
try {
if (packet instanceof DhcpDiscoverPacket) {
processDiscover((DhcpDiscoverPacket) packet);
} else if (packet instanceof DhcpRequestPacket) {
processRequest((DhcpRequestPacket) packet);
} else if (packet instanceof DhcpReleasePacket) {
processRelease((DhcpReleasePacket) packet);
} else if (packet instanceof DhcpDeclinePacket) {
processDecline((DhcpDeclinePacket) packet);
} else {
mLog.e("Unknown packet type: " + packet.getClass().getSimpleName());
}
} catch (MalformedPacketException e) {
// Not an internal error: only logging exception message, not stacktrace
mLog.e("Ignored malformed packet: " + e.getMessage());
}
}
private void logIgnoredPacketInvalidSubnet(DhcpLeaseRepository.InvalidSubnetException e) {
// Not an internal error: only logging exception message, not stacktrace
mLog.e("Ignored packet from invalid subnet: " + e.getMessage());
}
private void processDiscover(@NonNull DhcpDiscoverPacket packet)
throws MalformedPacketException {
final DhcpLease lease;
final MacAddress clientMac = getMacAddr(packet);
try {
if (mDhcpRapidCommitEnabled && packet.mRapidCommit) {
lease = mLeaseRepo.getCommittedLease(packet.getExplicitClientIdOrNull(),
clientMac, packet.mRelayIp, packet.mHostName);
transmitAck(packet, lease, clientMac);
} else {
lease = mLeaseRepo.getOffer(packet.getExplicitClientIdOrNull(), clientMac,
packet.mRelayIp, packet.mRequestedIp, packet.mHostName);
transmitOffer(packet, lease, clientMac);
}
} catch (DhcpLeaseRepository.OutOfAddressesException e) {
transmitNak(packet, "Out of addresses to offer");
} catch (DhcpLeaseRepository.InvalidSubnetException e) {
logIgnoredPacketInvalidSubnet(e);
}
}
private void processRequest(@NonNull DhcpRequestPacket packet)
throws MalformedPacketException {
// If set, packet SID matches with this server's ID as checked in processPacket().
final boolean sidSet = packet.mServerIdentifier != null;
final DhcpLease lease;
final MacAddress clientMac = getMacAddr(packet);
try {
lease = mLeaseRepo.requestLease(packet.getExplicitClientIdOrNull(), clientMac,
packet.mClientIp, packet.mRelayIp, packet.mRequestedIp, sidSet,
packet.mHostName);
} catch (DhcpLeaseRepository.InvalidAddressException e) {
transmitNak(packet, "Invalid requested address");
return;
} catch (DhcpLeaseRepository.InvalidSubnetException e) {
logIgnoredPacketInvalidSubnet(e);
return;
}
transmitAck(packet, lease, clientMac);
}
private void processRelease(@NonNull DhcpReleasePacket packet)
throws MalformedPacketException {
final byte[] clientId = packet.getExplicitClientIdOrNull();
final MacAddress macAddr = getMacAddr(packet);
// Don't care about success (there is no ACK/NAK); logging is already done
// in the repository.
mLeaseRepo.releaseLease(clientId, macAddr, packet.mClientIp);
}
private void processDecline(@NonNull DhcpDeclinePacket packet)
throws MalformedPacketException {
final byte[] clientId = packet.getExplicitClientIdOrNull();
final MacAddress macAddr = getMacAddr(packet);
int committedLeasesCount = mLeaseRepo.getCommittedLeases().size();
// If peer's clientID and macAddr doesn't match with any issued lease, nothing to do.
if (!mLeaseRepo.markAndReleaseDeclinedLease(clientId, macAddr, packet.mRequestedIp)) {
return;
}
// Check whether the boolean flag which requests a new prefix is enabled, and if
// it's enabled, make sure the issued lease count should be only one, otherwise,
// changing a different prefix will cause other exist host(s) configured with the
// current prefix lose appropriate route.
if (!mServingParams.changePrefixOnDecline || committedLeasesCount > 1) return;
if (mEventCallbacks == null) {
mLog.e("changePrefixOnDecline enabled but caller didn't pass a valid"
+ "IDhcpEventCallbacks callback.");
return;
}
try {
mEventCallbacks.onNewPrefixRequest(
DhcpServingParams.makeIpPrefix(mServingParams.serverAddr));
transitionTo(mWaitBeforeRetrievalState);
} catch (RemoteException e) {
mLog.e("could not request a new prefix to caller", e);
}
}
}
class WaitBeforeRetrievalState extends State {
@Override
public boolean processMessage(Message msg) {
switch (msg.what) {
case CMD_UPDATE_PARAMS:
final Pair<DhcpServingParams, INetworkStackStatusCallback> pair =
(Pair<DhcpServingParams, INetworkStackStatusCallback>) msg.obj;
final IpPrefix currentPrefix =
DhcpServingParams.makeIpPrefix(mServingParams.serverAddr);
final IpPrefix newPrefix =
DhcpServingParams.makeIpPrefix(pair.first.serverAddr);
handleUpdateServingParams(pair.first, pair.second);
if (currentPrefix != null && !currentPrefix.equals(newPrefix)) {
transitionTo(mRunningState);
}
return HANDLED;
case CMD_RECEIVE_PACKET:
deferMessage(msg);
return HANDLED;
default:
// Fall through to StartedState.
return NOT_HANDLED;
}
}
}
private Inet4Address getAckOrOfferDst(@NonNull DhcpPacket request, @NonNull DhcpLease lease,
boolean broadcastFlag) {
// Unless relayed or broadcast, send to client IP if already configured on the client, or to
// the lease address if the client has no configured address
if (!isEmpty(request.mRelayIp)) {
return request.mRelayIp;
} else if (broadcastFlag) {
return IPV4_ADDR_ALL;
} else if (!isEmpty(request.mClientIp)) {
return request.mClientIp;
} else {
return lease.getNetAddr();
}
}
/**
* Determine whether the broadcast flag should be set in the BOOTP packet flags. This does not
* apply to NAK responses, which should always have it set.
*/
private static boolean getBroadcastFlag(@NonNull DhcpPacket request, @NonNull DhcpLease lease) {
// No broadcast flag if the client already has a configured IP to unicast to. RFC2131 #4.1
// has some contradictions regarding broadcast behavior if a client already has an IP
// configured and sends a request with both ciaddr (renew/rebind) and the broadcast flag
// set. Sending a unicast response to ciaddr matches previous behavior and is more
// efficient.
// If the client has no configured IP, broadcast if requested by the client or if the lease
// address cannot be used to send a unicast reply either.
return isEmpty(request.mClientIp) && (request.mBroadcast || isEmpty(lease.getNetAddr()));
}
/**
* Get the hostname from a lease if non-empty and requested in the incoming request.
* @param request The incoming request.
* @return The hostname, or null if not requested or empty.
*/
@Nullable
private static String getHostnameIfRequested(@NonNull DhcpPacket request,
@NonNull DhcpLease lease) {
return request.hasRequestedParam(DHCP_HOST_NAME) && !TextUtils.isEmpty(lease.getHostname())
? lease.getHostname()
: null;
}
private boolean transmitOffer(@NonNull DhcpPacket request, @NonNull DhcpLease lease,
@NonNull MacAddress clientMac) {
final boolean broadcastFlag = getBroadcastFlag(request, lease);
final int timeout = getLeaseTimeout(lease);
final Inet4Address prefixMask =
getPrefixMaskAsInet4Address(mServingParams.serverAddr.getPrefixLength());
final Inet4Address broadcastAddr = getBroadcastAddress(
mServingParams.getServerInet4Addr(), mServingParams.serverAddr.getPrefixLength());
final String hostname = getHostnameIfRequested(request, lease);
final ByteBuffer offerPacket = DhcpPacket.buildOfferPacket(
ENCAP_BOOTP, request.mTransId, broadcastFlag, mServingParams.getServerInet4Addr(),
request.mRelayIp, lease.getNetAddr(), request.mClientMac, timeout, prefixMask,
broadcastAddr, new ArrayList<>(mServingParams.defaultRouters),
new ArrayList<>(mServingParams.dnsServers),
mServingParams.getServerInet4Addr(), null /* domainName */, hostname,
mServingParams.metered, (short) mServingParams.linkMtu,
// TODO (b/144402437): advertise the URL if known
null /* captivePortalApiUrl */);
return transmitOfferOrAckPacket(offerPacket, request, lease, clientMac, broadcastFlag);
}
private boolean transmitAck(@NonNull DhcpPacket packet, @NonNull DhcpLease lease,
@NonNull MacAddress clientMac) {
// TODO: replace DhcpPacket's build methods with real builders and use common code with
// transmitOffer above
final boolean broadcastFlag = getBroadcastFlag(packet, lease);
final int timeout = getLeaseTimeout(lease);
final String hostname = getHostnameIfRequested(packet, lease);
final ByteBuffer ackPacket = DhcpPacket.buildAckPacket(ENCAP_BOOTP, packet.mTransId,
broadcastFlag, mServingParams.getServerInet4Addr(), packet.mRelayIp,
lease.getNetAddr(), packet.mClientIp, packet.mClientMac, timeout,
mServingParams.getPrefixMaskAsAddress(), mServingParams.getBroadcastAddress(),
new ArrayList<>(mServingParams.defaultRouters),
new ArrayList<>(mServingParams.dnsServers),
mServingParams.getServerInet4Addr(), null /* domainName */, hostname,
mServingParams.metered, (short) mServingParams.linkMtu,
// TODO (b/144402437): advertise the URL if known
packet.mRapidCommit && mDhcpRapidCommitEnabled, null /* captivePortalApiUrl */);
return transmitOfferOrAckPacket(ackPacket, packet, lease, clientMac, broadcastFlag);
}
private boolean transmitNak(DhcpPacket request, String message) {
mLog.w("Transmitting NAK: " + message);
// Always set broadcast flag for NAK: client may not have a correct IP
final ByteBuffer nakPacket = DhcpPacket.buildNakPacket(
ENCAP_BOOTP, request.mTransId, mServingParams.getServerInet4Addr(),
request.mRelayIp, request.mClientMac, true /* broadcast */, message);
final Inet4Address dst = isEmpty(request.mRelayIp)
? IPV4_ADDR_ALL
: request.mRelayIp;
return transmitPacket(nakPacket, DhcpNakPacket.class.getSimpleName(), dst);
}
private boolean transmitOfferOrAckPacket(@NonNull ByteBuffer buf, @NonNull DhcpPacket request,
@NonNull DhcpLease lease, @NonNull MacAddress clientMac, boolean broadcastFlag) {
mLog.logf("Transmitting %s with lease %s", request.getClass().getSimpleName(), lease);
// Client may not yet respond to ARP for the lease address, which may be the destination
// address. Add an entry to the ARP cache to save future ARP probes and make sure the
// packet reaches its destination.
if (!addArpEntry(clientMac, lease.getNetAddr())) {
// Logging for error already done
return false;
}
final Inet4Address dst = getAckOrOfferDst(request, lease, broadcastFlag);
return transmitPacket(buf, request.getClass().getSimpleName(), dst);
}
private boolean transmitPacket(@NonNull ByteBuffer buf, @NonNull String packetTypeTag,
@NonNull Inet4Address dst) {
try {
mDeps.sendPacket(mSocket, buf, dst);
} catch (ErrnoException | IOException e) {
mLog.e("Can't send packet " + packetTypeTag, e);
return false;
}
return true;
}
private boolean addArpEntry(@NonNull MacAddress macAddr, @NonNull Inet4Address inetAddr) {
try {
mDeps.addArpEntry(inetAddr, macAddr, mIfName, mSocket);
return true;
} catch (IOException e) {
mLog.e("Error adding client to ARP table", e);
return false;
}
}
/**
* Get the remaining lease time in seconds, starting from {@link Clock#elapsedRealtime()}.
*
* <p>This is an unsigned 32-bit integer, so it cannot be read as a standard (signed) Java int.
* The return value is only intended to be used to populate the lease time field in a DHCP
* response, considering that lease time is an unsigned 32-bit integer field in DHCP packets.
*
* <p>Lease expiration times are tracked internally with millisecond precision: this method
* returns a rounded down value.
*/
private int getLeaseTimeout(@NonNull DhcpLease lease) {
final long remainingTimeSecs = (lease.getExpTime() - mClock.elapsedRealtime()) / 1000;
if (remainingTimeSecs < 0) {
mLog.e("Processing expired lease " + lease);
return EXPIRED_FALLBACK_LEASE_TIME_SECS;
}
if (remainingTimeSecs >= toUnsignedLong(INFINITE_LEASE)) {
return INFINITE_LEASE;
}
return (int) remainingTimeSecs;
}
/**
* Get the client MAC address from a packet.
*
* @throws MalformedPacketException The address in the packet uses an unsupported format.
*/
@NonNull
private MacAddress getMacAddr(@NonNull DhcpPacket packet) throws MalformedPacketException {
try {
return MacAddress.fromBytes(packet.getClientMac());
} catch (IllegalArgumentException e) {
final String message = "Invalid MAC address in packet: "
+ HexDump.dumpHexString(packet.getClientMac());
throw new MalformedPacketException(message, e);
}
}
private static boolean isEmpty(@Nullable Inet4Address address) {
return address == null || IPV4_ADDR_ANY.equals(address);
}
private class PacketListener extends DhcpPacketListener {
PacketListener(Handler handler) {
super(handler);
}
@Override
protected void onReceive(@NonNull DhcpPacket packet, @NonNull Inet4Address srcAddr,
int srcPort) {
if (srcPort != DHCP_CLIENT) {
final String packetType = packet.getClass().getSimpleName();
mLog.logf("Ignored packet of type %s sent from client port %d",
packetType, srcPort);
return;
}
sendMessage(CMD_RECEIVE_PACKET, packet);
}
@Override
protected void logError(@NonNull String msg, Exception e) {
mLog.e("Error receiving packet: " + msg, e);
}
@Override
protected void logParseError(@NonNull byte[] packet, int length,
@NonNull DhcpPacket.ParseException e) {
mLog.e("Error parsing packet", e);
}
@Override
protected FileDescriptor createFd() {
// TODO: have and use an API to set a socket tag without going through the thread tag
final int oldTag = TrafficStats.getAndSetThreadStatsTag(TAG_SYSTEM_DHCP_SERVER);
try {
mSocket = Os.socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, IPPROTO_UDP);
SocketUtils.bindSocketToInterface(mSocket, mIfName);
Os.setsockoptInt(mSocket, SOL_SOCKET, SO_REUSEADDR, 1);
Os.setsockoptInt(mSocket, SOL_SOCKET, SO_BROADCAST, 1);
Os.bind(mSocket, IPV4_ADDR_ANY, DHCP_SERVER);
return mSocket;
} catch (IOException | ErrnoException e) {
mLog.e("Error creating UDP socket", e);
return null;
} finally {
TrafficStats.setThreadStatsTag(oldTag);
}
}
}
}