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