Merge "Add DhcpServer"
This commit is contained in:
@@ -139,6 +139,8 @@ public class TrafficStats {
|
||||
public static final int TAG_SYSTEM_GPS = 0xFFFFFF44;
|
||||
/** @hide */
|
||||
public static final int TAG_SYSTEM_PAC = 0xFFFFFF45;
|
||||
/** @hide */
|
||||
public static final int TAG_SYSTEM_DHCP_SERVER = 0xFFFFFF46;
|
||||
|
||||
private static INetworkStatsService sStatsService;
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import android.annotation.Nullable;
|
||||
import android.net.IpPrefix;
|
||||
import android.net.MacAddress;
|
||||
import android.net.util.SharedLog;
|
||||
import android.os.SystemClock;
|
||||
import android.net.dhcp.DhcpServer.Clock;
|
||||
import android.util.ArrayMap;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
@@ -73,15 +73,6 @@ class DhcpLeaseRepository {
|
||||
private int mNumAddresses;
|
||||
private long mLeaseTimeMs;
|
||||
|
||||
public static class Clock {
|
||||
/**
|
||||
* @see SystemClock#elapsedRealtime()
|
||||
*/
|
||||
public long elapsedRealtime() {
|
||||
return SystemClock.elapsedRealtime();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Next timestamp when committed or declined leases should be checked for expired ones. This
|
||||
* will always be lower than or equal to the time for the first lease to expire: it's OK not to
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.os.Build;
|
||||
import android.os.SystemProperties;
|
||||
import android.system.OsConstants;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
@@ -350,6 +351,14 @@ public abstract class DhcpPacket {
|
||||
return mClientId != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to return the client ID if it was set explicitly, or null otherwise.
|
||||
*/
|
||||
@Nullable
|
||||
public byte[] getExplicitClientIdOrNull() {
|
||||
return hasExplicitClientId() ? getClientId() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the client ID. If not set explicitly, this follows RFC 2132 and creates a client ID
|
||||
* based on the hardware address.
|
||||
|
||||
511
services/net/java/android/net/dhcp/DhcpServer.java
Normal file
511
services/net/java/android/net/dhcp/DhcpServer.java
Normal file
@@ -0,0 +1,511 @@
|
||||
/*
|
||||
* 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.NetworkUtils.getBroadcastAddress;
|
||||
import static android.net.NetworkUtils.getPrefixMaskAsInet4Address;
|
||||
import static android.net.TrafficStats.TAG_SYSTEM_DHCP_SERVER;
|
||||
import static android.net.dhcp.DhcpPacket.DHCP_SERVER;
|
||||
import static android.net.dhcp.DhcpPacket.ENCAP_BOOTP;
|
||||
import static android.net.dhcp.DhcpPacket.INFINITE_LEASE;
|
||||
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.SOL_SOCKET;
|
||||
import static android.system.OsConstants.SO_BINDTODEVICE;
|
||||
import static android.system.OsConstants.SO_BROADCAST;
|
||||
import static android.system.OsConstants.SO_REUSEADDR;
|
||||
|
||||
import static java.lang.Integer.toUnsignedLong;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.net.MacAddress;
|
||||
import android.net.NetworkUtils;
|
||||
import android.net.TrafficStats;
|
||||
import android.net.util.InterfaceParams;
|
||||
import android.net.util.SharedLog;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.SystemClock;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.util.HexDump;
|
||||
|
||||
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 is single-threaded (including send/receive operations): all internal operations are
|
||||
* done on the provided {@link Looper}. Public methods are thread-safe and will schedule operations
|
||||
* on the looper asynchronously.
|
||||
* @hide
|
||||
*/
|
||||
public class DhcpServer {
|
||||
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;
|
||||
|
||||
@NonNull
|
||||
private final ServerHandler mHandler;
|
||||
@NonNull
|
||||
private final InterfaceParams mIface;
|
||||
@NonNull
|
||||
private final DhcpLeaseRepository mLeaseRepo;
|
||||
@NonNull
|
||||
private final SharedLog mLog;
|
||||
@NonNull
|
||||
private final Dependencies mDeps;
|
||||
@NonNull
|
||||
private final Clock mClock;
|
||||
@NonNull
|
||||
private final DhcpPacketListener mPacketListener;
|
||||
|
||||
@Nullable
|
||||
private FileDescriptor mSocket;
|
||||
@NonNull
|
||||
private DhcpServingParams mServingParams;
|
||||
|
||||
public static class Clock {
|
||||
/**
|
||||
* @see SystemClock#elapsedRealtime()
|
||||
*/
|
||||
public long elapsedRealtime() {
|
||||
return SystemClock.elapsedRealtime();
|
||||
}
|
||||
}
|
||||
|
||||
public interface Dependencies {
|
||||
void sendPacket(@NonNull FileDescriptor fd, @NonNull ByteBuffer buffer,
|
||||
@NonNull InetAddress dst) throws ErrnoException, IOException;
|
||||
DhcpLeaseRepository makeLeaseRepository(@NonNull DhcpServingParams servingParams,
|
||||
@NonNull SharedLog log, @NonNull Clock clock);
|
||||
DhcpPacketListener makePacketListener();
|
||||
Clock makeClock();
|
||||
void addArpEntry(@NonNull Inet4Address ipv4Addr, @NonNull MacAddress ethAddr,
|
||||
@NonNull String ifname, @NonNull FileDescriptor fd) throws IOException;
|
||||
}
|
||||
|
||||
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, log.forSubComponent(REPO_TAG), clock);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DhcpPacketListener makePacketListener() {
|
||||
return new PacketListener();
|
||||
}
|
||||
|
||||
@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 {
|
||||
NetworkUtils.addArpEntry(ipv4Addr, ethAddr, ifname, fd);
|
||||
}
|
||||
}
|
||||
|
||||
private static class MalformedPacketException extends Exception {
|
||||
MalformedPacketException(String message, Throwable t) {
|
||||
super(message, t);
|
||||
}
|
||||
}
|
||||
|
||||
public DhcpServer(@NonNull Looper looper, @NonNull InterfaceParams iface,
|
||||
@NonNull DhcpServingParams params, @NonNull SharedLog log) {
|
||||
this(looper, iface, params, log, null);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
DhcpServer(@NonNull Looper looper, @NonNull InterfaceParams iface,
|
||||
@NonNull DhcpServingParams params, @NonNull SharedLog log,
|
||||
@Nullable Dependencies deps) {
|
||||
if (deps == null) {
|
||||
deps = new DependenciesImpl();
|
||||
}
|
||||
mHandler = new ServerHandler(looper);
|
||||
mIface = iface;
|
||||
mServingParams = params;
|
||||
mLog = log;
|
||||
mDeps = deps;
|
||||
mClock = deps.makeClock();
|
||||
mPacketListener = deps.makePacketListener();
|
||||
mLeaseRepo = deps.makeLeaseRepository(mServingParams, mLog, mClock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start listening for and responding to packets.
|
||||
*/
|
||||
public void start() {
|
||||
mHandler.sendEmptyMessage(CMD_START_DHCP_SERVER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public void updateParams(@NonNull DhcpServingParams params) {
|
||||
sendMessage(CMD_UPDATE_PARAMS, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop listening for packets.
|
||||
*
|
||||
* <p>As the server is stopped asynchronously, some packets may still be processed shortly after
|
||||
* calling this method.
|
||||
*/
|
||||
public void stop() {
|
||||
mHandler.sendEmptyMessage(CMD_STOP_DHCP_SERVER);
|
||||
}
|
||||
|
||||
private void sendMessage(int what, @Nullable Object obj) {
|
||||
mHandler.sendMessage(mHandler.obtainMessage(what, obj));
|
||||
}
|
||||
|
||||
private class ServerHandler extends Handler {
|
||||
public ServerHandler(@NonNull Looper looper) {
|
||||
super(looper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(@NonNull Message msg) {
|
||||
switch (msg.what) {
|
||||
case CMD_UPDATE_PARAMS:
|
||||
final DhcpServingParams params = (DhcpServingParams) msg.obj;
|
||||
mServingParams = params;
|
||||
mLeaseRepo.updateParams(
|
||||
DhcpServingParams.makeIpPrefix(mServingParams.serverAddr),
|
||||
params.excludedAddrs,
|
||||
params.dhcpLeaseTimeSecs);
|
||||
break;
|
||||
case CMD_START_DHCP_SERVER:
|
||||
// This is a no-op if the listener is already started
|
||||
mPacketListener.start();
|
||||
break;
|
||||
case CMD_STOP_DHCP_SERVER:
|
||||
// This is a no-op if the listener was not started
|
||||
mPacketListener.stop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
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 {
|
||||
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 processDiscover(@NonNull DhcpDiscoverPacket packet)
|
||||
throws MalformedPacketException {
|
||||
final DhcpLease lease;
|
||||
final MacAddress clientMac = getMacAddr(packet);
|
||||
try {
|
||||
lease = mLeaseRepo.getOffer(packet.getExplicitClientIdOrNull(), clientMac,
|
||||
packet.mRelayIp, packet.mRequestedIp, packet.mHostName);
|
||||
} catch (DhcpLeaseRepository.OutOfAddressesException e) {
|
||||
transmitNak(packet, "Out of addresses to offer");
|
||||
return;
|
||||
} catch (DhcpLeaseRepository.InvalidAddressException e) {
|
||||
transmitNak(packet, "Lease requested from an invalid subnet");
|
||||
return;
|
||||
}
|
||||
|
||||
transmitOffer(packet, lease, clientMac);
|
||||
}
|
||||
|
||||
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.mRequestedIp, sidSet, packet.mHostName);
|
||||
} catch (DhcpLeaseRepository.InvalidAddressException e) {
|
||||
transmitNak(packet, "Invalid requested address");
|
||||
return;
|
||||
}
|
||||
|
||||
transmitAck(packet, lease, clientMac);
|
||||
}
|
||||
|
||||
private void processRelease(@Nullable 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 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 (Inet4Address) Inet4Address.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()));
|
||||
}
|
||||
|
||||
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 ByteBuffer offerPacket = DhcpPacket.buildOfferPacket(
|
||||
ENCAP_BOOTP, request.mTransId, broadcastFlag, mServingParams.getServerInet4Addr(),
|
||||
lease.getNetAddr(), request.mClientMac, timeout,
|
||||
prefixMask,
|
||||
broadcastAddr,
|
||||
new ArrayList<>(mServingParams.defaultRouters),
|
||||
new ArrayList<>(mServingParams.dnsServers),
|
||||
mServingParams.getServerInet4Addr(), null /* domainName */);
|
||||
|
||||
return transmitOfferOrAckPacket(offerPacket, request, lease, clientMac, broadcastFlag);
|
||||
}
|
||||
|
||||
private boolean transmitAck(@NonNull DhcpPacket request, @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(request, lease);
|
||||
final int timeout = getLeaseTimeout(lease);
|
||||
final ByteBuffer ackPacket = DhcpPacket.buildAckPacket(ENCAP_BOOTP, request.mTransId,
|
||||
broadcastFlag, mServingParams.getServerInet4Addr(), lease.getNetAddr(),
|
||||
request.mClientMac, timeout, mServingParams.getPrefixMaskAsAddress(),
|
||||
mServingParams.getBroadcastAddress(),
|
||||
new ArrayList<>(mServingParams.defaultRouters),
|
||||
new ArrayList<>(mServingParams.dnsServers),
|
||||
mServingParams.getServerInet4Addr(), null /* domainName */);
|
||||
|
||||
return transmitOfferOrAckPacket(ackPacket, request, 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.mClientMac, true /* broadcast */, message);
|
||||
|
||||
final Inet4Address dst = isEmpty(request.mRelayIp)
|
||||
? (Inet4Address) Inet4Address.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, mIface.name, 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(@NonNull Inet4Address address) {
|
||||
return address == null || Inet4Address.ANY.equals(address);
|
||||
}
|
||||
|
||||
private class PacketListener extends DhcpPacketListener {
|
||||
public PacketListener() {
|
||||
super(mHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceive(DhcpPacket packet, Inet4Address srcAddr) {
|
||||
processPacket(packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void logError(String msg, Exception e) {
|
||||
mLog.e("Error receiving packet: " + msg, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void logParseError(byte[] packet, int length, 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, IPPROTO_UDP);
|
||||
Os.setsockoptInt(mSocket, SOL_SOCKET, SO_REUSEADDR, 1);
|
||||
// SO_BINDTODEVICE actually takes a string. This works because the first member
|
||||
// of struct ifreq is a NULL-terminated interface name.
|
||||
// TODO: add a setsockoptString()
|
||||
Os.setsockoptIfreq(mSocket, SOL_SOCKET, SO_BINDTODEVICE, mIface.name);
|
||||
Os.setsockoptInt(mSocket, SOL_SOCKET, SO_BROADCAST, 1);
|
||||
Os.bind(mSocket, Inet4Address.ANY, DHCP_SERVER);
|
||||
NetworkUtils.protectFromVpn(mSocket);
|
||||
|
||||
return mSocket;
|
||||
} catch (IOException | ErrnoException e) {
|
||||
mLog.e("Error creating UDP socket", e);
|
||||
DhcpServer.this.stop();
|
||||
return null;
|
||||
} finally {
|
||||
TrafficStats.setThreadStatsTag(oldTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,7 @@ public abstract class FdEventsReader<BufferType> {
|
||||
mBuffer = buffer;
|
||||
}
|
||||
|
||||
public final void start() {
|
||||
public void start() {
|
||||
if (onCorrectThread()) {
|
||||
createAndRegisterFd();
|
||||
} else {
|
||||
@@ -100,7 +100,7 @@ public abstract class FdEventsReader<BufferType> {
|
||||
}
|
||||
}
|
||||
|
||||
public final void stop() {
|
||||
public void stop() {
|
||||
if (onCorrectThread()) {
|
||||
unregisterAndDestroyFd();
|
||||
} else {
|
||||
|
||||
@@ -34,8 +34,8 @@ import static java.net.InetAddress.parseNumericAddress;
|
||||
import android.annotation.NonNull;
|
||||
import android.net.IpPrefix;
|
||||
import android.net.MacAddress;
|
||||
import android.net.dhcp.DhcpLeaseRepository.Clock;
|
||||
import android.net.util.SharedLog;
|
||||
import android.net.dhcp.DhcpServer.Clock;
|
||||
import android.support.test.filters.SmallTest;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
|
||||
|
||||
301
tests/net/java/android/net/dhcp/DhcpServerTest.java
Normal file
301
tests/net/java/android/net/dhcp/DhcpServerTest.java
Normal file
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
* 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.ENCAP_BOOTP;
|
||||
import static android.net.dhcp.DhcpPacket.INADDR_ANY;
|
||||
import static android.net.dhcp.DhcpPacket.INADDR_BROADCAST;
|
||||
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
import static junit.framework.Assert.assertFalse;
|
||||
import static junit.framework.Assert.assertNotNull;
|
||||
import static junit.framework.Assert.assertNull;
|
||||
import static junit.framework.Assert.assertTrue;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import static java.net.InetAddress.parseNumericAddress;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.net.LinkAddress;
|
||||
import android.net.MacAddress;
|
||||
import android.net.dhcp.DhcpLeaseRepository.InvalidAddressException;
|
||||
import android.net.dhcp.DhcpLeaseRepository.OutOfAddressesException;
|
||||
import android.net.dhcp.DhcpServer.Clock;
|
||||
import android.net.dhcp.DhcpServer.Dependencies;
|
||||
import android.net.util.InterfaceParams;
|
||||
import android.net.util.SharedLog;
|
||||
import android.os.test.TestLooper;
|
||||
import android.support.test.filters.SmallTest;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@SmallTest
|
||||
public class DhcpServerTest {
|
||||
private static final String PROP_DEXMAKER_SHARE_CLASSLOADER = "dexmaker.share_classloader";
|
||||
private static final String TEST_IFACE = "testiface";
|
||||
private static final MacAddress TEST_IFACE_MAC = MacAddress.fromString("11:22:33:44:55:66");
|
||||
private static final InterfaceParams TEST_IFACEPARAMS =
|
||||
new InterfaceParams(TEST_IFACE, 1, TEST_IFACE_MAC);
|
||||
|
||||
private static final Inet4Address TEST_SERVER_ADDR = parseAddr("192.168.0.2");
|
||||
private static final LinkAddress TEST_SERVER_LINKADDR = new LinkAddress(TEST_SERVER_ADDR, 20);
|
||||
private static final Set<Inet4Address> TEST_DEFAULT_ROUTERS = new HashSet<>(
|
||||
Arrays.asList(parseAddr("192.168.0.123"), parseAddr("192.168.0.124")));
|
||||
private static final Set<Inet4Address> TEST_DNS_SERVERS = new HashSet<>(
|
||||
Arrays.asList(parseAddr("192.168.0.126"), parseAddr("192.168.0.127")));
|
||||
private static final Set<Inet4Address> TEST_EXCLUDED_ADDRS = new HashSet<>(
|
||||
Arrays.asList(parseAddr("192.168.0.200"), parseAddr("192.168.0.201")));
|
||||
private static final long TEST_LEASE_TIME_SECS = 3600L;
|
||||
private static final int TEST_MTU = 1500;
|
||||
|
||||
private static final int TEST_TRANSACTION_ID = 123;
|
||||
private static final byte[] TEST_CLIENT_MAC_BYTES = new byte [] { 1, 2, 3, 4, 5, 6 };
|
||||
private static final MacAddress TEST_CLIENT_MAC = MacAddress.fromBytes(TEST_CLIENT_MAC_BYTES);
|
||||
private static final Inet4Address TEST_CLIENT_ADDR = parseAddr("192.168.0.42");
|
||||
|
||||
private static final long TEST_CLOCK_TIME = 1234L;
|
||||
private static final int TEST_LEASE_EXPTIME_SECS = 3600;
|
||||
private static final DhcpLease TEST_LEASE = new DhcpLease(null, TEST_CLIENT_MAC,
|
||||
TEST_CLIENT_ADDR, TEST_LEASE_EXPTIME_SECS*1000L + TEST_CLOCK_TIME, null /* hostname */);
|
||||
|
||||
@NonNull @Mock
|
||||
private Dependencies mDeps;
|
||||
@NonNull @Mock
|
||||
private DhcpLeaseRepository mRepository;
|
||||
@NonNull @Mock
|
||||
private Clock mClock;
|
||||
@NonNull @Mock
|
||||
private DhcpPacketListener mPacketListener;
|
||||
|
||||
@NonNull @Captor
|
||||
private ArgumentCaptor<ByteBuffer> mSentPacketCaptor;
|
||||
@NonNull @Captor
|
||||
private ArgumentCaptor<Inet4Address> mResponseDstAddrCaptor;
|
||||
|
||||
@NonNull
|
||||
private TestLooper mLooper;
|
||||
@NonNull
|
||||
private DhcpServer mServer;
|
||||
|
||||
@Nullable
|
||||
private String mPrevShareClassloaderProp;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
// Allow mocking package-private classes
|
||||
mPrevShareClassloaderProp = System.getProperty(PROP_DEXMAKER_SHARE_CLASSLOADER);
|
||||
System.setProperty(PROP_DEXMAKER_SHARE_CLASSLOADER, "true");
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
when(mDeps.makeLeaseRepository(any(), any(), any())).thenReturn(mRepository);
|
||||
when(mDeps.makeClock()).thenReturn(mClock);
|
||||
when(mDeps.makePacketListener()).thenReturn(mPacketListener);
|
||||
doNothing().when(mDeps)
|
||||
.sendPacket(any(), mSentPacketCaptor.capture(), mResponseDstAddrCaptor.capture());
|
||||
when(mClock.elapsedRealtime()).thenReturn(TEST_CLOCK_TIME);
|
||||
|
||||
final DhcpServingParams servingParams = new DhcpServingParams.Builder()
|
||||
.setDefaultRouters(TEST_DEFAULT_ROUTERS)
|
||||
.setDhcpLeaseTimeSecs(TEST_LEASE_TIME_SECS)
|
||||
.setDnsServers(TEST_DNS_SERVERS)
|
||||
.setServerAddr(TEST_SERVER_LINKADDR)
|
||||
.setLinkMtu(TEST_MTU)
|
||||
.setExcludedAddrs(TEST_EXCLUDED_ADDRS)
|
||||
.build();
|
||||
|
||||
mLooper = new TestLooper();
|
||||
mServer = new DhcpServer(mLooper.getLooper(), TEST_IFACEPARAMS, servingParams,
|
||||
new SharedLog(DhcpServerTest.class.getSimpleName()), mDeps);
|
||||
|
||||
mServer.start();
|
||||
mLooper.dispatchAll();
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
// Calling stop() several times is not an issue
|
||||
mServer.stop();
|
||||
System.setProperty(PROP_DEXMAKER_SHARE_CLASSLOADER,
|
||||
(mPrevShareClassloaderProp == null ? "" : mPrevShareClassloaderProp));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStart() throws Exception {
|
||||
verify(mPacketListener, times(1)).start();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStop() throws Exception {
|
||||
mServer.stop();
|
||||
mLooper.dispatchAll();
|
||||
verify(mPacketListener, times(1)).stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDiscover() throws Exception {
|
||||
// TODO: refactor packet construction to eliminate unnecessary/confusing/duplicate fields
|
||||
when(mRepository.getOffer(isNull() /* clientId */, eq(TEST_CLIENT_MAC),
|
||||
eq(INADDR_ANY) /* relayAddr */, isNull() /* reqAddr */, isNull() /* hostname */))
|
||||
.thenReturn(TEST_LEASE);
|
||||
|
||||
final DhcpDiscoverPacket discover = new DhcpDiscoverPacket(TEST_TRANSACTION_ID,
|
||||
(short) 0 /* secs */, INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES,
|
||||
false /* broadcast */, INADDR_ANY /* srcIp */);
|
||||
mServer.processPacket(discover);
|
||||
|
||||
assertResponseSentTo(TEST_CLIENT_ADDR);
|
||||
final DhcpOfferPacket packet = assertOffer(getPacket());
|
||||
assertMatchesTestLease(packet);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDiscover_OutOfAddresses() throws Exception {
|
||||
when(mRepository.getOffer(isNull() /* clientId */, eq(TEST_CLIENT_MAC),
|
||||
eq(INADDR_ANY) /* relayAddr */, isNull() /* reqAddr */, isNull() /* hostname */))
|
||||
.thenThrow(new OutOfAddressesException("Test exception"));
|
||||
|
||||
final DhcpDiscoverPacket discover = new DhcpDiscoverPacket(TEST_TRANSACTION_ID,
|
||||
(short) 0 /* secs */, INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES,
|
||||
false /* broadcast */, INADDR_ANY /* srcIp */);
|
||||
mServer.processPacket(discover);
|
||||
|
||||
assertResponseSentTo(INADDR_BROADCAST);
|
||||
final DhcpNakPacket packet = assertNak(getPacket());
|
||||
assertMatchesClient(packet);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequest_Selecting_Ack() throws Exception {
|
||||
when(mRepository.requestLease(isNull() /* clientId */, eq(TEST_CLIENT_MAC),
|
||||
eq(INADDR_ANY) /* clientAddr */, eq(TEST_CLIENT_ADDR) /* reqAddr */,
|
||||
eq(true) /* sidSet */, isNull() /* hostname */))
|
||||
.thenReturn(TEST_LEASE);
|
||||
|
||||
final DhcpRequestPacket request = new DhcpRequestPacket(TEST_TRANSACTION_ID,
|
||||
(short) 0 /* secs */, INADDR_ANY /* clientIp */, INADDR_ANY /* relayIp */,
|
||||
TEST_CLIENT_MAC_BYTES, false /* broadcast */);
|
||||
request.mServerIdentifier = TEST_SERVER_ADDR;
|
||||
request.mRequestedIp = TEST_CLIENT_ADDR;
|
||||
mServer.processPacket(request);
|
||||
|
||||
assertResponseSentTo(TEST_CLIENT_ADDR);
|
||||
final DhcpAckPacket packet = assertAck(getPacket());
|
||||
assertMatchesTestLease(packet);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRequest_Selecting_Nak() throws Exception {
|
||||
when(mRepository.requestLease(isNull(), eq(TEST_CLIENT_MAC),
|
||||
eq(INADDR_ANY) /* clientAddr */, eq(TEST_CLIENT_ADDR) /* reqAddr */,
|
||||
eq(true) /* sidSet */, isNull() /* hostname */))
|
||||
.thenThrow(new InvalidAddressException("Test error"));
|
||||
|
||||
final DhcpRequestPacket request = new DhcpRequestPacket(TEST_TRANSACTION_ID,
|
||||
(short) 0 /* secs */, INADDR_ANY /* clientIp */, INADDR_ANY /* relayIp */,
|
||||
TEST_CLIENT_MAC_BYTES, false /* broadcast */);
|
||||
request.mServerIdentifier = TEST_SERVER_ADDR;
|
||||
request.mRequestedIp = TEST_CLIENT_ADDR;
|
||||
mServer.processPacket(request);
|
||||
|
||||
assertResponseSentTo(INADDR_BROADCAST);
|
||||
final DhcpNakPacket packet = assertNak(getPacket());
|
||||
assertMatchesClient(packet);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRelease() throws Exception {
|
||||
final DhcpReleasePacket release = new DhcpReleasePacket(TEST_TRANSACTION_ID,
|
||||
TEST_SERVER_ADDR, TEST_CLIENT_ADDR,
|
||||
INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES);
|
||||
mServer.processPacket(release);
|
||||
|
||||
verify(mRepository, times(1))
|
||||
.releaseLease(isNull(), eq(TEST_CLIENT_MAC), eq(TEST_CLIENT_ADDR));
|
||||
}
|
||||
|
||||
/* TODO: add more tests once packet construction is refactored, including:
|
||||
* - usage of giaddr
|
||||
* - usage of broadcast bit
|
||||
* - other request states (init-reboot/renewing/rebinding)
|
||||
*/
|
||||
|
||||
private void assertMatchesTestLease(@NonNull DhcpPacket packet) {
|
||||
assertMatchesClient(packet);
|
||||
assertFalse(packet.hasExplicitClientId());
|
||||
assertEquals(TEST_SERVER_ADDR, packet.mServerIdentifier);
|
||||
assertEquals(TEST_CLIENT_ADDR, packet.mYourIp);
|
||||
assertNotNull(packet.mLeaseTime);
|
||||
assertEquals(TEST_LEASE_EXPTIME_SECS, (int) packet.mLeaseTime);
|
||||
assertNull(packet.mHostName);
|
||||
}
|
||||
|
||||
private void assertMatchesClient(@NonNull DhcpPacket packet) {
|
||||
assertEquals(TEST_TRANSACTION_ID, packet.mTransId);
|
||||
assertEquals(TEST_CLIENT_MAC, MacAddress.fromBytes(packet.mClientMac));
|
||||
}
|
||||
|
||||
private void assertResponseSentTo(@NonNull Inet4Address addr) {
|
||||
assertEquals(addr, mResponseDstAddrCaptor.getValue());
|
||||
}
|
||||
|
||||
private static DhcpNakPacket assertNak(@Nullable DhcpPacket packet) {
|
||||
assertTrue(packet instanceof DhcpNakPacket);
|
||||
return (DhcpNakPacket) packet;
|
||||
}
|
||||
|
||||
private static DhcpAckPacket assertAck(@Nullable DhcpPacket packet) {
|
||||
assertTrue(packet instanceof DhcpAckPacket);
|
||||
return (DhcpAckPacket) packet;
|
||||
}
|
||||
|
||||
private static DhcpOfferPacket assertOffer(@Nullable DhcpPacket packet) {
|
||||
assertTrue(packet instanceof DhcpOfferPacket);
|
||||
return (DhcpOfferPacket) packet;
|
||||
}
|
||||
|
||||
private DhcpPacket getPacket() throws Exception {
|
||||
verify(mDeps, times(1)).sendPacket(any(), any(), any());
|
||||
return DhcpPacket.decodeFullPacket(mSentPacketCaptor.getValue(), ENCAP_BOOTP);
|
||||
}
|
||||
|
||||
private static Inet4Address parseAddr(@Nullable String inet4Addr) {
|
||||
return (Inet4Address) parseNumericAddress(inet4Addr);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user