diff --git a/services/net/java/android/net/dhcp/DhcpLease.java b/services/net/java/android/net/dhcp/DhcpLease.java new file mode 100644 index 0000000000000..d2a15b37dcc0c --- /dev/null +++ b/services/net/java/android/net/dhcp/DhcpLease.java @@ -0,0 +1,138 @@ +/* + * 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; + } + + @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. + * + *

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)); + } + + 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); + } + + @Override + public String toString() { + return String.format("clientId: %s, hwAddr: %s, netAddr: %s, expTime: %d, hostname: %s", + clientIdToString(mClientId), mHwAddr.toString(), mNetAddr, mExpTime, mHostname); + } +} diff --git a/services/net/java/android/net/dhcp/DhcpLeaseRepository.java b/services/net/java/android/net/dhcp/DhcpLeaseRepository.java new file mode 100644 index 0000000000000..b7a55a47a6a6c --- /dev/null +++ b/services/net/java/android/net/dhcp/DhcpLeaseRepository.java @@ -0,0 +1,538 @@ +/* + * 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.util.NetworkConstants.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.util.SharedLog; +import android.os.SystemClock; +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. + * + *

This class is not thread-safe. All public methods should be called on a common thread or + * use some synchronization mechanism. + * + *

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 mReservedAddrs; + private int mSubnetAddr; + private int mSubnetMask; + private int mNumAddresses; + private long mLeaseTimeMs; + + public static class Clock { + /** + * @see SystemClock#elapsedRealtime() + */ + public long elapsedRealtime() { + return SystemClock.elapsedRealtime(); + } + } + + /** + * Next timestamp when committed or declined leases should be checked for expired ones. This + * will always be lower than or equal to the time for the first lease to expire: it's OK not to + * 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); + } + } + + /** + * Leases by IP address + */ + private final ArrayMap 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 mDeclinedAddrs = new LinkedHashMap<>(); + + public DhcpLeaseRepository(@NonNull IpPrefix prefix, @NonNull Set 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 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 void cleanMap(Map map) { + final Iterator> 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 InvalidAddressException 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, InvalidAddressException { + final long currentTime = mClock.elapsedRealtime(); + final long expTime = currentTime + mLeaseTimeMs; + + removeExpiredLeases(currentTime); + + // 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 InvalidAddressException("Lease requested by relay from outside of subnet"); + } + + 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 static boolean isIpAddrOutsidePrefix(IpPrefix prefix, 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. + * + *

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, @Nullable Inet4Address reqAddr, boolean sidSet, + @Nullable String hostname) throws InvalidAddressException { + final long currentTime = mClock.elapsedRealtime(); + removeExpiredLeases(currentTime); + 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, reqAddr, sidSet, lease); + return lease; + } + + /** + * Check that the client can request the specified address, make or renew the lease if yes, and + * commit it. + * + *

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 " + 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", addr); + return; + } + final long expTime = mClock.elapsedRealtime() + mLeaseTimeMs; + mDeclinedAddrs.put(addr, expTime); + mLog.logf("Marked %s as declined expiring %d", addr, expTime); + maybeUpdateEarliestExpiration(expTime); + } + + /** + * Get the list of currently valid committed leases in the repository. + */ + @NonNull + public List 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 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 long removeExpired(long currentTime, @NonNull Map map, + @NonNull String tag, @NonNull Function getExpTime) { + final Iterator> it = map.entrySet().iterator(); + long firstExpiration = EXPIRATION_NEVER; + while (it.hasNext()) { + final Entry 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. + * + *

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. + * + *

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. + * + *

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. + * + *

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. + * + *

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 = (Inet4Address) 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 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", 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/tests/net/java/android/net/dhcp/DhcpLeaseRepositoryTest.java b/tests/net/java/android/net/dhcp/DhcpLeaseRepositoryTest.java new file mode 100644 index 0000000000000..edadd6ead667f --- /dev/null +++ b/tests/net/java/android/net/dhcp/DhcpLeaseRepositoryTest.java @@ -0,0 +1,519 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.dhcp; + +import static android.net.dhcp.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 static java.net.InetAddress.parseNumericAddress; + +import android.annotation.NonNull; +import android.net.IpPrefix; +import android.net.MacAddress; +import android.net.dhcp.DhcpLeaseRepository.Clock; +import android.net.util.SharedLog; +import android.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 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 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, + INETADDR_UNSPEC /* 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())); + + mRepo.requestLease(null, newMac, null, lease.getNetAddr(), true, 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, + null /* relayAddr */, null /* 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 = mRepo.requestLease( + CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, reqAddrIn28, false, HOSTNAME_NONE); + 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 = mRepo.requestLease( + CLIENTID_UNSPEC, TEST_MAC_3, INET4_ANY, reqAddrIn22, false, HOSTNAME_NONE); + mRepo.markLeaseDeclined(declinedAddrIn22); + + // Address that will be reserved in the updateParams call below + final Inet4Address reservedAddr = parseAddr4("192.168.42.244"); + final DhcpLease reservedAddrLease = mRepo.requestLease( + CLIENTID_UNSPEC, TEST_MAC_2, INET4_ANY, reservedAddr, false, HOSTNAME_NONE); + + // Update from /22 to /28 and add another reserved address + Set 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, + INETADDR_UNSPEC /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + + // Same lease is offered twice + final DhcpLease newLease = mRepo.getOffer(CLIENTID_UNSPEC, macAddr, + INETADDR_UNSPEC /* 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, + INETADDR_UNSPEC, INETADDR_UNSPEC, HOSTNAME_NONE); + assertTrue(newPrefix.contains(lease.getNetAddr())); + } + + @Test + public void testGetOffer_ExistingLease() throws Exception { + mRepo.requestLease( + CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, TEST_INETADDR_1, false, TEST_HOSTNAME_1); + + DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, + INETADDR_UNSPEC, INETADDR_UNSPEC, 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, TEST_INETADDR_1, false, + TEST_HOSTNAME_1); + + // Different MAC, but same clientId + DhcpLease offer = mRepo.getOffer(clientId, TEST_MAC_2, + INETADDR_UNSPEC, INETADDR_UNSPEC, 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, TEST_INETADDR_1, false, + TEST_HOSTNAME_1); + + // Same MAC, different client ID + DhcpLease offer = mRepo.getOffer(clientId2, TEST_MAC_1, + INETADDR_UNSPEC, INETADDR_UNSPEC, 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, + TEST_INETADDR_1, TEST_HOSTNAME_1); + assertEquals(TEST_INETADDR_1, offer.getNetAddr()); + assertEquals(TEST_HOSTNAME_1, offer.getHostname()); + } + + @Test + public void testGetOffer_RequestedAddressInUse() throws Exception { + mRepo.requestLease( + CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, TEST_INETADDR_1, false, HOSTNAME_NONE); + DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_2, INET4_ANY, + TEST_INETADDR_1, 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, + TEST_RESERVED_ADDR, 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, + invalidAddr, 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, + invalidAddr, HOSTNAME_NONE); + assertNotEquals(invalidAddr, offer.getNetAddr()); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testGetOffer_RelayInInvalidSubnet() throws Exception { + mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, + parseAddr4("192.168.254.2") /* relayAddr */, INETADDR_UNSPEC, HOSTNAME_NONE); + } + + @Test + public void testRequestLease_SelectingTwice() throws Exception { + DhcpLease lease1 = mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + TEST_INETADDR_1, true /* sidSet */, TEST_HOSTNAME_1); + + // Second request from same client for a different address + DhcpLease lease2 = mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + TEST_INETADDR_2, true /* sidSet */, 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 + DhcpLease lease3 = mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_2, INET4_ANY, + TEST_INETADDR_1, true /* sidSet */, HOSTNAME_NONE); + assertEquals(TEST_INETADDR_1, lease3.getNetAddr()); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_SelectingInvalid() throws Exception { + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + parseAddr4("192.168.254.5"), true /* sidSet */, HOSTNAME_NONE); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_SelectingInUse() throws Exception { + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + TEST_INETADDR_1, true /* sidSet */, HOSTNAME_NONE); + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_2, INET4_ANY, + TEST_INETADDR_1, true /* sidSet */, HOSTNAME_NONE); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_SelectingReserved() throws Exception { + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + TEST_RESERVED_ADDR, true /* sidSet */, HOSTNAME_NONE); + } + + @Test + public void testRequestLease_InitReboot() throws Exception { + // Request address once + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + TEST_INETADDR_1, true /* sidSet */, HOSTNAME_NONE); + + final long newTime = TEST_TIME + 100; + when(mClock.elapsedRealtime()).thenReturn(newTime); + + // init-reboot (sidSet == false): verify configuration + DhcpLease lease = mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + TEST_INETADDR_1, false, HOSTNAME_NONE); + 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 + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + TEST_INETADDR_1, true /* sidSet */, HOSTNAME_NONE); + // init-reboot with different requested address + mRepo.requestLease( + CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, TEST_INETADDR_2, false, HOSTNAME_NONE); + } + + @Test + public void testRequestLease_InitRebootUnknownAddr() throws Exception { + // init-reboot with unknown requested address + DhcpLease lease = mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + TEST_INETADDR_2, false, HOSTNAME_NONE); + // 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 { + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + parseAddr4("192.168.254.2"), false /* sidSet */, HOSTNAME_NONE); + } + + @Test + public void testRequestLease_Renewing() throws Exception { + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, + INET4_ANY /* clientAddr */, TEST_INETADDR_1 /* reqAddr */, true, HOSTNAME_NONE); + + final long newTime = TEST_TIME + 100; + when(mClock.elapsedRealtime()).thenReturn(newTime); + + // Renewing: clientAddr filled in, no reqAddr + DhcpLease lease = mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, + TEST_INETADDR_1 /* clientAddr */, INETADDR_UNSPEC /* reqAddr */, false, + HOSTNAME_NONE); + + 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); + DhcpLease lease = mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, + TEST_INETADDR_1 /* clientAddr */, INETADDR_UNSPEC /* reqAddr */, false, + HOSTNAME_NONE); + // 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 { + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_2, + INET4_ANY /* clientAddr */, TEST_INETADDR_1 /* reqAddr */, true, HOSTNAME_NONE); + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, + TEST_INETADDR_1 /* clientAddr */, INETADDR_UNSPEC /* reqAddr */, false, + HOSTNAME_NONE); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_RenewingInvalidAddr() throws Exception { + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, parseAddr4("192.168.254.2") /* clientAddr */, + INETADDR_UNSPEC /* reqAddr */, false, HOSTNAME_NONE); + } + + @Test + public void testReleaseLease() throws Exception { + DhcpLease lease1 = mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + TEST_INETADDR_1, true /* sidSet */, HOSTNAME_NONE); + + assertHasLease(lease1); + assertTrue(mRepo.releaseLease(CLIENTID_UNSPEC, TEST_MAC_1, TEST_INETADDR_1)); + assertNoLease(lease1); + + DhcpLease lease2 = mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_2, INET4_ANY, + TEST_INETADDR_1, true /* sidSet */, HOSTNAME_NONE); + + 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, + INETADDR_UNSPEC /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + mRepo.requestLease( + CLIENTID_UNSPEC, mac, INET4_ANY, lease.getNetAddr(), true, + HOSTNAME_NONE); + mRepo.releaseLease(CLIENTID_UNSPEC, mac, lease.getNetAddr()); + + // Same lease is offered after it was released + final DhcpLease newLease = mRepo.getOffer(CLIENTID_UNSPEC, mac, + INETADDR_UNSPEC /* 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, + INETADDR_UNSPEC /* 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, + INETADDR_UNSPEC /* 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, + INETADDR_UNSPEC /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, TEST_HOSTNAME_1); + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, firstLease.getNetAddr(), true, + HOSTNAME_NONE); + + final DhcpLease secondLease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_2, + INETADDR_UNSPEC /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, TEST_HOSTNAME_2); + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_2, INET4_ANY, secondLease.getNetAddr(), true, + HOSTNAME_NONE); + + // Now out of addresses + try { + mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_3, INETADDR_UNSPEC /* 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 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)); + } +}