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