/* * 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.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.net.module.util.NetworkStackConstants.TAG_SYSTEM_DHCP_SERVER; 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. * *
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. * *
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. * *
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. * *
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. * *
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. * *
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 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.
*
* 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);
}
}
}
}