Move DhcpServer to NetworkStack app
Test: atest FrameworksNetTests && atest NetworkStackTests
Bug: b/112869080
Change-Id: I96c40e63e9ceb37b67705bdd4d120307e114715b
diff --git a/packages/NetworkStack/Android.bp b/packages/NetworkStack/Android.bp
index 55bb517..4688848 100644
--- a/packages/NetworkStack/Android.bp
+++ b/packages/NetworkStack/Android.bp
@@ -22,6 +22,10 @@
srcs: [
"src/**/*.java",
],
+ static_libs: [
+ "dhcp-packet-lib",
+ "frameworks-net-shared-utils",
+ ]
}
// Updatable network stack packaged as an application
diff --git a/packages/NetworkStack/src/android/net/dhcp/DhcpLease.java b/packages/NetworkStack/src/android/net/dhcp/DhcpLease.java
new file mode 100644
index 0000000..6849cfa
--- /dev/null
+++ b/packages/NetworkStack/src/android/net/dhcp/DhcpLease.java
@@ -0,0 +1,153 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.MacAddress;
+import android.os.SystemClock;
+import android.text.TextUtils;
+
+import com.android.internal.util.HexDump;
+
+import java.net.Inet4Address;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * An IPv4 address assignment done through DHCPv4.
+ * @hide
+ */
+public class DhcpLease {
+ public static final long EXPIRATION_NEVER = Long.MAX_VALUE;
+ public static final String HOSTNAME_NONE = null;
+
+ @Nullable
+ private final byte[] mClientId;
+ @NonNull
+ private final MacAddress mHwAddr;
+ @NonNull
+ private final Inet4Address mNetAddr;
+ /**
+ * Expiration time for the lease, to compare with {@link SystemClock#elapsedRealtime()}.
+ */
+ private final long mExpTime;
+ @Nullable
+ private final String mHostname;
+
+ public DhcpLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr,
+ @NonNull Inet4Address netAddr, long expTime, @Nullable String hostname) {
+ mClientId = (clientId == null ? null : Arrays.copyOf(clientId, clientId.length));
+ mHwAddr = hwAddr;
+ mNetAddr = netAddr;
+ mExpTime = expTime;
+ mHostname = hostname;
+ }
+
+ /**
+ * Get the clientId associated with this lease, if any.
+ *
+ * <p>If the lease is not associated to a clientId, this returns null.
+ */
+ @Nullable
+ public byte[] getClientId() {
+ if (mClientId == null) {
+ return null;
+ }
+ return Arrays.copyOf(mClientId, mClientId.length);
+ }
+
+ @NonNull
+ public MacAddress getHwAddr() {
+ return mHwAddr;
+ }
+
+ @Nullable
+ public String getHostname() {
+ return mHostname;
+ }
+
+ @NonNull
+ public Inet4Address getNetAddr() {
+ return mNetAddr;
+ }
+
+ public long getExpTime() {
+ return mExpTime;
+ }
+
+ /**
+ * Push back the expiration time of this lease. If the provided time is sooner than the original
+ * expiration time, the lease time will not be updated.
+ *
+ * <p>The lease hostname is updated with the provided one if set.
+ * @return A {@link DhcpLease} with expiration time set to max(expTime, currentExpTime)
+ */
+ public DhcpLease renewedLease(long expTime, @Nullable String hostname) {
+ return new DhcpLease(mClientId, mHwAddr, mNetAddr, Math.max(expTime, mExpTime),
+ (hostname == null ? mHostname : hostname));
+ }
+
+ /**
+ * Determine whether this lease matches a client with the specified parameters.
+ * @param clientId clientId of the client if any, or null otherwise.
+ * @param hwAddr Hardware address of the client.
+ */
+ public boolean matchesClient(@Nullable byte[] clientId, @NonNull MacAddress hwAddr) {
+ if (mClientId != null) {
+ return Arrays.equals(mClientId, clientId);
+ } else {
+ return clientId == null && mHwAddr.equals(hwAddr);
+ }
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof DhcpLease)) {
+ return false;
+ }
+ final DhcpLease other = (DhcpLease) obj;
+ return Arrays.equals(mClientId, other.mClientId)
+ && mHwAddr.equals(other.mHwAddr)
+ && mNetAddr.equals(other.mNetAddr)
+ && mExpTime == other.mExpTime
+ && TextUtils.equals(mHostname, other.mHostname);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mClientId, mHwAddr, mNetAddr, mHostname, mExpTime);
+ }
+
+ static String clientIdToString(byte[] bytes) {
+ if (bytes == null) {
+ return "null";
+ }
+ return HexDump.toHexString(bytes);
+ }
+
+ static String inet4AddrToString(@Nullable Inet4Address addr) {
+ return (addr == null) ? "null" : addr.getHostAddress();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("clientId: %s, hwAddr: %s, netAddr: %s, expTime: %d, hostname: %s",
+ clientIdToString(mClientId), mHwAddr.toString(), inet4AddrToString(mNetAddr),
+ mExpTime, mHostname);
+ }
+}
diff --git a/packages/NetworkStack/src/android/net/dhcp/DhcpLeaseRepository.java b/packages/NetworkStack/src/android/net/dhcp/DhcpLeaseRepository.java
new file mode 100644
index 0000000..0d298de
--- /dev/null
+++ b/packages/NetworkStack/src/android/net/dhcp/DhcpLeaseRepository.java
@@ -0,0 +1,545 @@
+/*
+ * 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.inet4AddressToIntHTH;
+import static android.net.NetworkUtils.intToInet4AddressHTH;
+import static android.net.NetworkUtils.prefixLengthToV4NetmaskIntHTH;
+import static android.net.dhcp.DhcpLease.EXPIRATION_NEVER;
+import static android.net.dhcp.DhcpLease.inet4AddrToString;
+
+import static com.android.server.util.NetworkStackConstants.IPV4_ADDR_BITS;
+
+import static java.lang.Math.min;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.IpPrefix;
+import android.net.MacAddress;
+import android.net.dhcp.DhcpServer.Clock;
+import android.net.util.SharedLog;
+import android.util.ArrayMap;
+
+import java.net.Inet4Address;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.function.Function;
+
+/**
+ * A repository managing IPv4 address assignments through DHCPv4.
+ *
+ * <p>This class is not thread-safe. All public methods should be called on a common thread or
+ * use some synchronization mechanism.
+ *
+ * <p>Methods are optimized for a small number of allocated leases, assuming that most of the time
+ * only 2~10 addresses will be allocated, which is the common case. Managing a large number of
+ * addresses is supported but will be slower: some operations have complexity in O(num_leases).
+ * @hide
+ */
+class DhcpLeaseRepository {
+ public static final byte[] CLIENTID_UNSPEC = null;
+ public static final Inet4Address INETADDR_UNSPEC = null;
+
+ @NonNull
+ private final SharedLog mLog;
+ @NonNull
+ private final Clock mClock;
+
+ @NonNull
+ private IpPrefix mPrefix;
+ @NonNull
+ private Set<Inet4Address> mReservedAddrs;
+ private int mSubnetAddr;
+ private int mSubnetMask;
+ private int mNumAddresses;
+ private long mLeaseTimeMs;
+
+ /**
+ * 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
+ * update this when removing entries, but it must always be updated when adding/updating.
+ */
+ private long mNextExpirationCheck = EXPIRATION_NEVER;
+
+ static class DhcpLeaseException extends Exception {
+ DhcpLeaseException(String message) {
+ super(message);
+ }
+ }
+
+ static class OutOfAddressesException extends DhcpLeaseException {
+ OutOfAddressesException(String message) {
+ super(message);
+ }
+ }
+
+ static class InvalidAddressException extends DhcpLeaseException {
+ InvalidAddressException(String message) {
+ super(message);
+ }
+ }
+
+ static class InvalidSubnetException extends DhcpLeaseException {
+ InvalidSubnetException(String message) {
+ super(message);
+ }
+ }
+
+ /**
+ * Leases by IP address
+ */
+ private final ArrayMap<Inet4Address, DhcpLease> mCommittedLeases = new ArrayMap<>();
+
+ /**
+ * Map address -> expiration timestamp in ms. Addresses are guaranteed to be valid as defined
+ * by {@link #isValidAddress(Inet4Address)}, but are not necessarily otherwise available for
+ * assignment.
+ */
+ private final LinkedHashMap<Inet4Address, Long> mDeclinedAddrs = new LinkedHashMap<>();
+
+ DhcpLeaseRepository(@NonNull IpPrefix prefix, @NonNull Set<Inet4Address> reservedAddrs,
+ long leaseTimeMs, @NonNull SharedLog log, @NonNull Clock clock) {
+ updateParams(prefix, reservedAddrs, leaseTimeMs);
+ mLog = log;
+ mClock = clock;
+ }
+
+ public void updateParams(@NonNull IpPrefix prefix, @NonNull Set<Inet4Address> reservedAddrs,
+ long leaseTimeMs) {
+ mPrefix = prefix;
+ mReservedAddrs = Collections.unmodifiableSet(new HashSet<>(reservedAddrs));
+ mSubnetMask = prefixLengthToV4NetmaskIntHTH(prefix.getPrefixLength());
+ mSubnetAddr = inet4AddressToIntHTH((Inet4Address) prefix.getAddress()) & mSubnetMask;
+ mNumAddresses = 1 << (IPV4_ADDR_BITS - prefix.getPrefixLength());
+ mLeaseTimeMs = leaseTimeMs;
+
+ cleanMap(mCommittedLeases);
+ cleanMap(mDeclinedAddrs);
+ }
+
+ /**
+ * From a map keyed by {@link Inet4Address}, remove entries where the key is invalid (as
+ * specified by {@link #isValidAddress(Inet4Address)}), or is a reserved address.
+ */
+ private <T> void cleanMap(Map<Inet4Address, T> map) {
+ final Iterator<Entry<Inet4Address, T>> it = map.entrySet().iterator();
+ while (it.hasNext()) {
+ final Inet4Address addr = it.next().getKey();
+ if (!isValidAddress(addr) || mReservedAddrs.contains(addr)) {
+ it.remove();
+ }
+ }
+ }
+
+ /**
+ * Get a DHCP offer, to reply to a DHCPDISCOVER. Follows RFC2131 #4.3.1.
+ *
+ * @param clientId Client identifier option if specified, or {@link #CLIENTID_UNSPEC}
+ * @param relayAddr Internet address of the relay (giaddr), can be {@link Inet4Address#ANY}
+ * @param reqAddr Requested address by the client (option 50), or {@link #INETADDR_UNSPEC}
+ * @param hostname Client-provided hostname, or {@link DhcpLease#HOSTNAME_NONE}
+ * @throws OutOfAddressesException The server does not have any available address
+ * @throws InvalidSubnetException The lease was requested from an unsupported subnet
+ */
+ @NonNull
+ public DhcpLease getOffer(@Nullable byte[] clientId, @NonNull MacAddress hwAddr,
+ @NonNull Inet4Address relayAddr, @Nullable Inet4Address reqAddr,
+ @Nullable String hostname) throws OutOfAddressesException, InvalidSubnetException {
+ final long currentTime = mClock.elapsedRealtime();
+ final long expTime = currentTime + mLeaseTimeMs;
+
+ removeExpiredLeases(currentTime);
+ checkValidRelayAddr(relayAddr);
+
+ final DhcpLease currentLease = findByClient(clientId, hwAddr);
+ final DhcpLease newLease;
+ if (currentLease != null) {
+ newLease = currentLease.renewedLease(expTime, hostname);
+ mLog.log("Offering extended lease " + newLease);
+ // Do not update lease time in the map: the offer is not committed yet.
+ } else if (reqAddr != null && isValidAddress(reqAddr) && isAvailable(reqAddr)) {
+ newLease = new DhcpLease(clientId, hwAddr, reqAddr, expTime, hostname);
+ mLog.log("Offering requested lease " + newLease);
+ } else {
+ newLease = makeNewOffer(clientId, hwAddr, expTime, hostname);
+ mLog.log("Offering new generated lease " + newLease);
+ }
+ return newLease;
+ }
+
+ private void checkValidRelayAddr(@Nullable Inet4Address relayAddr)
+ throws InvalidSubnetException {
+ // As per #4.3.1, addresses are assigned based on the relay address if present. This
+ // implementation only assigns addresses if the relayAddr is inside our configured subnet.
+ // This also applies when the client requested a specific address for consistency between
+ // requests, and with older behavior.
+ if (isIpAddrOutsidePrefix(mPrefix, relayAddr)) {
+ throw new InvalidSubnetException("Lease requested by relay from outside of subnet");
+ }
+ }
+
+ private static boolean isIpAddrOutsidePrefix(@NonNull IpPrefix prefix,
+ @Nullable Inet4Address addr) {
+ return addr != null && !addr.equals(Inet4Address.ANY) && !prefix.contains(addr);
+ }
+
+ @Nullable
+ private DhcpLease findByClient(@Nullable byte[] clientId, @NonNull MacAddress hwAddr) {
+ for (DhcpLease lease : mCommittedLeases.values()) {
+ if (lease.matchesClient(clientId, hwAddr)) {
+ return lease;
+ }
+ }
+
+ // Note this differs from dnsmasq behavior, which would match by hwAddr if clientId was
+ // given but no lease keyed on clientId matched. This would prevent one interface from
+ // obtaining multiple leases with different clientId.
+ return null;
+ }
+
+ /**
+ * Make a lease conformant to a client DHCPREQUEST or renew the client's existing lease,
+ * commit it to the repository and return it.
+ *
+ * <p>This method always succeeds and commits the lease if it does not throw, and has no side
+ * effects if it throws.
+ *
+ * @param clientId Client identifier option if specified, or {@link #CLIENTID_UNSPEC}
+ * @param reqAddr Requested address by the client (option 50), or {@link #INETADDR_UNSPEC}
+ * @param sidSet Whether the server identifier was set in the request
+ * @return The newly created or renewed lease
+ * @throws InvalidAddressException The client provided an address that conflicts with its
+ * current configuration, or other committed/reserved leases.
+ */
+ @NonNull
+ public DhcpLease requestLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr,
+ @NonNull Inet4Address clientAddr, @NonNull Inet4Address relayAddr,
+ @Nullable Inet4Address reqAddr, boolean sidSet, @Nullable String hostname)
+ throws InvalidAddressException, InvalidSubnetException {
+ final long currentTime = mClock.elapsedRealtime();
+ removeExpiredLeases(currentTime);
+ checkValidRelayAddr(relayAddr);
+ final DhcpLease assignedLease = findByClient(clientId, hwAddr);
+
+ final Inet4Address leaseAddr = reqAddr != null ? reqAddr : clientAddr;
+ if (assignedLease != null) {
+ if (sidSet && reqAddr != null) {
+ // Client in SELECTING state; remove any current lease before creating a new one.
+ mCommittedLeases.remove(assignedLease.getNetAddr());
+ } else if (!assignedLease.getNetAddr().equals(leaseAddr)) {
+ // reqAddr null (RENEWING/REBINDING): client renewing its own lease for clientAddr.
+ // reqAddr set with sid not set (INIT-REBOOT): client verifying configuration.
+ // In both cases, throw if clientAddr or reqAddr does not match the known lease.
+ throw new InvalidAddressException("Incorrect address for client in "
+ + (reqAddr != null ? "INIT-REBOOT" : "RENEWING/REBINDING"));
+ }
+ }
+
+ // In the init-reboot case, RFC2131 #4.3.2 says that the server must not reply if
+ // assignedLease == null, but dnsmasq will let the client use the requested address if
+ // available, when configured with --dhcp-authoritative. This is preferable to avoid issues
+ // if the server lost the lease DB: the client would not get a reply because the server
+ // does not know their lease.
+ // Similarly in RENEWING/REBINDING state, create a lease when possible if the
+ // client-provided lease is unknown.
+ final DhcpLease lease =
+ checkClientAndMakeLease(clientId, hwAddr, leaseAddr, hostname, currentTime);
+ mLog.logf("DHCPREQUEST assignedLease %s, reqAddr=%s, sidSet=%s: created/renewed lease %s",
+ assignedLease, inet4AddrToString(reqAddr), sidSet, lease);
+ return lease;
+ }
+
+ /**
+ * Check that the client can request the specified address, make or renew the lease if yes, and
+ * commit it.
+ *
+ * <p>This method always succeeds and returns the lease if it does not throw, and has no
+ * side-effect if it throws.
+ *
+ * @return The newly created or renewed, committed lease
+ * @throws InvalidAddressException The client provided an address that conflicts with its
+ * current configuration, or other committed/reserved leases.
+ */
+ private DhcpLease checkClientAndMakeLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr,
+ @NonNull Inet4Address addr, @Nullable String hostname, long currentTime)
+ throws InvalidAddressException {
+ final long expTime = currentTime + mLeaseTimeMs;
+ final DhcpLease currentLease = mCommittedLeases.getOrDefault(addr, null);
+ if (currentLease != null && !currentLease.matchesClient(clientId, hwAddr)) {
+ throw new InvalidAddressException("Address in use");
+ }
+
+ final DhcpLease lease;
+ if (currentLease == null) {
+ if (isValidAddress(addr) && !mReservedAddrs.contains(addr)) {
+ lease = new DhcpLease(clientId, hwAddr, addr, expTime, hostname);
+ } else {
+ throw new InvalidAddressException("Lease not found and address unavailable");
+ }
+ } else {
+ lease = currentLease.renewedLease(expTime, hostname);
+ }
+ commitLease(lease);
+ return lease;
+ }
+
+ private void commitLease(@NonNull DhcpLease lease) {
+ mCommittedLeases.put(lease.getNetAddr(), lease);
+ maybeUpdateEarliestExpiration(lease.getExpTime());
+ }
+
+ /**
+ * Delete a committed lease from the repository.
+ *
+ * @return true if a lease matching parameters was found.
+ */
+ public boolean releaseLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr,
+ @NonNull Inet4Address addr) {
+ final DhcpLease currentLease = mCommittedLeases.getOrDefault(addr, null);
+ if (currentLease == null) {
+ mLog.w("Could not release unknown lease for " + inet4AddrToString(addr));
+ return false;
+ }
+ if (currentLease.matchesClient(clientId, hwAddr)) {
+ mCommittedLeases.remove(addr);
+ mLog.log("Released lease " + currentLease);
+ return true;
+ }
+ mLog.w(String.format("Not releasing lease %s: does not match client (cid %s, hwAddr %s)",
+ currentLease, DhcpLease.clientIdToString(clientId), hwAddr));
+ return false;
+ }
+
+ public void markLeaseDeclined(@NonNull Inet4Address addr) {
+ if (mDeclinedAddrs.containsKey(addr) || !isValidAddress(addr)) {
+ mLog.logf("Not marking %s as declined: already declined or not assignable",
+ inet4AddrToString(addr));
+ return;
+ }
+ final long expTime = mClock.elapsedRealtime() + mLeaseTimeMs;
+ mDeclinedAddrs.put(addr, expTime);
+ mLog.logf("Marked %s as declined expiring %d", inet4AddrToString(addr), expTime);
+ maybeUpdateEarliestExpiration(expTime);
+ }
+
+ /**
+ * Get the list of currently valid committed leases in the repository.
+ */
+ @NonNull
+ public List<DhcpLease> getCommittedLeases() {
+ removeExpiredLeases(mClock.elapsedRealtime());
+ return new ArrayList<>(mCommittedLeases.values());
+ }
+
+ /**
+ * Get the set of addresses that have been marked as declined in the repository.
+ */
+ @NonNull
+ public Set<Inet4Address> getDeclinedAddresses() {
+ removeExpiredLeases(mClock.elapsedRealtime());
+ return new HashSet<>(mDeclinedAddrs.keySet());
+ }
+
+ /**
+ * Given the expiration time of a new committed lease or declined address, update
+ * {@link #mNextExpirationCheck} so it stays lower than or equal to the time for the first lease
+ * to expire.
+ */
+ private void maybeUpdateEarliestExpiration(long expTime) {
+ if (expTime < mNextExpirationCheck) {
+ mNextExpirationCheck = expTime;
+ }
+ }
+
+ /**
+ * Remove expired entries from a map keyed by {@link Inet4Address}.
+ *
+ * @param tag Type of lease in the map, for logging
+ * @param getExpTime Functor returning the expiration time for an object in the map.
+ * Must not return null.
+ * @return The lowest expiration time among entries remaining in the map
+ */
+ private <T> long removeExpired(long currentTime, @NonNull Map<Inet4Address, T> map,
+ @NonNull String tag, @NonNull Function<T, Long> getExpTime) {
+ final Iterator<Entry<Inet4Address, T>> it = map.entrySet().iterator();
+ long firstExpiration = EXPIRATION_NEVER;
+ while (it.hasNext()) {
+ final Entry<Inet4Address, T> lease = it.next();
+ final long expTime = getExpTime.apply(lease.getValue());
+ if (expTime <= currentTime) {
+ mLog.logf("Removing expired %s lease for %s (expTime=%s, currentTime=%s)",
+ tag, lease.getKey(), expTime, currentTime);
+ it.remove();
+ } else {
+ firstExpiration = min(firstExpiration, expTime);
+ }
+ }
+ return firstExpiration;
+ }
+
+ /**
+ * Go through committed and declined leases and remove the expired ones.
+ */
+ private void removeExpiredLeases(long currentTime) {
+ if (currentTime < mNextExpirationCheck) {
+ return;
+ }
+
+ final long commExp = removeExpired(
+ currentTime, mCommittedLeases, "committed", DhcpLease::getExpTime);
+ final long declExp = removeExpired(
+ currentTime, mDeclinedAddrs, "declined", Function.identity());
+
+ mNextExpirationCheck = min(commExp, declExp);
+ }
+
+ private boolean isAvailable(@NonNull Inet4Address addr) {
+ return !mReservedAddrs.contains(addr) && !mCommittedLeases.containsKey(addr);
+ }
+
+ /**
+ * Get the 0-based index of an address in the subnet.
+ *
+ * <p>Given ordering of addresses 5.6.7.8 < 5.6.7.9 < 5.6.8.0, the index on a subnet is defined
+ * so that the first address is 0, the second 1, etc. For example on a /16, 192.168.0.0 -> 0,
+ * 192.168.0.1 -> 1, 192.168.1.0 -> 256
+ *
+ */
+ private int getAddrIndex(int addr) {
+ return addr & ~mSubnetMask;
+ }
+
+ private int getAddrByIndex(int index) {
+ return mSubnetAddr | index;
+ }
+
+ /**
+ * Get a valid address starting from the supplied one.
+ *
+ * <p>This only checks that the address is numerically valid for assignment, not whether it is
+ * already in use. The return value is always inside the configured prefix, even if the supplied
+ * address is not.
+ *
+ * <p>If the provided address is valid, it is returned as-is. Otherwise, the next valid
+ * address (with the ordering in {@link #getAddrIndex(int)}) is returned.
+ */
+ private int getValidAddress(int addr) {
+ final int lastByteMask = 0xff;
+ int addrIndex = getAddrIndex(addr); // 0-based index of the address in the subnet
+
+ // Some OSes do not handle addresses in .255 or .0 correctly: avoid those.
+ final int lastByte = getAddrByIndex(addrIndex) & lastByteMask;
+ if (lastByte == lastByteMask) {
+ // Avoid .255 address, and .0 address that follows
+ addrIndex = (addrIndex + 2) % mNumAddresses;
+ } else if (lastByte == 0) {
+ // Avoid .0 address
+ addrIndex = (addrIndex + 1) % mNumAddresses;
+ }
+
+ // Do not use first or last address of range
+ if (addrIndex == 0 || addrIndex == mNumAddresses - 1) {
+ // Always valid and not end of range since prefixLength is at most 30 in serving params
+ addrIndex = 1;
+ }
+ return getAddrByIndex(addrIndex);
+ }
+
+ /**
+ * Returns whether the address is in the configured subnet and part of the assignable range.
+ */
+ private boolean isValidAddress(Inet4Address addr) {
+ final int intAddr = inet4AddressToIntHTH(addr);
+ return getValidAddress(intAddr) == intAddr;
+ }
+
+ private int getNextAddress(int addr) {
+ final int addrIndex = getAddrIndex(addr);
+ final int nextAddress = getAddrByIndex((addrIndex + 1) % mNumAddresses);
+ return getValidAddress(nextAddress);
+ }
+
+ /**
+ * Calculate a first candidate address for a client by hashing the hardware address.
+ *
+ * <p>This will be a valid address as checked by {@link #getValidAddress(int)}, but may be
+ * in use.
+ *
+ * @return An IPv4 address encoded as 32-bit int
+ */
+ private int getFirstClientAddress(MacAddress hwAddr) {
+ // This follows dnsmasq behavior. Advantages are: clients will often get the same
+ // offers for different DISCOVER even if the lease was not yet accepted or has expired,
+ // and address generation will generally not need to loop through many allocated addresses
+ // until it finds a free one.
+ int hash = 0;
+ for (byte b : hwAddr.toByteArray()) {
+ hash += b + (b << 8) + (b << 16);
+ }
+ // This implementation will not always result in the same IPs as dnsmasq would give out in
+ // Android <= P, because it includes invalid and reserved addresses in mNumAddresses while
+ // the configured ranges for dnsmasq did not.
+ final int addrIndex = hash % mNumAddresses;
+ return getValidAddress(getAddrByIndex(addrIndex));
+ }
+
+ /**
+ * Create a lease that can be offered to respond to a client DISCOVER.
+ *
+ * <p>This method always succeeds and returns the lease if it does not throw. If no non-declined
+ * address is available, it will try to offer the oldest declined address if valid.
+ *
+ * @throws OutOfAddressesException The server has no address left to offer
+ */
+ private DhcpLease makeNewOffer(@Nullable byte[] clientId, @NonNull MacAddress hwAddr,
+ long expTime, @Nullable String hostname) throws OutOfAddressesException {
+ int intAddr = getFirstClientAddress(hwAddr);
+ // Loop until a free address is found, or there are no more addresses.
+ // There is slightly less than this many usable addresses, but some extra looping is OK
+ for (int i = 0; i < mNumAddresses; i++) {
+ final Inet4Address addr = intToInet4AddressHTH(intAddr);
+ if (isAvailable(addr) && !mDeclinedAddrs.containsKey(addr)) {
+ return new DhcpLease(clientId, hwAddr, addr, expTime, hostname);
+ }
+ intAddr = getNextAddress(intAddr);
+ }
+
+ // Try freeing DECLINEd addresses if out of addresses.
+ final Iterator<Inet4Address> it = mDeclinedAddrs.keySet().iterator();
+ while (it.hasNext()) {
+ final Inet4Address addr = it.next();
+ it.remove();
+ mLog.logf("Out of addresses in address pool: dropped declined addr %s",
+ inet4AddrToString(addr));
+ // isValidAddress() is always verified for entries in mDeclinedAddrs.
+ // However declined addresses may have been requested (typically by the machine that was
+ // already using the address) after being declined.
+ if (isAvailable(addr)) {
+ return new DhcpLease(clientId, hwAddr, addr, expTime, hostname);
+ }
+ }
+
+ throw new OutOfAddressesException("No address available for offer");
+ }
+}
diff --git a/packages/NetworkStack/src/android/net/dhcp/DhcpPacketListener.java b/packages/NetworkStack/src/android/net/dhcp/DhcpPacketListener.java
new file mode 100644
index 0000000..dce8b61
--- /dev/null
+++ b/packages/NetworkStack/src/android/net/dhcp/DhcpPacketListener.java
@@ -0,0 +1,88 @@
+/*
+ * 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 android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.util.FdEventsReader;
+import android.os.Handler;
+import android.system.Os;
+
+import java.io.FileDescriptor;
+import java.net.Inet4Address;
+import java.net.InetSocketAddress;
+
+/**
+ * A {@link FdEventsReader} to receive and parse {@link DhcpPacket}.
+ * @hide
+ */
+abstract class DhcpPacketListener extends FdEventsReader<DhcpPacketListener.Payload> {
+ static final class Payload {
+ protected final byte[] mBytes = new byte[DhcpPacket.MAX_LENGTH];
+ protected Inet4Address mSrcAddr;
+ protected int mSrcPort;
+ }
+
+ DhcpPacketListener(@NonNull Handler handler) {
+ super(handler, new Payload());
+ }
+
+ @Override
+ protected int recvBufSize(@NonNull Payload buffer) {
+ return buffer.mBytes.length;
+ }
+
+ @Override
+ protected final void handlePacket(@NonNull Payload recvbuf, int length) {
+ if (recvbuf.mSrcAddr == null) {
+ return;
+ }
+
+ try {
+ final DhcpPacket packet = DhcpPacket.decodeFullPacket(recvbuf.mBytes, length,
+ DhcpPacket.ENCAP_BOOTP);
+ onReceive(packet, recvbuf.mSrcAddr, recvbuf.mSrcPort);
+ } catch (DhcpPacket.ParseException e) {
+ logParseError(recvbuf.mBytes, length, e);
+ }
+ }
+
+ @Override
+ protected int readPacket(@NonNull FileDescriptor fd, @NonNull Payload packetBuffer)
+ throws Exception {
+ final InetSocketAddress addr = new InetSocketAddress();
+ final int read = Os.recvfrom(
+ fd, packetBuffer.mBytes, 0, packetBuffer.mBytes.length, 0 /* flags */, addr);
+
+ // Buffers with null srcAddr will be dropped in handlePacket()
+ packetBuffer.mSrcAddr = inet4AddrOrNull(addr);
+ packetBuffer.mSrcPort = addr.getPort();
+ return read;
+ }
+
+ @Nullable
+ private static Inet4Address inet4AddrOrNull(@NonNull InetSocketAddress addr) {
+ return addr.getAddress() instanceof Inet4Address
+ ? (Inet4Address) addr.getAddress()
+ : null;
+ }
+
+ protected abstract void onReceive(@NonNull DhcpPacket packet, @NonNull Inet4Address srcAddr,
+ int srcPort);
+ protected abstract void logParseError(@NonNull byte[] packet, int length,
+ @NonNull DhcpPacket.ParseException e);
+}
diff --git a/packages/NetworkStack/src/android/net/dhcp/DhcpServer.java b/packages/NetworkStack/src/android/net/dhcp/DhcpServer.java
new file mode 100644
index 0000000..14e2936
--- /dev/null
+++ b/packages/NetworkStack/src/android/net/dhcp/DhcpServer.java
@@ -0,0 +1,651 @@
+/*
+ * 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_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.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 com.android.server.util.NetworkStackConstants.INFINITE_LEASE;
+import static com.android.server.util.PermissionUtil.checkNetworkStackCallingPermission;
+
+import static java.lang.Integer.toUnsignedLong;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.INetworkStackStatusCallback;
+import android.net.MacAddress;
+import android.net.NetworkUtils;
+import android.net.TrafficStats;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+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 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 extends IDhcpServer.Stub {
+ 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 HandlerThread mHandlerThread;
+ @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;
+
+ @Nullable
+ private volatile ServerHandler mHandler;
+
+ // Accessed only on the handler thread
+ @Nullable
+ private DhcpPacketListener mPacketListener;
+ @Nullable
+ private FileDescriptor mSocket;
+ @NonNull
+ private DhcpServingParams mServingParams;
+
+ /**
+ * 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();
+
+ /**
+ * 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;
+
+ /**
+ * Verify that the caller is allowed to call public methods on DhcpServer.
+ * @throws SecurityException The caller is not allowed to call public methods on DhcpServer.
+ */
+ void checkCaller() throws SecurityException;
+ }
+
+ 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);
+ }
+
+ @Override
+ public void checkCaller() {
+ checkNetworkStackCallingPermission();
+ }
+ }
+
+ private static class MalformedPacketException extends Exception {
+ MalformedPacketException(String message, Throwable t) {
+ super(message, t);
+ }
+ }
+
+ public DhcpServer(@NonNull String ifName,
+ @NonNull DhcpServingParams params, @NonNull SharedLog log) {
+ this(new HandlerThread(DhcpServer.class.getSimpleName() + "." + ifName),
+ ifName, params, log, null);
+ }
+
+ @VisibleForTesting
+ DhcpServer(@NonNull HandlerThread handlerThread, @NonNull String ifName,
+ @NonNull DhcpServingParams params, @NonNull SharedLog log,
+ @Nullable Dependencies deps) {
+ if (deps == null) {
+ deps = new DependenciesImpl();
+ }
+ mHandlerThread = handlerThread;
+ mIfName = ifName;
+ mServingParams = params;
+ mLog = log;
+ mDeps = deps;
+ mClock = deps.makeClock();
+ mLeaseRepo = deps.makeLeaseRepository(mServingParams, mLog, mClock);
+ }
+
+ /**
+ * 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.
+ */
+ @Override
+ public void start(@Nullable INetworkStackStatusCallback cb) {
+ mDeps.checkCaller();
+ mHandlerThread.start();
+ mHandler = new ServerHandler(mHandlerThread.getLooper());
+ sendMessage(CMD_START_DHCP_SERVER, cb);
+ }
+
+ /**
+ * 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.
+ */
+ @Override
+ public void updateParams(@Nullable DhcpServingParamsParcel params,
+ @Nullable INetworkStackStatusCallback cb) throws RemoteException {
+ mDeps.checkCaller();
+ 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);
+ if (cb != null) {
+ cb.onStatusAvailable(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.
+ */
+ @Override
+ public void stop(@Nullable INetworkStackStatusCallback cb) {
+ mDeps.checkCaller();
+ sendMessage(CMD_STOP_DHCP_SERVER, cb);
+ }
+
+ private void sendMessage(int what, @Nullable Object obj) {
+ if (mHandler == null) {
+ mLog.e("Attempting to send a command to stopped DhcpServer: " + what);
+ return;
+ }
+ mHandler.sendMessage(mHandler.obtainMessage(what, obj));
+ }
+
+ private class ServerHandler extends Handler {
+ ServerHandler(@NonNull Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(@NonNull Message msg) {
+ final INetworkStackStatusCallback cb;
+ switch (msg.what) {
+ case CMD_UPDATE_PARAMS:
+ final Pair<DhcpServingParams, INetworkStackStatusCallback> pair =
+ (Pair<DhcpServingParams, INetworkStackStatusCallback>) msg.obj;
+ final DhcpServingParams params = pair.first;
+ mServingParams = params;
+ mLeaseRepo.updateParams(
+ DhcpServingParams.makeIpPrefix(mServingParams.serverAddr),
+ params.excludedAddrs,
+ params.dhcpLeaseTimeSecs);
+
+ cb = pair.second;
+ break;
+ case CMD_START_DHCP_SERVER:
+ mPacketListener = mDeps.makePacketListener();
+ mPacketListener.start();
+ cb = (INetworkStackStatusCallback) msg.obj;
+ break;
+ case CMD_STOP_DHCP_SERVER:
+ if (mPacketListener != null) {
+ mPacketListener.stop();
+ mPacketListener = null;
+ }
+ mHandlerThread.quitSafely();
+ cb = (INetworkStackStatusCallback) msg.obj;
+ break;
+ default:
+ return;
+ }
+ if (cb != null) {
+ try {
+ cb.onStatusAvailable(STATUS_SUCCESS);
+ } catch (RemoteException e) {
+ mLog.e("Could not send status back to caller", e);
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ void processPacket(@NonNull DhcpPacket packet, int srcPort) {
+ final String packetType = packet.getClass().getSimpleName();
+ if (srcPort != DHCP_CLIENT) {
+ mLog.logf("Ignored packet of type %s sent from client port %d", packetType, srcPort);
+ return;
+ }
+
+ mLog.log("Received packet of type " + packetType);
+ 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 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 {
+ 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.InvalidSubnetException e) {
+ logIgnoredPacketInvalidSubnet(e);
+ 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.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 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()));
+ }
+
+ /**
+ * 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);
+
+ 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 String hostname = getHostnameIfRequested(request, lease);
+ final ByteBuffer ackPacket = DhcpPacket.buildAckPacket(ENCAP_BOOTP, request.mTransId,
+ broadcastFlag, mServingParams.getServerInet4Addr(), request.mRelayIp,
+ lease.getNetAddr(), request.mClientIp, request.mClientMac, timeout,
+ mServingParams.getPrefixMaskAsAddress(), mServingParams.getBroadcastAddress(),
+ new ArrayList<>(mServingParams.defaultRouters),
+ new ArrayList<>(mServingParams.dnsServers),
+ mServingParams.getServerInet4Addr(), null /* domainName */, hostname,
+ mServingParams.metered, (short) mServingParams.linkMtu);
+
+ 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.mRelayIp, 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, 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 || Inet4Address.ANY.equals(address);
+ }
+
+ private class PacketListener extends DhcpPacketListener {
+ PacketListener() {
+ super(mHandler);
+ }
+
+ @Override
+ protected void onReceive(@NonNull DhcpPacket packet, @NonNull Inet4Address srcAddr,
+ int srcPort) {
+ processPacket(packet, srcPort);
+ }
+
+ @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, 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, mIfName);
+ 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(null);
+ return null;
+ } finally {
+ TrafficStats.setThreadStatsTag(oldTag);
+ }
+ }
+ }
+}
diff --git a/packages/NetworkStack/src/android/net/dhcp/DhcpServingParams.java b/packages/NetworkStack/src/android/net/dhcp/DhcpServingParams.java
new file mode 100644
index 0000000..f38888a
--- /dev/null
+++ b/packages/NetworkStack/src/android/net/dhcp/DhcpServingParams.java
@@ -0,0 +1,370 @@
+/*
+ * 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.getPrefixMaskAsInet4Address;
+import static android.net.NetworkUtils.intToInet4AddressHTH;
+
+import static com.android.server.util.NetworkStackConstants.INFINITE_LEASE;
+import static com.android.server.util.NetworkStackConstants.IPV4_MAX_MTU;
+import static com.android.server.util.NetworkStackConstants.IPV4_MIN_MTU;
+
+import static java.lang.Integer.toUnsignedLong;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.NetworkUtils;
+
+import com.google.android.collect.Sets;
+
+import java.net.Inet4Address;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Parameters used by the DhcpServer to serve requests.
+ *
+ * <p>Instances are immutable. Use {@link DhcpServingParams.Builder} to instantiate.
+ * @hide
+ */
+public class DhcpServingParams {
+ public static final int MTU_UNSET = 0;
+ public static final int MIN_PREFIX_LENGTH = 16;
+ public static final int MAX_PREFIX_LENGTH = 30;
+
+ /** Server inet address and prefix to serve */
+ @NonNull
+ public final LinkAddress serverAddr;
+
+ /**
+ * Default routers to be advertised to DHCP clients. May be empty.
+ * This set is provided by {@link DhcpServingParams.Builder} and is immutable.
+ */
+ @NonNull
+ public final Set<Inet4Address> defaultRouters;
+
+ /**
+ * DNS servers to be advertised to DHCP clients. May be empty.
+ * This set is provided by {@link DhcpServingParams.Builder} and is immutable.
+ */
+ @NonNull
+ public final Set<Inet4Address> dnsServers;
+
+ /**
+ * Excluded addresses that the DHCP server is not allowed to assign to clients.
+ * This set is provided by {@link DhcpServingParams.Builder} and is immutable.
+ */
+ @NonNull
+ public final Set<Inet4Address> excludedAddrs;
+
+ // DHCP uses uint32. Use long for clearer code, and check range when building.
+ public final long dhcpLeaseTimeSecs;
+ public final int linkMtu;
+
+ /**
+ * Indicates whether the DHCP server should send the ANDROID_METERED vendor-specific option.
+ */
+ public final boolean metered;
+
+ /**
+ * Checked exception thrown when some parameters used to build {@link DhcpServingParams} are
+ * missing or invalid.
+ */
+ public static class InvalidParameterException extends Exception {
+ public InvalidParameterException(String message) {
+ super(message);
+ }
+ }
+
+ private DhcpServingParams(@NonNull LinkAddress serverAddr,
+ @NonNull Set<Inet4Address> defaultRouters,
+ @NonNull Set<Inet4Address> dnsServers, @NonNull Set<Inet4Address> excludedAddrs,
+ long dhcpLeaseTimeSecs, int linkMtu, boolean metered) {
+ this.serverAddr = serverAddr;
+ this.defaultRouters = defaultRouters;
+ this.dnsServers = dnsServers;
+ this.excludedAddrs = excludedAddrs;
+ this.dhcpLeaseTimeSecs = dhcpLeaseTimeSecs;
+ this.linkMtu = linkMtu;
+ this.metered = metered;
+ }
+
+ /**
+ * Create parameters from a stable AIDL-compatible parcel.
+ * @throws InvalidParameterException The parameters parcelable is null or invalid.
+ */
+ public static DhcpServingParams fromParcelableObject(@Nullable DhcpServingParamsParcel parcel)
+ throws InvalidParameterException {
+ if (parcel == null) {
+ throw new InvalidParameterException("Null serving parameters");
+ }
+ final LinkAddress serverAddr = new LinkAddress(
+ intToInet4AddressHTH(parcel.serverAddr),
+ parcel.serverAddrPrefixLength);
+ return new Builder()
+ .setServerAddr(serverAddr)
+ .setDefaultRouters(toInet4AddressSet(parcel.defaultRouters))
+ .setDnsServers(toInet4AddressSet(parcel.dnsServers))
+ .setExcludedAddrs(toInet4AddressSet(parcel.excludedAddrs))
+ .setDhcpLeaseTimeSecs(parcel.dhcpLeaseTimeSecs)
+ .setLinkMtu(parcel.linkMtu)
+ .setMetered(parcel.metered)
+ .build();
+ }
+
+ private static Set<Inet4Address> toInet4AddressSet(@Nullable int[] addrs) {
+ if (addrs == null) {
+ return new HashSet<>(0);
+ }
+
+ final HashSet<Inet4Address> res = new HashSet<>();
+ for (int addr : addrs) {
+ res.add(intToInet4AddressHTH(addr));
+ }
+ return res;
+ }
+
+ @NonNull
+ public Inet4Address getServerInet4Addr() {
+ return (Inet4Address) serverAddr.getAddress();
+ }
+
+ /**
+ * Get the served prefix mask as an IPv4 address.
+ *
+ * <p>For example, if the served prefix is 192.168.42.0/24, this will return 255.255.255.0.
+ */
+ @NonNull
+ public Inet4Address getPrefixMaskAsAddress() {
+ return getPrefixMaskAsInet4Address(serverAddr.getPrefixLength());
+ }
+
+ /**
+ * Get the server broadcast address.
+ *
+ * <p>For example, if the server {@link LinkAddress} is 192.168.42.1/24, this will return
+ * 192.168.42.255.
+ */
+ @NonNull
+ public Inet4Address getBroadcastAddress() {
+ return NetworkUtils.getBroadcastAddress(getServerInet4Addr(), serverAddr.getPrefixLength());
+ }
+
+ /**
+ * Utility class to create new instances of {@link DhcpServingParams} while checking validity
+ * of the parameters.
+ */
+ public static class Builder {
+ private LinkAddress mServerAddr;
+ private Set<Inet4Address> mDefaultRouters;
+ private Set<Inet4Address> mDnsServers;
+ private Set<Inet4Address> mExcludedAddrs;
+ private long mDhcpLeaseTimeSecs;
+ private int mLinkMtu = MTU_UNSET;
+ private boolean mMetered;
+
+ /**
+ * Set the server address and served prefix for the DHCP server.
+ *
+ * <p>This parameter is required.
+ */
+ public Builder setServerAddr(@NonNull LinkAddress serverAddr) {
+ this.mServerAddr = serverAddr;
+ return this;
+ }
+
+ /**
+ * Set the default routers to be advertised to DHCP clients.
+ *
+ * <p>Each router must be inside the served prefix. This may be an empty set, but it must
+ * always be set explicitly before building the {@link DhcpServingParams}.
+ */
+ public Builder setDefaultRouters(@NonNull Set<Inet4Address> defaultRouters) {
+ this.mDefaultRouters = defaultRouters;
+ return this;
+ }
+
+ /**
+ * Set the default routers to be advertised to DHCP clients.
+ *
+ * <p>Each router must be inside the served prefix. This may be an empty list of routers,
+ * but it must always be set explicitly before building the {@link DhcpServingParams}.
+ */
+ public Builder setDefaultRouters(@NonNull Inet4Address... defaultRouters) {
+ return setDefaultRouters(Sets.newArraySet(defaultRouters));
+ }
+
+ /**
+ * Convenience method to build the parameters with no default router.
+ *
+ * <p>Equivalent to calling {@link #setDefaultRouters(Inet4Address...)} with no address.
+ */
+ public Builder withNoDefaultRouter() {
+ return setDefaultRouters();
+ }
+
+ /**
+ * Set the DNS servers to be advertised to DHCP clients.
+ *
+ * <p>This may be an empty set, but it must always be set explicitly before building the
+ * {@link DhcpServingParams}.
+ */
+ public Builder setDnsServers(@NonNull Set<Inet4Address> dnsServers) {
+ this.mDnsServers = dnsServers;
+ return this;
+ }
+
+ /**
+ * Set the DNS servers to be advertised to DHCP clients.
+ *
+ * <p>This may be an empty list of servers, but it must always be set explicitly before
+ * building the {@link DhcpServingParams}.
+ */
+ public Builder setDnsServers(@NonNull Inet4Address... dnsServers) {
+ return setDnsServers(Sets.newArraySet(dnsServers));
+ }
+
+ /**
+ * Convenience method to build the parameters with no DNS server.
+ *
+ * <p>Equivalent to calling {@link #setDnsServers(Inet4Address...)} with no address.
+ */
+ public Builder withNoDnsServer() {
+ return setDnsServers();
+ }
+
+ /**
+ * Set excluded addresses that the DHCP server is not allowed to assign to clients.
+ *
+ * <p>This parameter is optional. DNS servers and default routers are always excluded
+ * and do not need to be set here.
+ */
+ public Builder setExcludedAddrs(@NonNull Set<Inet4Address> excludedAddrs) {
+ this.mExcludedAddrs = excludedAddrs;
+ return this;
+ }
+
+ /**
+ * Set excluded addresses that the DHCP server is not allowed to assign to clients.
+ *
+ * <p>This parameter is optional. DNS servers and default routers are always excluded
+ * and do not need to be set here.
+ */
+ public Builder setExcludedAddrs(@NonNull Inet4Address... excludedAddrs) {
+ return setExcludedAddrs(Sets.newArraySet(excludedAddrs));
+ }
+
+ /**
+ * Set the lease time for leases assigned by the DHCP server.
+ *
+ * <p>This parameter is required.
+ */
+ public Builder setDhcpLeaseTimeSecs(long dhcpLeaseTimeSecs) {
+ this.mDhcpLeaseTimeSecs = dhcpLeaseTimeSecs;
+ return this;
+ }
+
+ /**
+ * Set the link MTU to be advertised to DHCP clients.
+ *
+ * <p>If set to {@link #MTU_UNSET}, no MTU will be advertised to clients. This parameter
+ * is optional and defaults to {@link #MTU_UNSET}.
+ */
+ public Builder setLinkMtu(int linkMtu) {
+ this.mLinkMtu = linkMtu;
+ return this;
+ }
+
+ /**
+ * Set whether the DHCP server should send the ANDROID_METERED vendor-specific option.
+ *
+ * <p>If not set, the default value is false.
+ */
+ public Builder setMetered(boolean metered) {
+ this.mMetered = metered;
+ return this;
+ }
+
+ /**
+ * Create a new {@link DhcpServingParams} instance based on parameters set in the builder.
+ *
+ * <p>This method has no side-effects. If it does not throw, a valid
+ * {@link DhcpServingParams} is returned.
+ * @return The constructed parameters.
+ * @throws InvalidParameterException At least one parameter is missing or invalid.
+ */
+ @NonNull
+ public DhcpServingParams build() throws InvalidParameterException {
+ if (mServerAddr == null) {
+ throw new InvalidParameterException("Missing serverAddr");
+ }
+ if (mDefaultRouters == null) {
+ throw new InvalidParameterException("Missing defaultRouters");
+ }
+ if (mDnsServers == null) {
+ // Empty set is OK, but enforce explicitly setting it
+ throw new InvalidParameterException("Missing dnsServers");
+ }
+ if (mDhcpLeaseTimeSecs <= 0 || mDhcpLeaseTimeSecs > toUnsignedLong(INFINITE_LEASE)) {
+ throw new InvalidParameterException("Invalid lease time: " + mDhcpLeaseTimeSecs);
+ }
+ if (mLinkMtu != MTU_UNSET && (mLinkMtu < IPV4_MIN_MTU || mLinkMtu > IPV4_MAX_MTU)) {
+ throw new InvalidParameterException("Invalid link MTU: " + mLinkMtu);
+ }
+ if (!mServerAddr.isIPv4()) {
+ throw new InvalidParameterException("serverAddr must be IPv4");
+ }
+ if (mServerAddr.getPrefixLength() < MIN_PREFIX_LENGTH
+ || mServerAddr.getPrefixLength() > MAX_PREFIX_LENGTH) {
+ throw new InvalidParameterException("Prefix length is not in supported range");
+ }
+
+ final IpPrefix prefix = makeIpPrefix(mServerAddr);
+ for (Inet4Address addr : mDefaultRouters) {
+ if (!prefix.contains(addr)) {
+ throw new InvalidParameterException(String.format(
+ "Default router %s is not in server prefix %s", addr, mServerAddr));
+ }
+ }
+
+ final Set<Inet4Address> excl = new HashSet<>();
+ if (mExcludedAddrs != null) {
+ excl.addAll(mExcludedAddrs);
+ }
+ excl.add((Inet4Address) mServerAddr.getAddress());
+ excl.addAll(mDefaultRouters);
+ excl.addAll(mDnsServers);
+
+ return new DhcpServingParams(mServerAddr,
+ Collections.unmodifiableSet(new HashSet<>(mDefaultRouters)),
+ Collections.unmodifiableSet(new HashSet<>(mDnsServers)),
+ Collections.unmodifiableSet(excl),
+ mDhcpLeaseTimeSecs, mLinkMtu, mMetered);
+ }
+ }
+
+ /**
+ * Utility method to create an IpPrefix with the address and prefix length of a LinkAddress.
+ */
+ @NonNull
+ static IpPrefix makeIpPrefix(@NonNull LinkAddress addr) {
+ return new IpPrefix(addr.getAddress(), addr.getPrefixLength());
+ }
+}
diff --git a/packages/NetworkStack/src/android/net/util/SharedLog.java b/packages/NetworkStack/src/android/net/util/SharedLog.java
new file mode 100644
index 0000000..74bc147
--- /dev/null
+++ b/packages/NetworkStack/src/android/net/util/SharedLog.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2017 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.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.LocalLog;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.StringJoiner;
+
+
+/**
+ * Class to centralize logging functionality for tethering.
+ *
+ * All access to class methods other than dump() must be on the same thread.
+ *
+ * @hide
+ */
+public class SharedLog {
+ private static final int DEFAULT_MAX_RECORDS = 500;
+ private static final String COMPONENT_DELIMITER = ".";
+
+ private enum Category {
+ NONE,
+ ERROR,
+ MARK,
+ WARN,
+ };
+
+ private final LocalLog mLocalLog;
+ // The tag to use for output to the system log. This is not output to the
+ // LocalLog because that would be redundant.
+ private final String mTag;
+ // The component (or subcomponent) of a system that is sharing this log.
+ // This can grow in depth if components call forSubComponent() to obtain
+ // their SharedLog instance. The tag is not included in the component for
+ // brevity.
+ private final String mComponent;
+
+ public SharedLog(String tag) {
+ this(DEFAULT_MAX_RECORDS, tag);
+ }
+
+ public SharedLog(int maxRecords, String tag) {
+ this(new LocalLog(maxRecords), tag, tag);
+ }
+
+ private SharedLog(LocalLog localLog, String tag, String component) {
+ mLocalLog = localLog;
+ mTag = tag;
+ mComponent = component;
+ }
+
+ /**
+ * Create a SharedLog based on this log with an additional component prefix on each logged line.
+ */
+ public SharedLog forSubComponent(String component) {
+ if (!isRootLogInstance()) {
+ component = mComponent + COMPONENT_DELIMITER + component;
+ }
+ return new SharedLog(mLocalLog, mTag, component);
+ }
+
+ /**
+ * Dump the contents of this log.
+ *
+ * <p>This method may be called on any thread.
+ */
+ public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+ mLocalLog.readOnlyLocalLog().dump(fd, writer, args);
+ }
+
+ //////
+ // Methods that both log an entry and emit it to the system log.
+ //////
+
+ /**
+ * Log an error due to an exception. This does not include the exception stacktrace.
+ *
+ * <p>The log entry will be also added to the system log.
+ * @see #e(String, Throwable)
+ */
+ public void e(Exception e) {
+ Log.e(mTag, record(Category.ERROR, e.toString()));
+ }
+
+ /**
+ * Log an error message.
+ *
+ * <p>The log entry will be also added to the system log.
+ */
+ public void e(String msg) {
+ Log.e(mTag, record(Category.ERROR, msg));
+ }
+
+ /**
+ * Log an error due to an exception, with the exception stacktrace if provided.
+ *
+ * <p>The error and exception message appear in the shared log, but the stacktrace is only
+ * logged in general log output (logcat). The log entry will be also added to the system log.
+ */
+ public void e(@NonNull String msg, @Nullable Throwable exception) {
+ if (exception == null) {
+ e(msg);
+ return;
+ }
+ Log.e(mTag, record(Category.ERROR, msg + ": " + exception.getMessage()), exception);
+ }
+
+ /**
+ * Log an informational message.
+ *
+ * <p>The log entry will be also added to the system log.
+ */
+ public void i(String msg) {
+ Log.i(mTag, record(Category.NONE, msg));
+ }
+
+ /**
+ * Log a warning message.
+ *
+ * <p>The log entry will be also added to the system log.
+ */
+ public void w(String msg) {
+ Log.w(mTag, record(Category.WARN, msg));
+ }
+
+ //////
+ // Methods that only log an entry (and do NOT emit to the system log).
+ //////
+
+ /**
+ * Log a general message to be only included in the in-memory log.
+ *
+ * <p>The log entry will *not* be added to the system log.
+ */
+ public void log(String msg) {
+ record(Category.NONE, msg);
+ }
+
+ /**
+ * Log a general, formatted message to be only included in the in-memory log.
+ *
+ * <p>The log entry will *not* be added to the system log.
+ * @see String#format(String, Object...)
+ */
+ public void logf(String fmt, Object... args) {
+ log(String.format(fmt, args));
+ }
+
+ /**
+ * Log a message with MARK level.
+ *
+ * <p>The log entry will *not* be added to the system log.
+ */
+ public void mark(String msg) {
+ record(Category.MARK, msg);
+ }
+
+ private String record(Category category, String msg) {
+ final String entry = logLine(category, msg);
+ mLocalLog.log(entry);
+ return entry;
+ }
+
+ private String logLine(Category category, String msg) {
+ final StringJoiner sj = new StringJoiner(" ");
+ if (!isRootLogInstance()) sj.add("[" + mComponent + "]");
+ if (category != Category.NONE) sj.add(category.toString());
+ return sj.add(msg).toString();
+ }
+
+ // Check whether this SharedLog instance is nominally the top level in
+ // a potential hierarchy of shared logs (the root of a tree),
+ // or is a subcomponent within the hierarchy.
+ private boolean isRootLogInstance() {
+ return TextUtils.isEmpty(mComponent) || mComponent.equals(mTag);
+ }
+}
diff --git a/packages/NetworkStack/src/com/android/server/NetworkStackService.java b/packages/NetworkStack/src/com/android/server/NetworkStackService.java
index 5afaf58..7fea1e0 100644
--- a/packages/NetworkStack/src/com/android/server/NetworkStackService.java
+++ b/packages/NetworkStack/src/com/android/server/NetworkStackService.java
@@ -16,15 +16,24 @@
package com.android.server;
-import static android.os.Binder.getCallingUid;
+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 com.android.server.util.PermissionUtil.checkNetworkStackCallingPermission;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Service;
import android.content.Intent;
import android.net.INetworkStackConnector;
+import android.net.dhcp.DhcpServer;
+import android.net.dhcp.DhcpServingParams;
+import android.net.dhcp.DhcpServingParamsParcel;
+import android.net.dhcp.IDhcpServerCallbacks;
+import android.net.util.SharedLog;
import android.os.IBinder;
-import android.os.Process;
+import android.os.RemoteException;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -54,21 +63,37 @@
}
private static class NetworkStackConnector extends INetworkStackConnector.Stub {
- // TODO: makeDhcpServer(), etc. will go here.
+ @NonNull
+ private final SharedLog mLog = new SharedLog(TAG);
+
+ @Override
+ public void makeDhcpServer(@NonNull String ifName, @NonNull DhcpServingParamsParcel params,
+ @NonNull IDhcpServerCallbacks cb) throws RemoteException {
+ checkNetworkStackCallingPermission();
+ final DhcpServer server;
+ try {
+ server = new DhcpServer(
+ ifName,
+ DhcpServingParams.fromParcelableObject(params),
+ mLog.forSubComponent(ifName + ".DHCP"));
+ } catch (DhcpServingParams.InvalidParameterException e) {
+ mLog.e("Invalid DhcpServingParams", e);
+ cb.onDhcpServerCreated(STATUS_INVALID_ARGUMENT, null);
+ return;
+ } catch (Exception e) {
+ mLog.e("Unknown error starting DhcpServer", e);
+ cb.onDhcpServerCreated(STATUS_UNKNOWN_ERROR, null);
+ return;
+ }
+ cb.onDhcpServerCreated(STATUS_SUCCESS, server);
+ }
@Override
protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter fout,
@Nullable String[] args) {
- checkCaller();
+ checkNetworkStackCallingPermission();
fout.println("NetworkStack logs:");
- // TODO: dump logs here
- }
- }
-
- private static void checkCaller() {
- // TODO: check that the calling PID is the system server.
- if (getCallingUid() != Process.SYSTEM_UID && getCallingUid() != Process.ROOT_UID) {
- throw new SecurityException("Invalid caller: " + getCallingUid());
+ mLog.dump(fd, fout, args);
}
}
}
diff --git a/packages/NetworkStack/src/com/android/server/util/NetworkStackConstants.java b/packages/NetworkStack/src/com/android/server/util/NetworkStackConstants.java
new file mode 100644
index 0000000..bb5900c
--- /dev/null
+++ b/packages/NetworkStack/src/com/android/server/util/NetworkStackConstants.java
@@ -0,0 +1,45 @@
+/*
+ * 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 com.android.server.util;
+
+/**
+ * Network constants used by the network stack.
+ */
+public final class NetworkStackConstants {
+
+ /**
+ * IPv4 constants.
+ *
+ * See also:
+ * - https://tools.ietf.org/html/rfc791
+ */
+ public static final int IPV4_ADDR_BITS = 32;
+ public static final int IPV4_MIN_MTU = 68;
+ public static final int IPV4_MAX_MTU = 65_535;
+
+ /**
+ * DHCP constants.
+ *
+ * See also:
+ * - https://tools.ietf.org/html/rfc2131
+ */
+ public static final int INFINITE_LEASE = 0xffffffff;
+
+ private NetworkStackConstants() {
+ throw new UnsupportedOperationException("This class is not to be instantiated");
+ }
+}
diff --git a/packages/NetworkStack/src/com/android/server/util/PermissionUtil.java b/packages/NetworkStack/src/com/android/server/util/PermissionUtil.java
new file mode 100644
index 0000000..733f873
--- /dev/null
+++ b/packages/NetworkStack/src/com/android/server/util/PermissionUtil.java
@@ -0,0 +1,42 @@
+/*
+ * 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 com.android.server.util;
+
+import static android.os.Binder.getCallingUid;
+
+import android.os.Process;
+
+/**
+ * Utility class to check calling permissions on the network stack.
+ */
+public final class PermissionUtil {
+
+ /**
+ * Check that the caller is allowed to communicate with the network stack.
+ * @throws SecurityException The caller is not allowed to communicate with the network stack.
+ */
+ public static void checkNetworkStackCallingPermission() {
+ // TODO: check that the calling PID is the system server.
+ if (getCallingUid() != Process.SYSTEM_UID && getCallingUid() != Process.ROOT_UID) {
+ throw new SecurityException("Invalid caller: " + getCallingUid());
+ }
+ }
+
+ private PermissionUtil() {
+ throw new UnsupportedOperationException("This class is not to be instantiated");
+ }
+}
diff --git a/packages/NetworkStack/tests/Android.bp b/packages/NetworkStack/tests/Android.bp
new file mode 100644
index 0000000..bd7ff2a
--- /dev/null
+++ b/packages/NetworkStack/tests/Android.bp
@@ -0,0 +1,35 @@
+//
+// 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.
+//
+
+android_test {
+ name: "NetworkStackTests",
+ srcs: ["src/**/*.java"],
+ static_libs: [
+ "android-support-test",
+ "mockito-target-extended-minus-junit4",
+ "NetworkStackLib",
+ "testables",
+ ],
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ ],
+ jni_libs: [
+ // For mockito extended
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ ]
+}
\ No newline at end of file
diff --git a/packages/NetworkStack/tests/AndroidManifest.xml b/packages/NetworkStack/tests/AndroidManifest.xml
new file mode 100644
index 0000000..8b8474f
--- /dev/null
+++ b/packages/NetworkStack/tests/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.server.networkstack.tests">
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+ <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.server.networkstack.tests"
+ android:label="Networking service tests">
+ </instrumentation>
+</manifest>
\ No newline at end of file
diff --git a/packages/NetworkStack/tests/AndroidTest.xml b/packages/NetworkStack/tests/AndroidTest.xml
new file mode 100644
index 0000000..6b08b57
--- /dev/null
+++ b/packages/NetworkStack/tests/AndroidTest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Runs Tests for NetworkStack">
+ <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+ <option name="test-file-name" value="NetworkStackTests.apk" />
+ </target_preparer>
+
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-suite-tag" value="framework-base-presubmit" />
+ <option name="test-tag" value="NetworkStackTests" />
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.server.networkstack.tests" />
+ <option name="runner" value="android.support.test.runner.AndroidJUnitRunner" />
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+</configuration>
\ No newline at end of file
diff --git a/packages/NetworkStack/tests/src/android/net/dhcp/DhcpLeaseRepositoryTest.java b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpLeaseRepositoryTest.java
new file mode 100644
index 0000000..51d50d9
--- /dev/null
+++ b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpLeaseRepositoryTest.java
@@ -0,0 +1,539 @@
+/*
+ * 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.InetAddresses.parseNumericAddress;
+import static android.net.dhcp.DhcpLease.HOSTNAME_NONE;
+import static android.net.dhcp.DhcpLeaseRepository.CLIENTID_UNSPEC;
+import static android.net.dhcp.DhcpLeaseRepository.INETADDR_UNSPEC;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.when;
+
+import static java.lang.String.format;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.IpPrefix;
+import android.net.MacAddress;
+import android.net.dhcp.DhcpServer.Clock;
+import android.net.util.SharedLog;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.Inet4Address;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DhcpLeaseRepositoryTest {
+ private static final Inet4Address INET4_ANY = (Inet4Address) Inet4Address.ANY;
+ private static final Inet4Address TEST_DEF_ROUTER = parseAddr4("192.168.42.247");
+ private static final Inet4Address TEST_SERVER_ADDR = parseAddr4("192.168.42.241");
+ private static final Inet4Address TEST_RESERVED_ADDR = parseAddr4("192.168.42.243");
+ private static final MacAddress TEST_MAC_1 = MacAddress.fromBytes(
+ new byte[] { 5, 4, 3, 2, 1, 0 });
+ private static final MacAddress TEST_MAC_2 = MacAddress.fromBytes(
+ new byte[] { 0, 1, 2, 3, 4, 5 });
+ private static final MacAddress TEST_MAC_3 = MacAddress.fromBytes(
+ new byte[] { 0, 1, 2, 3, 4, 6 });
+ private static final Inet4Address TEST_INETADDR_1 = parseAddr4("192.168.42.248");
+ private static final Inet4Address TEST_INETADDR_2 = parseAddr4("192.168.42.249");
+ private static final String TEST_HOSTNAME_1 = "hostname1";
+ private static final String TEST_HOSTNAME_2 = "hostname2";
+ private static final IpPrefix TEST_IP_PREFIX = new IpPrefix(TEST_SERVER_ADDR, 22);
+ private static final long TEST_TIME = 100L;
+ private static final int TEST_LEASE_TIME_MS = 3_600_000;
+ private static final Set<Inet4Address> TEST_EXCL_SET =
+ Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+ TEST_SERVER_ADDR, TEST_DEF_ROUTER, TEST_RESERVED_ADDR)));
+
+ @NonNull
+ private SharedLog mLog;
+ @NonNull @Mock
+ private Clock mClock;
+ @NonNull
+ private DhcpLeaseRepository mRepo;
+
+ private static Inet4Address parseAddr4(String inet4Addr) {
+ return (Inet4Address) parseNumericAddress(inet4Addr);
+ }
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mLog = new SharedLog("DhcpLeaseRepositoryTest");
+ when(mClock.elapsedRealtime()).thenReturn(TEST_TIME);
+ mRepo = new DhcpLeaseRepository(
+ TEST_IP_PREFIX, TEST_EXCL_SET, TEST_LEASE_TIME_MS, mLog, mClock);
+ }
+
+ /**
+ * Request a number of addresses through offer/request. Useful to test address exhaustion.
+ * @param nAddr Number of addresses to request.
+ */
+ private void requestAddresses(byte nAddr) throws Exception {
+ final HashSet<Inet4Address> addrs = new HashSet<>();
+ byte[] hwAddrBytes = new byte[] { 8, 4, 3, 2, 1, 0 };
+ for (byte i = 0; i < nAddr; i++) {
+ hwAddrBytes[5] = i;
+ MacAddress newMac = MacAddress.fromBytes(hwAddrBytes);
+ final String hostname = "host_" + i;
+ final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, newMac,
+ INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, hostname);
+
+ assertNotNull(lease);
+ assertEquals(newMac, lease.getHwAddr());
+ assertEquals(hostname, lease.getHostname());
+ assertTrue(format("Duplicate address allocated: %s in %s", lease.getNetAddr(), addrs),
+ addrs.add(lease.getNetAddr()));
+
+ requestLeaseSelecting(newMac, lease.getNetAddr(), hostname);
+ }
+ }
+
+ @Test
+ public void testAddressExhaustion() throws Exception {
+ // Use a /28 to quickly run out of addresses
+ mRepo.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), TEST_EXCL_SET, TEST_LEASE_TIME_MS);
+
+ // /28 should have 16 addresses, 14 w/o the first/last, 11 w/o excluded addresses
+ requestAddresses((byte) 11);
+
+ try {
+ mRepo.getOffer(null, TEST_MAC_2,
+ INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+ fail("Should be out of addresses");
+ } catch (DhcpLeaseRepository.OutOfAddressesException e) {
+ // Expected
+ }
+ }
+
+ @Test
+ public void testUpdateParams_LeaseCleanup() throws Exception {
+ // Inside /28:
+ final Inet4Address reqAddrIn28 = parseAddr4("192.168.42.242");
+ final Inet4Address declinedAddrIn28 = parseAddr4("192.168.42.245");
+
+ // Inside /28, but not available there (first address of the range)
+ final Inet4Address declinedFirstAddrIn28 = parseAddr4("192.168.42.240");
+
+ final DhcpLease reqAddrIn28Lease = requestLeaseSelecting(TEST_MAC_1, reqAddrIn28);
+ mRepo.markLeaseDeclined(declinedAddrIn28);
+ mRepo.markLeaseDeclined(declinedFirstAddrIn28);
+
+ // Inside /22, but outside /28:
+ final Inet4Address reqAddrIn22 = parseAddr4("192.168.42.3");
+ final Inet4Address declinedAddrIn22 = parseAddr4("192.168.42.4");
+
+ final DhcpLease reqAddrIn22Lease = requestLeaseSelecting(TEST_MAC_3, reqAddrIn22);
+ mRepo.markLeaseDeclined(declinedAddrIn22);
+
+ // Address that will be reserved in the updateParams call below
+ final Inet4Address reservedAddr = parseAddr4("192.168.42.244");
+ final DhcpLease reservedAddrLease = requestLeaseSelecting(TEST_MAC_2, reservedAddr);
+
+ // Update from /22 to /28 and add another reserved address
+ Set<Inet4Address> newReserved = new HashSet<>(TEST_EXCL_SET);
+ newReserved.add(reservedAddr);
+ mRepo.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), newReserved, TEST_LEASE_TIME_MS);
+
+ assertHasLease(reqAddrIn28Lease);
+ assertDeclined(declinedAddrIn28);
+
+ assertNotDeclined(declinedFirstAddrIn28);
+
+ assertNoLease(reqAddrIn22Lease);
+ assertNotDeclined(declinedAddrIn22);
+
+ assertNoLease(reservedAddrLease);
+ }
+
+ @Test
+ public void testGetOffer_StableAddress() throws Exception {
+ for (final MacAddress macAddr : new MacAddress[] { TEST_MAC_1, TEST_MAC_2, TEST_MAC_3 }) {
+ final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, macAddr,
+ INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+
+ // Same lease is offered twice
+ final DhcpLease newLease = mRepo.getOffer(CLIENTID_UNSPEC, macAddr,
+ INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+ assertEquals(lease, newLease);
+ }
+ }
+
+ @Test
+ public void testUpdateParams_UsesNewPrefix() throws Exception {
+ final IpPrefix newPrefix = new IpPrefix(parseAddr4("192.168.123.0"), 24);
+ mRepo.updateParams(newPrefix, TEST_EXCL_SET, TEST_LEASE_TIME_MS);
+
+ DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1,
+ INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+ assertTrue(newPrefix.contains(lease.getNetAddr()));
+ }
+
+ @Test
+ public void testGetOffer_ExistingLease() throws Exception {
+ requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1, TEST_HOSTNAME_1);
+
+ DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1,
+ INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+ assertEquals(TEST_INETADDR_1, offer.getNetAddr());
+ assertEquals(TEST_HOSTNAME_1, offer.getHostname());
+ }
+
+ @Test
+ public void testGetOffer_ClientIdHasExistingLease() throws Exception {
+ final byte[] clientId = new byte[] { 1, 2 };
+ mRepo.requestLease(clientId, TEST_MAC_1, INET4_ANY /* clientAddr */,
+ INET4_ANY /* relayAddr */, TEST_INETADDR_1 /* reqAddr */, false, TEST_HOSTNAME_1);
+
+ // Different MAC, but same clientId
+ DhcpLease offer = mRepo.getOffer(clientId, TEST_MAC_2,
+ INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+ assertEquals(TEST_INETADDR_1, offer.getNetAddr());
+ assertEquals(TEST_HOSTNAME_1, offer.getHostname());
+ }
+
+ @Test
+ public void testGetOffer_DifferentClientId() throws Exception {
+ final byte[] clientId1 = new byte[] { 1, 2 };
+ final byte[] clientId2 = new byte[] { 3, 4 };
+ mRepo.requestLease(clientId1, TEST_MAC_1, INET4_ANY /* clientAddr */,
+ INET4_ANY /* relayAddr */, TEST_INETADDR_1 /* reqAddr */, false, TEST_HOSTNAME_1);
+
+ // Same MAC, different client ID
+ DhcpLease offer = mRepo.getOffer(clientId2, TEST_MAC_1,
+ INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+ // Obtains a different address
+ assertNotEquals(TEST_INETADDR_1, offer.getNetAddr());
+ assertEquals(HOSTNAME_NONE, offer.getHostname());
+ assertEquals(TEST_MAC_1, offer.getHwAddr());
+ }
+
+ @Test
+ public void testGetOffer_RequestedAddress() throws Exception {
+ DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* relayAddr */,
+ TEST_INETADDR_1 /* reqAddr */, TEST_HOSTNAME_1);
+ assertEquals(TEST_INETADDR_1, offer.getNetAddr());
+ assertEquals(TEST_HOSTNAME_1, offer.getHostname());
+ }
+
+ @Test
+ public void testGetOffer_RequestedAddressInUse() throws Exception {
+ requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1);
+ DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_2, INET4_ANY /* relayAddr */,
+ TEST_INETADDR_1 /* reqAddr */, HOSTNAME_NONE);
+ assertNotEquals(TEST_INETADDR_1, offer.getNetAddr());
+ }
+
+ @Test
+ public void testGetOffer_RequestedAddressReserved() throws Exception {
+ DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* relayAddr */,
+ TEST_RESERVED_ADDR /* reqAddr */, HOSTNAME_NONE);
+ assertNotEquals(TEST_RESERVED_ADDR, offer.getNetAddr());
+ }
+
+ @Test
+ public void testGetOffer_RequestedAddressInvalid() throws Exception {
+ final Inet4Address invalidAddr = parseAddr4("192.168.42.0");
+ DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* relayAddr */,
+ invalidAddr /* reqAddr */, HOSTNAME_NONE);
+ assertNotEquals(invalidAddr, offer.getNetAddr());
+ }
+
+ @Test
+ public void testGetOffer_RequestedAddressOutsideSubnet() throws Exception {
+ final Inet4Address invalidAddr = parseAddr4("192.168.254.2");
+ DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* relayAddr */,
+ invalidAddr /* reqAddr */, HOSTNAME_NONE);
+ assertNotEquals(invalidAddr, offer.getNetAddr());
+ }
+
+ @Test(expected = DhcpLeaseRepository.InvalidSubnetException.class)
+ public void testGetOffer_RelayInInvalidSubnet() throws Exception {
+ mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, parseAddr4("192.168.254.2") /* relayAddr */,
+ INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+ }
+
+ @Test
+ public void testRequestLease_SelectingTwice() throws Exception {
+ final DhcpLease lease1 = requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1,
+ TEST_HOSTNAME_1);
+
+ // Second request from same client for a different address
+ final DhcpLease lease2 = requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_2,
+ TEST_HOSTNAME_2);
+
+ assertEquals(TEST_INETADDR_1, lease1.getNetAddr());
+ assertEquals(TEST_HOSTNAME_1, lease1.getHostname());
+
+ assertEquals(TEST_INETADDR_2, lease2.getNetAddr());
+ assertEquals(TEST_HOSTNAME_2, lease2.getHostname());
+
+ // First address freed when client requested a different one: another client can request it
+ final DhcpLease lease3 = requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1, HOSTNAME_NONE);
+ assertEquals(TEST_INETADDR_1, lease3.getNetAddr());
+ }
+
+ @Test(expected = DhcpLeaseRepository.InvalidAddressException.class)
+ public void testRequestLease_SelectingInvalid() throws Exception {
+ requestLeaseSelecting(TEST_MAC_1, parseAddr4("192.168.254.5"));
+ }
+
+ @Test(expected = DhcpLeaseRepository.InvalidAddressException.class)
+ public void testRequestLease_SelectingInUse() throws Exception {
+ requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1);
+ requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1);
+ }
+
+ @Test(expected = DhcpLeaseRepository.InvalidAddressException.class)
+ public void testRequestLease_SelectingReserved() throws Exception {
+ requestLeaseSelecting(TEST_MAC_1, TEST_RESERVED_ADDR);
+ }
+
+ @Test(expected = DhcpLeaseRepository.InvalidSubnetException.class)
+ public void testRequestLease_SelectingRelayInInvalidSubnet() throws Exception {
+ mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* clientAddr */,
+ parseAddr4("192.168.128.1") /* relayAddr */, TEST_INETADDR_1 /* reqAddr */,
+ true /* sidSet */, HOSTNAME_NONE);
+ }
+
+ @Test
+ public void testRequestLease_InitReboot() throws Exception {
+ // Request address once
+ requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1);
+
+ final long newTime = TEST_TIME + 100;
+ when(mClock.elapsedRealtime()).thenReturn(newTime);
+
+ // init-reboot (sidSet == false): verify configuration
+ final DhcpLease lease = requestLeaseInitReboot(TEST_MAC_1, TEST_INETADDR_1);
+ assertEquals(TEST_INETADDR_1, lease.getNetAddr());
+ assertEquals(newTime + TEST_LEASE_TIME_MS, lease.getExpTime());
+ }
+
+ @Test(expected = DhcpLeaseRepository.InvalidAddressException.class)
+ public void testRequestLease_InitRebootWrongAddr() throws Exception {
+ // Request address once
+ requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1);
+ // init-reboot with different requested address
+ requestLeaseInitReboot(TEST_MAC_1, TEST_INETADDR_2);
+ }
+
+ @Test
+ public void testRequestLease_InitRebootUnknownAddr() throws Exception {
+ // init-reboot with unknown requested address
+ final DhcpLease lease = requestLeaseInitReboot(TEST_MAC_1, TEST_INETADDR_2);
+ // RFC2131 says we should not reply to accommodate other servers, but since we are
+ // authoritative we allow creating the lease to avoid issues with lost lease DB (same as
+ // dnsmasq behavior)
+ assertEquals(TEST_INETADDR_2, lease.getNetAddr());
+ }
+
+ @Test(expected = DhcpLeaseRepository.InvalidAddressException.class)
+ public void testRequestLease_InitRebootWrongSubnet() throws Exception {
+ requestLeaseInitReboot(TEST_MAC_1, parseAddr4("192.168.254.2"));
+ }
+
+ @Test
+ public void testRequestLease_Renewing() throws Exception {
+ requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1);
+
+ final long newTime = TEST_TIME + 100;
+ when(mClock.elapsedRealtime()).thenReturn(newTime);
+
+ final DhcpLease lease = requestLeaseRenewing(TEST_MAC_1, TEST_INETADDR_1);
+
+ assertEquals(TEST_INETADDR_1, lease.getNetAddr());
+ assertEquals(newTime + TEST_LEASE_TIME_MS, lease.getExpTime());
+ }
+
+ @Test
+ public void testRequestLease_RenewingUnknownAddr() throws Exception {
+ final long newTime = TEST_TIME + 100;
+ when(mClock.elapsedRealtime()).thenReturn(newTime);
+ final DhcpLease lease = requestLeaseRenewing(TEST_MAC_1, TEST_INETADDR_1);
+ // Allows renewing an unknown address if available
+ assertEquals(TEST_INETADDR_1, lease.getNetAddr());
+ assertEquals(newTime + TEST_LEASE_TIME_MS, lease.getExpTime());
+ }
+
+ @Test(expected = DhcpLeaseRepository.InvalidAddressException.class)
+ public void testRequestLease_RenewingAddrInUse() throws Exception {
+ requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1);
+ requestLeaseRenewing(TEST_MAC_1, TEST_INETADDR_1);
+ }
+
+ @Test(expected = DhcpLeaseRepository.InvalidAddressException.class)
+ public void testRequestLease_RenewingInvalidAddr() throws Exception {
+ requestLeaseRenewing(TEST_MAC_1, parseAddr4("192.168.254.2"));
+ }
+
+ @Test
+ public void testReleaseLease() throws Exception {
+ final DhcpLease lease1 = requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1);
+
+ assertHasLease(lease1);
+ assertTrue(mRepo.releaseLease(CLIENTID_UNSPEC, TEST_MAC_1, TEST_INETADDR_1));
+ assertNoLease(lease1);
+
+ final DhcpLease lease2 = requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1);
+ assertEquals(TEST_INETADDR_1, lease2.getNetAddr());
+ }
+
+ @Test
+ public void testReleaseLease_UnknownLease() {
+ assertFalse(mRepo.releaseLease(CLIENTID_UNSPEC, TEST_MAC_1, TEST_INETADDR_1));
+ }
+
+ @Test
+ public void testReleaseLease_StableOffer() throws Exception {
+ for (MacAddress mac : new MacAddress[] { TEST_MAC_1, TEST_MAC_2, TEST_MAC_3 }) {
+ final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, mac,
+ INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+
+ requestLeaseSelecting(mac, lease.getNetAddr());
+ mRepo.releaseLease(CLIENTID_UNSPEC, mac, lease.getNetAddr());
+
+ // Same lease is offered after it was released
+ final DhcpLease newLease = mRepo.getOffer(CLIENTID_UNSPEC, mac,
+ INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+ assertEquals(lease.getNetAddr(), newLease.getNetAddr());
+ }
+ }
+
+ @Test
+ public void testMarkLeaseDeclined() throws Exception {
+ final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1,
+ INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+
+ mRepo.markLeaseDeclined(lease.getNetAddr());
+
+ // Same lease is not offered again
+ final DhcpLease newLease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1,
+ INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+ assertNotEquals(lease.getNetAddr(), newLease.getNetAddr());
+ }
+
+ @Test
+ public void testMarkLeaseDeclined_UsedIfOutOfAddresses() throws Exception {
+ // Use a /28 to quickly run out of addresses
+ mRepo.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), TEST_EXCL_SET, TEST_LEASE_TIME_MS);
+
+ mRepo.markLeaseDeclined(TEST_INETADDR_1);
+ mRepo.markLeaseDeclined(TEST_INETADDR_2);
+
+ // /28 should have 16 addresses, 14 w/o the first/last, 11 w/o excluded addresses
+ requestAddresses((byte) 9);
+
+ // Last 2 addresses: addresses marked declined should be used
+ final DhcpLease firstLease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1,
+ INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, TEST_HOSTNAME_1);
+ requestLeaseSelecting(TEST_MAC_1, firstLease.getNetAddr());
+
+ final DhcpLease secondLease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_2,
+ INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, TEST_HOSTNAME_2);
+ requestLeaseSelecting(TEST_MAC_2, secondLease.getNetAddr());
+
+ // Now out of addresses
+ try {
+ mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_3, INET4_ANY /* relayAddr */,
+ INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+ fail("Repository should be out of addresses and throw");
+ } catch (DhcpLeaseRepository.OutOfAddressesException e) { /* expected */ }
+
+ assertEquals(TEST_INETADDR_1, firstLease.getNetAddr());
+ assertEquals(TEST_HOSTNAME_1, firstLease.getHostname());
+ assertEquals(TEST_INETADDR_2, secondLease.getNetAddr());
+ assertEquals(TEST_HOSTNAME_2, secondLease.getHostname());
+ }
+
+ private DhcpLease requestLease(@NonNull MacAddress macAddr, @NonNull Inet4Address clientAddr,
+ @Nullable Inet4Address reqAddr, @Nullable String hostname, boolean sidSet)
+ throws DhcpLeaseRepository.DhcpLeaseException {
+ return mRepo.requestLease(CLIENTID_UNSPEC, macAddr, clientAddr, INET4_ANY /* relayAddr */,
+ reqAddr, sidSet, hostname);
+ }
+
+ /**
+ * Request a lease simulating a client in the SELECTING state.
+ */
+ private DhcpLease requestLeaseSelecting(@NonNull MacAddress macAddr,
+ @NonNull Inet4Address reqAddr, @Nullable String hostname)
+ throws DhcpLeaseRepository.DhcpLeaseException {
+ return requestLease(macAddr, INET4_ANY /* clientAddr */, reqAddr, hostname,
+ true /* sidSet */);
+ }
+
+ /**
+ * Request a lease simulating a client in the SELECTING state.
+ */
+ private DhcpLease requestLeaseSelecting(@NonNull MacAddress macAddr,
+ @NonNull Inet4Address reqAddr) throws DhcpLeaseRepository.DhcpLeaseException {
+ return requestLeaseSelecting(macAddr, reqAddr, HOSTNAME_NONE);
+ }
+
+ /**
+ * Request a lease simulating a client in the INIT-REBOOT state.
+ */
+ private DhcpLease requestLeaseInitReboot(@NonNull MacAddress macAddr,
+ @NonNull Inet4Address reqAddr) throws DhcpLeaseRepository.DhcpLeaseException {
+ return requestLease(macAddr, INET4_ANY /* clientAddr */, reqAddr, HOSTNAME_NONE,
+ false /* sidSet */);
+ }
+
+ /**
+ * Request a lease simulating a client in the RENEWING state.
+ */
+ private DhcpLease requestLeaseRenewing(@NonNull MacAddress macAddr,
+ @NonNull Inet4Address clientAddr) throws DhcpLeaseRepository.DhcpLeaseException {
+ // Renewing: clientAddr filled in, no reqAddr
+ return requestLease(macAddr, clientAddr, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE,
+ true /* sidSet */);
+ }
+
+ private void assertNoLease(DhcpLease lease) {
+ assertFalse("Leases contain " + lease, mRepo.getCommittedLeases().contains(lease));
+ }
+
+ private void assertHasLease(DhcpLease lease) {
+ assertTrue("Leases do not contain " + lease, mRepo.getCommittedLeases().contains(lease));
+ }
+
+ private void assertNotDeclined(Inet4Address addr) {
+ assertFalse("Address is declined: " + addr, mRepo.getDeclinedAddresses().contains(addr));
+ }
+
+ private void assertDeclined(Inet4Address addr) {
+ assertTrue("Address is not declined: " + addr, mRepo.getDeclinedAddresses().contains(addr));
+ }
+}
diff --git a/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServerTest.java b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServerTest.java
new file mode 100644
index 0000000..d4c1e2e
--- /dev/null
+++ b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServerTest.java
@@ -0,0 +1,327 @@
+/*
+ * 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.InetAddresses.parseNumericAddress;
+import static android.net.dhcp.DhcpPacket.DHCP_CLIENT;
+import static android.net.dhcp.DhcpPacket.DHCP_HOST_NAME;
+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 android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.INetworkStackStatusCallback;
+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.SharedLog;
+import android.os.HandlerThread;
+import android.support.test.filters.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.testing.TestableLooper.RunWithLooper;
+
+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(AndroidTestingRunner.class)
+@SmallTest
+@RunWithLooper
+public class DhcpServerTest {
+ private static final String TEST_IFACE = "testiface";
+
+ 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 String TEST_HOSTNAME = "testhostname";
+
+ 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 */);
+ private static final DhcpLease TEST_LEASE_WITH_HOSTNAME = new DhcpLease(null, TEST_CLIENT_MAC,
+ TEST_CLIENT_ADDR, TEST_LEASE_EXPTIME_SECS * 1000L + TEST_CLOCK_TIME, TEST_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 HandlerThread mHandlerThread;
+ @NonNull
+ private TestableLooper mLooper;
+ @NonNull
+ private DhcpServer mServer;
+
+ @Nullable
+ private String mPrevShareClassloaderProp;
+
+ private final INetworkStackStatusCallback mAssertSuccessCallback =
+ new INetworkStackStatusCallback.Stub() {
+ @Override
+ public void onStatusAvailable(int statusCode) {
+ assertEquals(STATUS_SUCCESS, statusCode);
+ }
+ };
+
+ @Before
+ public void setUp() throws Exception {
+ 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 = TestableLooper.get(this);
+ mHandlerThread = spy(new HandlerThread("TestDhcpServer"));
+ when(mHandlerThread.getLooper()).thenReturn(mLooper.getLooper());
+ mServer = new DhcpServer(mHandlerThread, TEST_IFACE, servingParams,
+ new SharedLog(DhcpServerTest.class.getSimpleName()), mDeps);
+
+ mServer.start(mAssertSuccessCallback);
+ mLooper.processAllMessages();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mServer.stop(mAssertSuccessCallback);
+ mLooper.processMessages(1);
+ verify(mPacketListener, times(1)).stop();
+ verify(mHandlerThread, times(1)).quitSafely();
+ }
+
+ @Test
+ public void testStart() throws Exception {
+ verify(mPacketListener, times(1)).start();
+ }
+
+ @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, DHCP_CLIENT);
+
+ 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, DHCP_CLIENT);
+
+ assertResponseSentTo(INADDR_BROADCAST);
+ final DhcpNakPacket packet = assertNak(getPacket());
+ assertMatchesClient(packet);
+ }
+
+ private DhcpRequestPacket makeRequestSelectingPacket() {
+ 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;
+ return request;
+ }
+
+ @Test
+ public void testRequest_Selecting_Ack() throws Exception {
+ when(mRepository.requestLease(isNull() /* clientId */, eq(TEST_CLIENT_MAC),
+ eq(INADDR_ANY) /* clientAddr */, eq(INADDR_ANY) /* relayAddr */,
+ eq(TEST_CLIENT_ADDR) /* reqAddr */, eq(true) /* sidSet */, eq(TEST_HOSTNAME)))
+ .thenReturn(TEST_LEASE_WITH_HOSTNAME);
+
+ final DhcpRequestPacket request = makeRequestSelectingPacket();
+ request.mHostName = TEST_HOSTNAME;
+ request.mRequestedParams = new byte[] { DHCP_HOST_NAME };
+ mServer.processPacket(request, DHCP_CLIENT);
+
+ assertResponseSentTo(TEST_CLIENT_ADDR);
+ final DhcpAckPacket packet = assertAck(getPacket());
+ assertMatchesTestLease(packet, TEST_HOSTNAME);
+ }
+
+ @Test
+ public void testRequest_Selecting_Nak() throws Exception {
+ when(mRepository.requestLease(isNull(), eq(TEST_CLIENT_MAC),
+ eq(INADDR_ANY) /* clientAddr */, eq(INADDR_ANY) /* relayAddr */,
+ eq(TEST_CLIENT_ADDR) /* reqAddr */, eq(true) /* sidSet */, isNull() /* hostname */))
+ .thenThrow(new InvalidAddressException("Test error"));
+
+ final DhcpRequestPacket request = makeRequestSelectingPacket();
+ mServer.processPacket(request, DHCP_CLIENT);
+
+ assertResponseSentTo(INADDR_BROADCAST);
+ final DhcpNakPacket packet = assertNak(getPacket());
+ assertMatchesClient(packet);
+ }
+
+ @Test
+ public void testRequest_Selecting_WrongClientPort() throws Exception {
+ final DhcpRequestPacket request = makeRequestSelectingPacket();
+ mServer.processPacket(request, 50000);
+
+ verify(mRepository, never())
+ .requestLease(any(), any(), any(), any(), any(), anyBoolean(), any());
+ verify(mDeps, never()).sendPacket(any(), any(), any());
+ }
+
+ @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, DHCP_CLIENT);
+
+ 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, @Nullable String hostname) {
+ 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);
+ assertEquals(hostname, packet.mHostName);
+ }
+
+ private void assertMatchesTestLease(@NonNull DhcpPacket packet) {
+ assertMatchesTestLease(packet, null);
+ }
+
+ 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);
+ }
+}
diff --git a/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServingParamsTest.java b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServingParamsTest.java
new file mode 100644
index 0000000..3ca0564
--- /dev/null
+++ b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServingParamsTest.java
@@ -0,0 +1,220 @@
+/*
+ * 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.InetAddresses.parseNumericAddress;
+import static android.net.NetworkUtils.inet4AddressToIntHTH;
+import static android.net.dhcp.DhcpServingParams.MTU_UNSET;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.LinkAddress;
+import android.net.NetworkUtils;
+import android.net.dhcp.DhcpServingParams.InvalidParameterException;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Modifier;
+import java.net.Inet4Address;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DhcpServingParamsTest {
+ @NonNull
+ private DhcpServingParams.Builder mBuilder;
+
+ 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 long TEST_LEASE_TIME_SECS = 3600L;
+ 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 Inet4Address TEST_SERVER_ADDR = parseAddr("192.168.0.2");
+ private static final LinkAddress TEST_LINKADDR = new LinkAddress(TEST_SERVER_ADDR, 20);
+ private static final int TEST_MTU = 1500;
+ 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 boolean TEST_METERED = true;
+
+ @Before
+ public void setUp() {
+ mBuilder = new DhcpServingParams.Builder()
+ .setDefaultRouters(TEST_DEFAULT_ROUTERS)
+ .setDhcpLeaseTimeSecs(TEST_LEASE_TIME_SECS)
+ .setDnsServers(TEST_DNS_SERVERS)
+ .setServerAddr(TEST_LINKADDR)
+ .setLinkMtu(TEST_MTU)
+ .setExcludedAddrs(TEST_EXCLUDED_ADDRS)
+ .setMetered(TEST_METERED);
+ }
+
+ @Test
+ public void testBuild_Immutable() throws InvalidParameterException {
+ final Set<Inet4Address> routers = new HashSet<>(TEST_DEFAULT_ROUTERS);
+ final Set<Inet4Address> dnsServers = new HashSet<>(TEST_DNS_SERVERS);
+ final Set<Inet4Address> excludedAddrs = new HashSet<>(TEST_EXCLUDED_ADDRS);
+
+ final DhcpServingParams params = mBuilder
+ .setDefaultRouters(routers)
+ .setDnsServers(dnsServers)
+ .setExcludedAddrs(excludedAddrs)
+ .build();
+
+ // Modifications to source objects should not affect builder or final parameters
+ final Inet4Address addedAddr = parseAddr("192.168.0.223");
+ routers.add(addedAddr);
+ dnsServers.add(addedAddr);
+ excludedAddrs.add(addedAddr);
+
+ assertEquals(TEST_DEFAULT_ROUTERS, params.defaultRouters);
+ assertEquals(TEST_LEASE_TIME_SECS, params.dhcpLeaseTimeSecs);
+ assertEquals(TEST_DNS_SERVERS, params.dnsServers);
+ assertEquals(TEST_LINKADDR, params.serverAddr);
+ assertEquals(TEST_MTU, params.linkMtu);
+ assertEquals(TEST_METERED, params.metered);
+
+ assertContains(params.excludedAddrs, TEST_EXCLUDED_ADDRS);
+ assertContains(params.excludedAddrs, TEST_DEFAULT_ROUTERS);
+ assertContains(params.excludedAddrs, TEST_DNS_SERVERS);
+ assertContains(params.excludedAddrs, TEST_SERVER_ADDR);
+
+ assertFalse("excludedAddrs should not contain " + addedAddr,
+ params.excludedAddrs.contains(addedAddr));
+ }
+
+ @Test(expected = InvalidParameterException.class)
+ public void testBuild_NegativeLeaseTime() throws InvalidParameterException {
+ mBuilder.setDhcpLeaseTimeSecs(-1).build();
+ }
+
+ @Test(expected = InvalidParameterException.class)
+ public void testBuild_LeaseTimeTooLarge() throws InvalidParameterException {
+ // Set lease time larger than max value for uint32
+ mBuilder.setDhcpLeaseTimeSecs(1L << 32).build();
+ }
+
+ @Test
+ public void testBuild_InfiniteLeaseTime() throws InvalidParameterException {
+ final long infiniteLeaseTime = 0xffffffffL;
+ final DhcpServingParams params = mBuilder
+ .setDhcpLeaseTimeSecs(infiniteLeaseTime).build();
+ assertEquals(infiniteLeaseTime, params.dhcpLeaseTimeSecs);
+ assertTrue(params.dhcpLeaseTimeSecs > 0L);
+ }
+
+ @Test
+ public void testBuild_UnsetMtu() throws InvalidParameterException {
+ final DhcpServingParams params = mBuilder.setLinkMtu(MTU_UNSET).build();
+ assertEquals(MTU_UNSET, params.linkMtu);
+ }
+
+ @Test(expected = InvalidParameterException.class)
+ public void testBuild_MtuTooSmall() throws InvalidParameterException {
+ mBuilder.setLinkMtu(20).build();
+ }
+
+ @Test(expected = InvalidParameterException.class)
+ public void testBuild_MtuTooLarge() throws InvalidParameterException {
+ mBuilder.setLinkMtu(65_536).build();
+ }
+
+ @Test(expected = InvalidParameterException.class)
+ public void testBuild_IPv6Addr() throws InvalidParameterException {
+ mBuilder.setServerAddr(new LinkAddress(parseNumericAddress("fe80::1111"), 120)).build();
+ }
+
+ @Test(expected = InvalidParameterException.class)
+ public void testBuild_PrefixTooLarge() throws InvalidParameterException {
+ mBuilder.setServerAddr(new LinkAddress(TEST_SERVER_ADDR, 15)).build();
+ }
+
+ @Test(expected = InvalidParameterException.class)
+ public void testBuild_PrefixTooSmall() throws InvalidParameterException {
+ mBuilder.setDefaultRouters(parseAddr("192.168.0.254"))
+ .setServerAddr(new LinkAddress(TEST_SERVER_ADDR, 31))
+ .build();
+ }
+
+ @Test(expected = InvalidParameterException.class)
+ public void testBuild_RouterNotInPrefix() throws InvalidParameterException {
+ mBuilder.setDefaultRouters(parseAddr("192.168.254.254")).build();
+ }
+
+ @Test
+ public void testFromParcelableObject() throws InvalidParameterException {
+ final DhcpServingParams params = mBuilder.build();
+ final DhcpServingParamsParcel parcel = new DhcpServingParamsParcel();
+ parcel.defaultRouters = toIntArray(TEST_DEFAULT_ROUTERS);
+ parcel.dhcpLeaseTimeSecs = TEST_LEASE_TIME_SECS;
+ parcel.dnsServers = toIntArray(TEST_DNS_SERVERS);
+ parcel.serverAddr = inet4AddressToIntHTH(TEST_SERVER_ADDR);
+ parcel.serverAddrPrefixLength = TEST_LINKADDR.getPrefixLength();
+ parcel.linkMtu = TEST_MTU;
+ parcel.excludedAddrs = toIntArray(TEST_EXCLUDED_ADDRS);
+ parcel.metered = TEST_METERED;
+ final DhcpServingParams parceled = DhcpServingParams.fromParcelableObject(parcel);
+
+ assertEquals(params.defaultRouters, parceled.defaultRouters);
+ assertEquals(params.dhcpLeaseTimeSecs, parceled.dhcpLeaseTimeSecs);
+ assertEquals(params.dnsServers, parceled.dnsServers);
+ assertEquals(params.serverAddr, parceled.serverAddr);
+ assertEquals(params.linkMtu, parceled.linkMtu);
+ assertEquals(params.excludedAddrs, parceled.excludedAddrs);
+ assertEquals(params.metered, parceled.metered);
+
+ // Ensure that we do not miss any field if added in the future
+ final long numFields = Arrays.stream(DhcpServingParams.class.getDeclaredFields())
+ .filter(f -> !Modifier.isStatic(f.getModifiers()))
+ .count();
+ assertEquals(7, numFields);
+ }
+
+ @Test(expected = InvalidParameterException.class)
+ public void testFromParcelableObject_NullArgument() throws InvalidParameterException {
+ DhcpServingParams.fromParcelableObject(null);
+ }
+
+ private static int[] toIntArray(Collection<Inet4Address> addrs) {
+ return addrs.stream().mapToInt(NetworkUtils::inet4AddressToIntHTH).toArray();
+ }
+
+ private static <T> void assertContains(@NonNull Set<T> set, @NonNull Set<T> subset) {
+ for (final T elem : subset) {
+ assertContains(set, elem);
+ }
+ }
+
+ private static <T> void assertContains(@NonNull Set<T> set, @Nullable T elem) {
+ assertTrue("Set does not contain " + elem, set.contains(elem));
+ }
+
+ @NonNull
+ private static Inet4Address parseAddr(@NonNull String inet4Addr) {
+ return (Inet4Address) parseNumericAddress(inet4Addr);
+ }
+}
diff --git a/packages/NetworkStack/tests/src/com/android/server/util/SharedLogTest.java b/packages/NetworkStack/tests/src/com/android/server/util/SharedLogTest.java
new file mode 100644
index 0000000..07ad3123
--- /dev/null
+++ b/packages/NetworkStack/tests/src/com/android/server/util/SharedLogTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2017 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 com.android.server.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.net.util.SharedLog;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SharedLogTest {
+ private static final String TIMESTAMP_PATTERN = "\\d{2}:\\d{2}:\\d{2}";
+ private static final String TIMESTAMP = "HH:MM:SS";
+
+ @Test
+ public void testBasicOperation() {
+ final SharedLog logTop = new SharedLog("top");
+ logTop.mark("first post!");
+
+ final SharedLog logLevel2a = logTop.forSubComponent("twoA");
+ final SharedLog logLevel2b = logTop.forSubComponent("twoB");
+ logLevel2b.e("2b or not 2b");
+ logLevel2b.e("No exception", null);
+ logLevel2b.e("Wait, here's one", new Exception("Test"));
+ logLevel2a.w("second post?");
+
+ final SharedLog logLevel3 = logLevel2a.forSubComponent("three");
+ logTop.log("still logging");
+ logLevel3.log("3 >> 2");
+ logLevel2a.mark("ok: last post");
+
+ final String[] expected = {
+ " - MARK first post!",
+ " - [twoB] ERROR 2b or not 2b",
+ " - [twoB] ERROR No exception",
+ // No stacktrace in shared log, only in logcat
+ " - [twoB] ERROR Wait, here's one: Test",
+ " - [twoA] WARN second post?",
+ " - still logging",
+ " - [twoA.three] 3 >> 2",
+ " - [twoA] MARK ok: last post",
+ };
+ // Verify the logs are all there and in the correct order.
+ verifyLogLines(expected, logTop);
+
+ // In fact, because they all share the same underlying LocalLog,
+ // every subcomponent SharedLog's dump() is identical.
+ verifyLogLines(expected, logLevel2a);
+ verifyLogLines(expected, logLevel2b);
+ verifyLogLines(expected, logLevel3);
+ }
+
+ private static void verifyLogLines(String[] expected, SharedLog log) {
+ final ByteArrayOutputStream ostream = new ByteArrayOutputStream();
+ final PrintWriter pw = new PrintWriter(ostream, true);
+ log.dump(null, pw, null);
+
+ final String dumpOutput = ostream.toString();
+ assertTrue(dumpOutput != null);
+ assertTrue(!"".equals(dumpOutput));
+
+ final String[] lines = dumpOutput.split("\n");
+ assertEquals(expected.length, lines.length);
+
+ for (int i = 0; i < expected.length; i++) {
+ String got = lines[i];
+ String want = expected[i];
+ assertTrue(String.format("'%s' did not contain '%s'", got, want), got.endsWith(want));
+ assertTrue(String.format("'%s' did not contain a %s timestamp", got, TIMESTAMP),
+ got.replaceFirst(TIMESTAMP_PATTERN, TIMESTAMP).contains(TIMESTAMP));
+ }
+ }
+}