• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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