/* * 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.EXPIRATION_NEVER; import static android.net.dhcp.DhcpLease.inet4AddrToString; import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH; import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH; import static com.android.net.module.util.Inet4AddressUtils.prefixLengthToV4NetmaskIntHTH; import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY; import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_BITS; import static java.lang.Math.min; import android.net.IpPrefix; import android.net.MacAddress; import android.net.dhcp.DhcpServer.Clock; import android.net.util.SharedLog; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.util.ArrayMap; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; 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.Objects; 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 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.
// Do not notify of change as it will be done when the new lease is committed.
removeLease(assignedLease.getNetAddr(), false /* notifyChange */);
} 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.
*
* 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, mPrefixLength, 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());
notifyLeasesChanged();
}
private void removeLease(@NonNull Inet4Address address, boolean notifyChange) {
// Earliest expiration remains <= the first expiry time on remove, so no need to update it.
mCommittedLeases.remove(address);
if (notifyChange) notifyLeasesChanged();
}
/**
* 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)) {
mLog.log("Released lease " + currentLease);
removeLease(addr, true /* notifyChange */);
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;
}
private void notifyLeasesChanged() {
final List 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) {
// Only mClientAddr is valid if static client address is enforced.
if (mClientAddr != null) return inet4AddressToIntHTH(mClientAddr);
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 = intToInet4AddressHTH(intAddr);
if (isAvailable(addr) && !mDeclinedAddrs.containsKey(addr)) {
return new DhcpLease(clientId, hwAddr, addr, mPrefixLength, expTime, hostname);
}
intAddr = getNextAddress(intAddr);
}
// Try freeing DECLINEd addresses if out of addresses.
final Iterator