1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.net.dhcp; 18 19 import static android.net.dhcp.DhcpLease.EXPIRATION_NEVER; 20 import static android.net.dhcp.DhcpLease.inet4AddrToString; 21 22 import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH; 23 import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH; 24 import static com.android.net.module.util.Inet4AddressUtils.prefixLengthToV4NetmaskIntHTH; 25 import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY; 26 import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_BITS; 27 28 import static java.lang.Math.min; 29 30 import android.net.IpPrefix; 31 import android.net.MacAddress; 32 import android.net.dhcp.DhcpServer.Clock; 33 import android.os.RemoteCallbackList; 34 import android.os.RemoteException; 35 import android.util.ArrayMap; 36 37 import androidx.annotation.NonNull; 38 import androidx.annotation.Nullable; 39 import androidx.annotation.VisibleForTesting; 40 41 import com.android.net.module.util.SharedLog; 42 43 import java.net.Inet4Address; 44 import java.util.ArrayList; 45 import java.util.Collections; 46 import java.util.HashSet; 47 import java.util.Iterator; 48 import java.util.LinkedHashMap; 49 import java.util.List; 50 import java.util.Map; 51 import java.util.Map.Entry; 52 import java.util.Objects; 53 import java.util.Set; 54 import java.util.function.Function; 55 56 /** 57 * A repository managing IPv4 address assignments through DHCPv4. 58 * 59 * <p>This class is not thread-safe. All public methods should be called on a common thread or 60 * use some synchronization mechanism. 61 * 62 * <p>Methods are optimized for a small number of allocated leases, assuming that most of the time 63 * only 2~10 addresses will be allocated, which is the common case. Managing a large number of 64 * addresses is supported but will be slower: some operations have complexity in O(num_leases). 65 * @hide 66 */ 67 class DhcpLeaseRepository { 68 public static final byte[] CLIENTID_UNSPEC = null; 69 public static final Inet4Address INETADDR_UNSPEC = null; 70 71 @NonNull 72 private final SharedLog mLog; 73 @NonNull 74 private final Clock mClock; 75 76 @NonNull 77 private IpPrefix mPrefix; 78 @NonNull 79 private Set<Inet4Address> mReservedAddrs; 80 private int mLeasesSubnetAddr; 81 private int mPrefixLength; 82 private int mLeasesSubnetMask; 83 private int mNumAddresses; 84 private long mLeaseTimeMs; 85 @Nullable 86 private Inet4Address mClientAddr; 87 88 /** 89 * Next timestamp when committed or declined leases should be checked for expired ones. This 90 * will always be lower than or equal to the time for the first lease to expire: it's OK not to 91 * update this when removing entries, but it must always be updated when adding/updating. 92 */ 93 private long mNextExpirationCheck = EXPIRATION_NEVER; 94 95 @NonNull 96 private RemoteCallbackList<IDhcpEventCallbacks> mEventCallbacks = new RemoteCallbackList<>(); 97 98 static class DhcpLeaseException extends Exception { DhcpLeaseException(String message)99 DhcpLeaseException(String message) { 100 super(message); 101 } 102 } 103 104 static class OutOfAddressesException extends DhcpLeaseException { OutOfAddressesException(String message)105 OutOfAddressesException(String message) { 106 super(message); 107 } 108 } 109 110 static class InvalidAddressException extends DhcpLeaseException { InvalidAddressException(String message)111 InvalidAddressException(String message) { 112 super(message); 113 } 114 } 115 116 static class InvalidSubnetException extends DhcpLeaseException { InvalidSubnetException(String message)117 InvalidSubnetException(String message) { 118 super(message); 119 } 120 } 121 122 /** 123 * Leases by IP address 124 */ 125 private final ArrayMap<Inet4Address, DhcpLease> mCommittedLeases = new ArrayMap<>(); 126 127 /** 128 * Map address -> expiration timestamp in ms. Addresses are guaranteed to be valid as defined 129 * by {@link #isValidAddress(Inet4Address)}, but are not necessarily otherwise available for 130 * assignment. 131 */ 132 private final LinkedHashMap<Inet4Address, Long> mDeclinedAddrs = new LinkedHashMap<>(); 133 DhcpLeaseRepository(@onNull IpPrefix prefix, @NonNull Set<Inet4Address> reservedAddrs, long leaseTimeMs, @Nullable Inet4Address clientAddr, int leasesSubnetPrefixLength, @NonNull SharedLog log, @NonNull Clock clock)134 DhcpLeaseRepository(@NonNull IpPrefix prefix, @NonNull Set<Inet4Address> reservedAddrs, 135 long leaseTimeMs, @Nullable Inet4Address clientAddr, int leasesSubnetPrefixLength, 136 @NonNull SharedLog log, @NonNull Clock clock) { 137 mLog = log; 138 mClock = clock; 139 mClientAddr = clientAddr; 140 updateParams(prefix, reservedAddrs, leaseTimeMs, clientAddr, leasesSubnetPrefixLength); 141 } 142 updateParams(@onNull IpPrefix prefix, @NonNull Set<Inet4Address> reservedAddrs, long leaseTimeMs, @Nullable Inet4Address clientAddr, int leasesSubnetPrefixLength)143 public void updateParams(@NonNull IpPrefix prefix, @NonNull Set<Inet4Address> reservedAddrs, 144 long leaseTimeMs, @Nullable Inet4Address clientAddr, int leasesSubnetPrefixLength) { 145 mPrefix = prefix; 146 mReservedAddrs = Collections.unmodifiableSet(new HashSet<>(reservedAddrs)); 147 mPrefixLength = prefix.getPrefixLength(); 148 int subnetPrefixLength = mPrefixLength > leasesSubnetPrefixLength 149 ? mPrefixLength : leasesSubnetPrefixLength; 150 mLeasesSubnetMask = prefixLengthToV4NetmaskIntHTH(subnetPrefixLength); 151 mLeasesSubnetAddr = 152 inet4AddressToIntHTH((Inet4Address) prefix.getAddress()) & mLeasesSubnetMask; 153 mNumAddresses = clientAddr != null ? 1 : 1 << (IPV4_ADDR_BITS - subnetPrefixLength); 154 mLeaseTimeMs = leaseTimeMs; 155 mClientAddr = clientAddr; 156 157 cleanMap(mDeclinedAddrs); 158 if (cleanMap(mCommittedLeases)) { 159 notifyLeasesChanged(); 160 } 161 } 162 163 /** 164 * From a map keyed by {@link Inet4Address}, remove entries where the key is invalid (as 165 * specified by {@link #isValidAddress(Inet4Address)}), or is a reserved address. 166 * @return true if and only if at least one entry was removed. 167 */ cleanMap(Map<Inet4Address, T> map)168 private <T> boolean cleanMap(Map<Inet4Address, T> map) { 169 final Iterator<Entry<Inet4Address, T>> it = map.entrySet().iterator(); 170 boolean removed = false; 171 while (it.hasNext()) { 172 final Inet4Address addr = it.next().getKey(); 173 if (!isValidAddress(addr) || mReservedAddrs.contains(addr)) { 174 it.remove(); 175 removed = true; 176 } 177 } 178 return removed; 179 } 180 181 /** 182 * Get a DHCP offer, to reply to a DHCPDISCOVER. Follows RFC2131 #4.3.1. 183 * 184 * @param clientId Client identifier option if specified, or {@link #CLIENTID_UNSPEC} 185 * @param relayAddr Internet address of the relay (giaddr), can be {@link Inet4Address#ANY} 186 * @param reqAddr Requested address by the client (option 50), or {@link #INETADDR_UNSPEC} 187 * @param hostname Client-provided hostname, or {@link DhcpLease#HOSTNAME_NONE} 188 * @throws OutOfAddressesException The server does not have any available address 189 * @throws InvalidSubnetException The lease was requested from an unsupported subnet 190 */ 191 @NonNull getOffer(@ullable byte[] clientId, @NonNull MacAddress hwAddr, @NonNull Inet4Address relayAddr, @Nullable Inet4Address reqAddr, @Nullable String hostname)192 public DhcpLease getOffer(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, 193 @NonNull Inet4Address relayAddr, @Nullable Inet4Address reqAddr, 194 @Nullable String hostname) throws OutOfAddressesException, InvalidSubnetException { 195 final long currentTime = mClock.elapsedRealtime(); 196 final long expTime = currentTime + mLeaseTimeMs; 197 198 removeExpiredLeases(currentTime); 199 checkValidRelayAddr(relayAddr); 200 201 final DhcpLease currentLease = findByClient(clientId, hwAddr); 202 final DhcpLease newLease; 203 if (currentLease != null) { 204 newLease = currentLease.renewedLease(expTime, hostname); 205 mLog.log("Offering extended lease " + newLease); 206 // Do not update lease time in the map: the offer is not committed yet. 207 } else if (reqAddr != null && isValidAddress(reqAddr) && isAvailable(reqAddr)) { 208 newLease = new DhcpLease(clientId, hwAddr, reqAddr, mPrefixLength, expTime, hostname); 209 mLog.log("Offering requested lease " + newLease); 210 } else { 211 newLease = makeNewOffer(clientId, hwAddr, expTime, hostname); 212 mLog.log("Offering new generated lease " + newLease); 213 } 214 return newLease; 215 } 216 217 /** 218 * Get a rapid committed DHCP Lease, to reply to a DHCPDISCOVER w/ Rapid Commit option. 219 * 220 * @param clientId Client identifier option if specified, or {@link #CLIENTID_UNSPEC} 221 * @param relayAddr Internet address of the relay (giaddr), can be {@link Inet4Address#ANY} 222 * @param hostname Client-provided hostname, or {@link DhcpLease#HOSTNAME_NONE} 223 * @throws OutOfAddressesException The server does not have any available address 224 * @throws InvalidSubnetException The lease was requested from an unsupported subnet 225 */ 226 @NonNull getCommittedLease(@ullable byte[] clientId, @NonNull MacAddress hwAddr, @NonNull Inet4Address relayAddr, @Nullable String hostname)227 public DhcpLease getCommittedLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, 228 @NonNull Inet4Address relayAddr, @Nullable String hostname) 229 throws OutOfAddressesException, InvalidSubnetException { 230 final DhcpLease newLease = getOffer(clientId, hwAddr, relayAddr, null /* reqAddr */, 231 hostname); 232 commitLease(newLease); 233 return newLease; 234 } 235 checkValidRelayAddr(@ullable Inet4Address relayAddr)236 private void checkValidRelayAddr(@Nullable Inet4Address relayAddr) 237 throws InvalidSubnetException { 238 // As per #4.3.1, addresses are assigned based on the relay address if present. This 239 // implementation only assigns addresses if the relayAddr is inside our configured subnet. 240 // This also applies when the client requested a specific address for consistency between 241 // requests, and with older behavior. 242 if (isIpAddrOutsidePrefix(mPrefix, relayAddr)) { 243 throw new InvalidSubnetException("Lease requested by relay from outside of subnet"); 244 } 245 } 246 isIpAddrOutsidePrefix(@onNull IpPrefix prefix, @Nullable Inet4Address addr)247 private static boolean isIpAddrOutsidePrefix(@NonNull IpPrefix prefix, 248 @Nullable Inet4Address addr) { 249 return addr != null && !addr.equals(IPV4_ADDR_ANY) && !prefix.contains(addr); 250 } 251 252 @Nullable findByClient(@ullable byte[] clientId, @NonNull MacAddress hwAddr)253 private DhcpLease findByClient(@Nullable byte[] clientId, @NonNull MacAddress hwAddr) { 254 for (DhcpLease lease : mCommittedLeases.values()) { 255 if (lease.matchesClient(clientId, hwAddr)) { 256 return lease; 257 } 258 } 259 260 // Note this differs from dnsmasq behavior, which would match by hwAddr if clientId was 261 // given but no lease keyed on clientId matched. This would prevent one interface from 262 // obtaining multiple leases with different clientId. 263 return null; 264 } 265 266 /** 267 * Make a lease conformant to a client DHCPREQUEST or renew the client's existing lease, 268 * commit it to the repository and return it. 269 * 270 * <p>This method always succeeds and commits the lease if it does not throw, and has no side 271 * effects if it throws. 272 * 273 * @param clientId Client identifier option if specified, or {@link #CLIENTID_UNSPEC} 274 * @param reqAddr Requested address by the client (option 50), or {@link #INETADDR_UNSPEC} 275 * @param sidSet Whether the server identifier was set in the request 276 * @return The newly created or renewed lease 277 * @throws InvalidAddressException The client provided an address that conflicts with its 278 * current configuration, or other committed/reserved leases. 279 */ 280 @NonNull requestLease(@ullable byte[] clientId, @NonNull MacAddress hwAddr, @NonNull Inet4Address clientAddr, @NonNull Inet4Address relayAddr, @Nullable Inet4Address reqAddr, boolean sidSet, @Nullable String hostname)281 public DhcpLease requestLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, 282 @NonNull Inet4Address clientAddr, @NonNull Inet4Address relayAddr, 283 @Nullable Inet4Address reqAddr, boolean sidSet, @Nullable String hostname) 284 throws InvalidAddressException, InvalidSubnetException { 285 final long currentTime = mClock.elapsedRealtime(); 286 removeExpiredLeases(currentTime); 287 checkValidRelayAddr(relayAddr); 288 final DhcpLease assignedLease = findByClient(clientId, hwAddr); 289 290 final Inet4Address leaseAddr = reqAddr != null ? reqAddr : clientAddr; 291 if (assignedLease != null) { 292 if (sidSet && reqAddr != null) { 293 // Client in SELECTING state; remove any current lease before creating a new one. 294 // Do not notify of change as it will be done when the new lease is committed. 295 removeLease(assignedLease.getNetAddr(), false /* notifyChange */); 296 } else if (!assignedLease.getNetAddr().equals(leaseAddr)) { 297 // reqAddr null (RENEWING/REBINDING): client renewing its own lease for clientAddr. 298 // reqAddr set with sid not set (INIT-REBOOT): client verifying configuration. 299 // In both cases, throw if clientAddr or reqAddr does not match the known lease. 300 throw new InvalidAddressException("Incorrect address for client in " 301 + (reqAddr != null ? "INIT-REBOOT" : "RENEWING/REBINDING")); 302 } 303 } 304 305 // In the init-reboot case, RFC2131 #4.3.2 says that the server must not reply if 306 // assignedLease == null, but dnsmasq will let the client use the requested address if 307 // available, when configured with --dhcp-authoritative. This is preferable to avoid issues 308 // if the server lost the lease DB: the client would not get a reply because the server 309 // does not know their lease. 310 // Similarly in RENEWING/REBINDING state, create a lease when possible if the 311 // client-provided lease is unknown. 312 final DhcpLease lease = 313 checkClientAndMakeLease(clientId, hwAddr, leaseAddr, hostname, currentTime); 314 mLog.logf("DHCPREQUEST assignedLease %s, reqAddr=%s, sidSet=%s: created/renewed lease %s", 315 assignedLease, inet4AddrToString(reqAddr), sidSet, lease); 316 return lease; 317 } 318 319 /** 320 * Check that the client can request the specified address, make or renew the lease if yes, and 321 * commit it. 322 * 323 * <p>This method always succeeds and returns the lease if it does not throw, and has no 324 * side-effect if it throws. 325 * 326 * @return The newly created or renewed, committed lease 327 * @throws InvalidAddressException The client provided an address that conflicts with its 328 * current configuration, or other committed/reserved leases. 329 */ checkClientAndMakeLease(@ullable byte[] clientId, @NonNull MacAddress hwAddr, @NonNull Inet4Address addr, @Nullable String hostname, long currentTime)330 private DhcpLease checkClientAndMakeLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, 331 @NonNull Inet4Address addr, @Nullable String hostname, long currentTime) 332 throws InvalidAddressException { 333 final long expTime = currentTime + mLeaseTimeMs; 334 final DhcpLease currentLease = mCommittedLeases.getOrDefault(addr, null); 335 if (currentLease != null && !currentLease.matchesClient(clientId, hwAddr)) { 336 throw new InvalidAddressException("Address in use"); 337 } 338 339 final DhcpLease lease; 340 if (currentLease == null) { 341 if (isValidAddress(addr) && !mReservedAddrs.contains(addr)) { 342 lease = new DhcpLease(clientId, hwAddr, addr, mPrefixLength, expTime, hostname); 343 } else { 344 throw new InvalidAddressException("Lease not found and address unavailable"); 345 } 346 } else { 347 lease = currentLease.renewedLease(expTime, hostname); 348 } 349 commitLease(lease); 350 return lease; 351 } 352 commitLease(@onNull DhcpLease lease)353 private void commitLease(@NonNull DhcpLease lease) { 354 mCommittedLeases.put(lease.getNetAddr(), lease); 355 maybeUpdateEarliestExpiration(lease.getExpTime()); 356 notifyLeasesChanged(); 357 } 358 removeLease(@onNull Inet4Address address, boolean notifyChange)359 private void removeLease(@NonNull Inet4Address address, boolean notifyChange) { 360 // Earliest expiration remains <= the first expiry time on remove, so no need to update it. 361 mCommittedLeases.remove(address); 362 if (notifyChange) notifyLeasesChanged(); 363 } 364 365 /** 366 * Delete a committed lease from the repository. 367 * 368 * @return true if a lease matching parameters was found. 369 */ releaseLease(@ullable byte[] clientId, @NonNull MacAddress hwAddr, @NonNull Inet4Address addr)370 public boolean releaseLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, 371 @NonNull Inet4Address addr) { 372 final DhcpLease currentLease = mCommittedLeases.getOrDefault(addr, null); 373 if (currentLease == null) { 374 mLog.w("Could not release unknown lease for " + inet4AddrToString(addr)); 375 return false; 376 } 377 if (currentLease.matchesClient(clientId, hwAddr)) { 378 mLog.log("Released lease " + currentLease); 379 removeLease(addr, true /* notifyChange */); 380 return true; 381 } 382 mLog.w(String.format("Not releasing lease %s: does not match client (cid %s, hwAddr %s)", 383 currentLease, DhcpLease.clientIdToString(clientId), hwAddr)); 384 return false; 385 } 386 notifyLeasesChanged()387 private void notifyLeasesChanged() { 388 final List<DhcpLeaseParcelable> leaseParcelables = 389 new ArrayList<>(mCommittedLeases.size()); 390 for (DhcpLease committedLease : mCommittedLeases.values()) { 391 leaseParcelables.add(committedLease.toParcelable()); 392 } 393 394 final int cbCount = mEventCallbacks.beginBroadcast(); 395 for (int i = 0; i < cbCount; i++) { 396 try { 397 mEventCallbacks.getBroadcastItem(i).onLeasesChanged(leaseParcelables); 398 } catch (RemoteException e) { 399 mLog.e("Could not send lease callback", e); 400 } 401 } 402 mEventCallbacks.finishBroadcast(); 403 } 404 405 @VisibleForTesting markLeaseDeclined(@onNull Inet4Address addr)406 void markLeaseDeclined(@NonNull Inet4Address addr) { 407 if (mDeclinedAddrs.containsKey(addr) || !isValidAddress(addr)) { 408 mLog.logf("Not marking %s as declined: already declined or not assignable", 409 inet4AddrToString(addr)); 410 return; 411 } 412 final long expTime = mClock.elapsedRealtime() + mLeaseTimeMs; 413 mDeclinedAddrs.put(addr, expTime); 414 mLog.logf("Marked %s as declined expiring %d", inet4AddrToString(addr), expTime); 415 maybeUpdateEarliestExpiration(expTime); 416 } 417 418 /** 419 * Mark a committed lease matching the passed in clientId and hardware address parameters to be 420 * declined, and delete it from the repository. 421 * 422 * @param clientId Client identifier option if specified, or {@link #CLIENTID_UNSPEC} 423 * @param hwAddr client's mac address 424 * @param Addr IPv4 address to be declined 425 * @return true if a lease matching parameters was removed from committed repository. 426 */ markAndReleaseDeclinedLease(@ullable byte[] clientId, @NonNull MacAddress hwAddr, @NonNull Inet4Address addr)427 public boolean markAndReleaseDeclinedLease(@Nullable byte[] clientId, 428 @NonNull MacAddress hwAddr, @NonNull Inet4Address addr) { 429 if (!releaseLease(clientId, hwAddr, addr)) return false; 430 markLeaseDeclined(addr); 431 return true; 432 } 433 434 /** 435 * Get the list of currently valid committed leases in the repository. 436 */ 437 @NonNull getCommittedLeases()438 public List<DhcpLease> getCommittedLeases() { 439 removeExpiredLeases(mClock.elapsedRealtime()); 440 return new ArrayList<>(mCommittedLeases.values()); 441 } 442 443 /** 444 * Get the set of addresses that have been marked as declined in the repository. 445 */ 446 @NonNull getDeclinedAddresses()447 public Set<Inet4Address> getDeclinedAddresses() { 448 removeExpiredLeases(mClock.elapsedRealtime()); 449 return new HashSet<>(mDeclinedAddrs.keySet()); 450 } 451 452 /** 453 * Add callbacks that will be called on leases update. 454 */ addLeaseCallbacks(@onNull IDhcpEventCallbacks cb)455 public void addLeaseCallbacks(@NonNull IDhcpEventCallbacks cb) { 456 Objects.requireNonNull(cb, "Callbacks must be non-null"); 457 mEventCallbacks.register(cb); 458 } 459 460 /** 461 * Given the expiration time of a new committed lease or declined address, update 462 * {@link #mNextExpirationCheck} so it stays lower than or equal to the time for the first lease 463 * to expire. 464 */ maybeUpdateEarliestExpiration(long expTime)465 private void maybeUpdateEarliestExpiration(long expTime) { 466 if (expTime < mNextExpirationCheck) { 467 mNextExpirationCheck = expTime; 468 } 469 } 470 471 /** 472 * Remove expired entries from a map keyed by {@link Inet4Address}. 473 * 474 * @param tag Type of lease in the map, for logging 475 * @param getExpTime Functor returning the expiration time for an object in the map. 476 * Must not return null. 477 * @return The lowest expiration time among entries remaining in the map 478 */ removeExpired(long currentTime, @NonNull Map<Inet4Address, T> map, @NonNull String tag, @NonNull Function<T, Long> getExpTime)479 private <T> long removeExpired(long currentTime, @NonNull Map<Inet4Address, T> map, 480 @NonNull String tag, @NonNull Function<T, Long> getExpTime) { 481 final Iterator<Entry<Inet4Address, T>> it = map.entrySet().iterator(); 482 long firstExpiration = EXPIRATION_NEVER; 483 while (it.hasNext()) { 484 final Entry<Inet4Address, T> lease = it.next(); 485 final long expTime = getExpTime.apply(lease.getValue()); 486 if (expTime <= currentTime) { 487 mLog.logf("Removing expired %s lease for %s (expTime=%s, currentTime=%s)", 488 tag, lease.getKey(), expTime, currentTime); 489 it.remove(); 490 } else { 491 firstExpiration = min(firstExpiration, expTime); 492 } 493 } 494 return firstExpiration; 495 } 496 497 /** 498 * Go through committed and declined leases and remove the expired ones. 499 */ removeExpiredLeases(long currentTime)500 private void removeExpiredLeases(long currentTime) { 501 if (currentTime < mNextExpirationCheck) { 502 return; 503 } 504 505 final long commExp = removeExpired( 506 currentTime, mCommittedLeases, "committed", DhcpLease::getExpTime); 507 final long declExp = removeExpired( 508 currentTime, mDeclinedAddrs, "declined", Function.identity()); 509 510 mNextExpirationCheck = min(commExp, declExp); 511 } 512 isAvailable(@onNull Inet4Address addr)513 private boolean isAvailable(@NonNull Inet4Address addr) { 514 return !mReservedAddrs.contains(addr) && !mCommittedLeases.containsKey(addr); 515 } 516 517 /** 518 * Get the 0-based index of an address in the subnet. 519 * 520 * <p>Given ordering of addresses 5.6.7.8 < 5.6.7.9 < 5.6.8.0, the index on a subnet is defined 521 * so that the first address is 0, the second 1, etc. For example on a /16, 192.168.0.0 -> 0, 522 * 192.168.0.1 -> 1, 192.168.1.0 -> 256 523 * 524 */ getAddrIndex(int addr)525 private int getAddrIndex(int addr) { 526 return addr & ~mLeasesSubnetMask; 527 } 528 getAddrByIndex(int index)529 private int getAddrByIndex(int index) { 530 return mLeasesSubnetAddr | index; 531 } 532 533 /** 534 * Get a valid address starting from the supplied one. 535 * 536 * <p>This only checks that the address is numerically valid for assignment, not whether it is 537 * already in use. The return value is always inside the configured prefix, even if the supplied 538 * address is not. 539 * 540 * <p>If the provided address is valid, it is returned as-is. Otherwise, the next valid 541 * address (with the ordering in {@link #getAddrIndex(int)}) is returned. 542 */ getValidAddress(int addr)543 private int getValidAddress(int addr) { 544 // Only mClientAddr is valid if static client address is enforced. 545 if (mClientAddr != null) return inet4AddressToIntHTH(mClientAddr); 546 547 final int lastByteMask = 0xff; 548 int addrIndex = getAddrIndex(addr); // 0-based index of the address in the subnet 549 550 // Some OSes do not handle addresses in .255 or .0 correctly: avoid those. 551 final int lastByte = getAddrByIndex(addrIndex) & lastByteMask; 552 if (lastByte == lastByteMask) { 553 // Avoid .255 address, and .0 address that follows 554 addrIndex = (addrIndex + 2) % mNumAddresses; 555 } else if (lastByte == 0) { 556 // Avoid .0 address 557 addrIndex = (addrIndex + 1) % mNumAddresses; 558 } 559 560 // Do not use first or last address of range 561 if (addrIndex == 0 || addrIndex == mNumAddresses - 1) { 562 // Always valid and not end of range since prefixLength is at most 30 in serving params 563 addrIndex = 1; 564 } 565 return getAddrByIndex(addrIndex); 566 } 567 568 /** 569 * Returns whether the address is in the configured subnet and part of the assignable range. 570 */ isValidAddress(Inet4Address addr)571 private boolean isValidAddress(Inet4Address addr) { 572 final int intAddr = inet4AddressToIntHTH(addr); 573 return getValidAddress(intAddr) == intAddr; 574 } 575 getNextAddress(int addr)576 private int getNextAddress(int addr) { 577 final int addrIndex = getAddrIndex(addr); 578 final int nextAddress = getAddrByIndex((addrIndex + 1) % mNumAddresses); 579 return getValidAddress(nextAddress); 580 } 581 582 /** 583 * Calculate a first candidate address for a client by hashing the hardware address. 584 * 585 * <p>This will be a valid address as checked by {@link #getValidAddress(int)}, but may be 586 * in use. 587 * 588 * @return An IPv4 address encoded as 32-bit int 589 */ getFirstClientAddress(MacAddress hwAddr)590 private int getFirstClientAddress(MacAddress hwAddr) { 591 // This follows dnsmasq behavior. Advantages are: clients will often get the same 592 // offers for different DISCOVER even if the lease was not yet accepted or has expired, 593 // and address generation will generally not need to loop through many allocated addresses 594 // until it finds a free one. 595 int hash = 0; 596 for (byte b : hwAddr.toByteArray()) { 597 hash += b + (b << 8) + (b << 16); 598 } 599 // This implementation will not always result in the same IPs as dnsmasq would give out in 600 // Android <= P, because it includes invalid and reserved addresses in mNumAddresses while 601 // the configured ranges for dnsmasq did not. 602 final int addrIndex = hash % mNumAddresses; 603 return getValidAddress(getAddrByIndex(addrIndex)); 604 } 605 606 /** 607 * Create a lease that can be offered to respond to a client DISCOVER. 608 * 609 * <p>This method always succeeds and returns the lease if it does not throw. If no non-declined 610 * address is available, it will try to offer the oldest declined address if valid. 611 * 612 * @throws OutOfAddressesException The server has no address left to offer 613 */ makeNewOffer(@ullable byte[] clientId, @NonNull MacAddress hwAddr, long expTime, @Nullable String hostname)614 private DhcpLease makeNewOffer(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, 615 long expTime, @Nullable String hostname) throws OutOfAddressesException { 616 int intAddr = getFirstClientAddress(hwAddr); 617 // Loop until a free address is found, or there are no more addresses. 618 // There is slightly less than this many usable addresses, but some extra looping is OK 619 for (int i = 0; i < mNumAddresses; i++) { 620 final Inet4Address addr = intToInet4AddressHTH(intAddr); 621 if (isAvailable(addr) && !mDeclinedAddrs.containsKey(addr)) { 622 return new DhcpLease(clientId, hwAddr, addr, mPrefixLength, expTime, hostname); 623 } 624 intAddr = getNextAddress(intAddr); 625 } 626 627 // Try freeing DECLINEd addresses if out of addresses. 628 final Iterator<Inet4Address> it = mDeclinedAddrs.keySet().iterator(); 629 while (it.hasNext()) { 630 final Inet4Address addr = it.next(); 631 it.remove(); 632 mLog.logf("Out of addresses in address pool: dropped declined addr %s", 633 inet4AddrToString(addr)); 634 // isValidAddress() is always verified for entries in mDeclinedAddrs. 635 // However declined addresses may have been requested (typically by the machine that was 636 // already using the address) after being declined. 637 if (isAvailable(addr)) { 638 return new DhcpLease(clientId, hwAddr, addr, mPrefixLength, expTime, hostname); 639 } 640 } 641 642 throw new OutOfAddressesException("No address available for offer"); 643 } 644 } 645