Add DhcpLeaseRepository

This is a first component to build the new DHCP server.

Test: added tests pass
Bug: b/109584964
Change-Id: I5657d89c3010a23e9289ac827bf78381477d1355
This commit is contained in:
Remi NGUYEN VAN
2018-06-27 17:20:36 +09:00
parent 5831d309fa
commit eca5b4e753
3 changed files with 1195 additions and 0 deletions

View File

@@ -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.
*
* <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));
}
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);
}
}

View File

@@ -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.
*
* <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;
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<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<>();
public 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 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.
*
* <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, @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.
*
* <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 " + 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<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 = (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<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", 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");
}
}

View File

@@ -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<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,
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<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,
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));
}
}