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