• 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.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