summaryrefslogtreecommitdiff
path: root/src/android/net/dhcp/DhcpLeaseRepository.java
blob: 0a15cd7d3bd969f6c85f423e6cadaab0896fc02b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.net.dhcp;

import static android.net.dhcp.DhcpLease.EXPIRATION_NEVER;
import static android.net.dhcp.DhcpLease.inet4AddrToString;
import static android.net.shared.Inet4AddressUtils.inet4AddressToIntHTH;
import static android.net.shared.Inet4AddressUtils.intToInet4AddressHTH;
import static android.net.shared.Inet4AddressUtils.prefixLengthToV4NetmaskIntHTH;

import static com.android.server.util.NetworkStackConstants.IPV4_ADDR_ANY;
import static com.android.server.util.NetworkStackConstants.IPV4_ADDR_BITS;

import static java.lang.Math.min;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.IpPrefix;
import android.net.MacAddress;
import android.net.dhcp.DhcpServer.Clock;
import android.net.util.SharedLog;
import android.util.ArrayMap;

import java.net.Inet4Address;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Function;

/**
 * A repository managing IPv4 address assignments through DHCPv4.
 *
 * <p>This class is not thread-safe. All public methods should be called on a common thread or
 * use some synchronization mechanism.
 *
 * <p>Methods are optimized for a small number of allocated leases, assuming that most of the time
 * only 2~10 addresses will be allocated, which is the common case. Managing a large number of
 * addresses is supported but will be slower: some operations have complexity in O(num_leases).
 * @hide
 */
class DhcpLeaseRepository {
    public static final byte[] CLIENTID_UNSPEC = null;
    public static final Inet4Address INETADDR_UNSPEC = null;

    @NonNull
    private final SharedLog mLog;
    @NonNull
    private final Clock mClock;

    @NonNull
    private IpPrefix mPrefix;
    @NonNull
    private Set<Inet4Address> mReservedAddrs;
    private int mSubnetAddr;
    private int mSubnetMask;
    private int mNumAddresses;
    private long mLeaseTimeMs;

    /**
     * Next timestamp when committed or declined leases should be checked for expired ones. This
     * will always be lower than or equal to the time for the first lease to expire: it's OK not to
     * update this when removing entries, but it must always be updated when adding/updating.
     */
    private long mNextExpirationCheck = EXPIRATION_NEVER;

    static class DhcpLeaseException extends Exception {
        DhcpLeaseException(String message) {
            super(message);
        }
    }

    static class OutOfAddressesException extends DhcpLeaseException {
        OutOfAddressesException(String message) {
            super(message);
        }
    }

    static class InvalidAddressException extends DhcpLeaseException {
        InvalidAddressException(String message) {
            super(message);
        }
    }

    static class InvalidSubnetException extends DhcpLeaseException {
        InvalidSubnetException(String message) {
            super(message);
        }
    }

    /**
     * Leases by IP address
     */
    private final ArrayMap<Inet4Address, DhcpLease> mCommittedLeases = new ArrayMap<>();

    /**
     * Map address -> expiration timestamp in ms. Addresses are guaranteed to be valid as defined
     * by {@link #isValidAddress(Inet4Address)}, but are not necessarily otherwise available for
     * assignment.
     */
    private final LinkedHashMap<Inet4Address, Long> mDeclinedAddrs = new LinkedHashMap<>();

    DhcpLeaseRepository(@NonNull IpPrefix prefix, @NonNull Set<Inet4Address> reservedAddrs,
            long leaseTimeMs, @NonNull SharedLog log, @NonNull Clock clock) {
        updateParams(prefix, reservedAddrs, leaseTimeMs);
        mLog = log;
        mClock = clock;
    }

    public void updateParams(@NonNull IpPrefix prefix, @NonNull Set<Inet4Address> reservedAddrs,
            long leaseTimeMs) {
        mPrefix = prefix;
        mReservedAddrs = Collections.unmodifiableSet(new HashSet<>(reservedAddrs));
        mSubnetMask = prefixLengthToV4NetmaskIntHTH(prefix.getPrefixLength());
        mSubnetAddr = inet4AddressToIntHTH((Inet4Address) prefix.getAddress()) & mSubnetMask;
        mNumAddresses = 1 << (IPV4_ADDR_BITS - prefix.getPrefixLength());
        mLeaseTimeMs = leaseTimeMs;

        cleanMap(mCommittedLeases);
        cleanMap(mDeclinedAddrs);
    }

    /**
     * From a map keyed by {@link Inet4Address}, remove entries where the key is invalid (as
     * specified by {@link #isValidAddress(Inet4Address)}), or is a reserved address.
     */
    private <T> void cleanMap(Map<Inet4Address, T> map) {
        final Iterator<Entry<Inet4Address, T>> it = map.entrySet().iterator();
        while (it.hasNext()) {
            final Inet4Address addr = it.next().getKey();
            if (!isValidAddress(addr) || mReservedAddrs.contains(addr)) {
                it.remove();
            }
        }
    }

    /**
     * Get a DHCP offer, to reply to a DHCPDISCOVER. Follows RFC2131 #4.3.1.
     *
     * @param clientId Client identifier option if specified, or {@link #CLIENTID_UNSPEC}
     * @param relayAddr Internet address of the relay (giaddr), can be {@link Inet4Address#ANY}
     * @param reqAddr Requested address by the client (option 50), or {@link #INETADDR_UNSPEC}
     * @param hostname Client-provided hostname, or {@link DhcpLease#HOSTNAME_NONE}
     * @throws OutOfAddressesException The server does not have any available address
     * @throws InvalidSubnetException The lease was requested from an unsupported subnet
     */
    @NonNull
    public DhcpLease getOffer(@Nullable byte[] clientId, @NonNull MacAddress hwAddr,
            @NonNull Inet4Address relayAddr, @Nullable Inet4Address reqAddr,
            @Nullable String hostname) throws OutOfAddressesException, InvalidSubnetException {
        final long currentTime = mClock.elapsedRealtime();
        final long expTime = currentTime + mLeaseTimeMs;

        removeExpiredLeases(currentTime);
        checkValidRelayAddr(relayAddr);

        final DhcpLease currentLease = findByClient(clientId, hwAddr);
        final DhcpLease newLease;
        if (currentLease != null) {
            newLease = currentLease.renewedLease(expTime, hostname);
            mLog.log("Offering extended lease " + newLease);
            // Do not update lease time in the map: the offer is not committed yet.
        } else if (reqAddr != null && isValidAddress(reqAddr) && isAvailable(reqAddr)) {
            newLease = new DhcpLease(clientId, hwAddr, reqAddr, expTime, hostname);
            mLog.log("Offering requested lease " + newLease);
        } else {
            newLease = makeNewOffer(clientId, hwAddr, expTime, hostname);
            mLog.log("Offering new generated lease " + newLease);
        }
        return newLease;
    }

    private void checkValidRelayAddr(@Nullable Inet4Address relayAddr)
            throws InvalidSubnetException {
        // As per #4.3.1, addresses are assigned based on the relay address if present. This
        // implementation only assigns addresses if the relayAddr is inside our configured subnet.
        // This also applies when the client requested a specific address for consistency between
        // requests, and with older behavior.
        if (isIpAddrOutsidePrefix(mPrefix, relayAddr)) {
            throw new InvalidSubnetException("Lease requested by relay from outside of subnet");
        }
    }

    private static boolean isIpAddrOutsidePrefix(@NonNull IpPrefix prefix,
            @Nullable Inet4Address addr) {
        return addr != null && !addr.equals(IPV4_ADDR_ANY) && !prefix.contains(addr);
    }

    @Nullable
    private DhcpLease findByClient(@Nullable byte[] clientId, @NonNull MacAddress hwAddr) {
        for (DhcpLease lease : mCommittedLeases.values()) {
            if (lease.matchesClient(clientId, hwAddr)) {
                return lease;
            }
        }

        // Note this differs from dnsmasq behavior, which would match by hwAddr if clientId was
        // given but no lease keyed on clientId matched. This would prevent one interface from
        // obtaining multiple leases with different clientId.
        return null;
    }

    /**
     * Make a lease conformant to a client DHCPREQUEST or renew the client's existing lease,
     * commit it to the repository and return it.
     *
     * <p>This method always succeeds and commits the lease if it does not throw, and has no side
     * effects if it throws.
     *
     * @param clientId Client identifier option if specified, or {@link #CLIENTID_UNSPEC}
     * @param reqAddr Requested address by the client (option 50), or {@link #INETADDR_UNSPEC}
     * @param sidSet Whether the server identifier was set in the request
     * @return The newly created or renewed lease
     * @throws InvalidAddressException The client provided an address that conflicts with its
     *                                 current configuration, or other committed/reserved leases.
     */
    @NonNull
    public DhcpLease requestLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr,
            @NonNull Inet4Address clientAddr, @NonNull Inet4Address relayAddr,
            @Nullable Inet4Address reqAddr, boolean sidSet, @Nullable String hostname)
            throws InvalidAddressException, InvalidSubnetException {
        final long currentTime = mClock.elapsedRealtime();
        removeExpiredLeases(currentTime);
        checkValidRelayAddr(relayAddr);
        final DhcpLease assignedLease = findByClient(clientId, hwAddr);

        final Inet4Address leaseAddr = reqAddr != null ? reqAddr : clientAddr;
        if (assignedLease != null) {
            if (sidSet && reqAddr != null) {
                // Client in SELECTING state; remove any current lease before creating a new one.
                mCommittedLeases.remove(assignedLease.getNetAddr());
            } else if (!assignedLease.getNetAddr().equals(leaseAddr)) {
                // reqAddr null (RENEWING/REBINDING): client renewing its own lease for clientAddr.
                // reqAddr set with sid not set (INIT-REBOOT): client verifying configuration.
                // In both cases, throw if clientAddr or reqAddr does not match the known lease.
                throw new InvalidAddressException("Incorrect address for client in "
                        + (reqAddr != null ? "INIT-REBOOT" : "RENEWING/REBINDING"));
            }
        }

        // In the init-reboot case, RFC2131 #4.3.2 says that the server must not reply if
        // assignedLease == null, but dnsmasq will let the client use the requested address if
        // available, when configured with --dhcp-authoritative. This is preferable to avoid issues
        // if the server lost the lease DB: the client would not get a reply because the server
        // does not know their lease.
        // Similarly in RENEWING/REBINDING state, create a lease when possible if the
        // client-provided lease is unknown.
        final DhcpLease lease =
                checkClientAndMakeLease(clientId, hwAddr, leaseAddr, hostname, currentTime);
        mLog.logf("DHCPREQUEST assignedLease %s, reqAddr=%s, sidSet=%s: created/renewed lease %s",
                assignedLease, inet4AddrToString(reqAddr), sidSet, lease);
        return lease;
    }

    /**
     * Check that the client can request the specified address, make or renew the lease if yes, and
     * commit it.
     *
     * <p>This method always succeeds and returns the lease if it does not throw, and has no
     * side-effect if it throws.
     *
     * @return The newly created or renewed, committed lease
     * @throws InvalidAddressException The client provided an address that conflicts with its
     *                                 current configuration, or other committed/reserved leases.
     */
    private DhcpLease checkClientAndMakeLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr,
            @NonNull Inet4Address addr, @Nullable String hostname, long currentTime)
            throws InvalidAddressException {
        final long expTime = currentTime + mLeaseTimeMs;
        final DhcpLease currentLease = mCommittedLeases.getOrDefault(addr, null);
        if (currentLease != null && !currentLease.matchesClient(clientId, hwAddr)) {
            throw new InvalidAddressException("Address in use");
        }

        final DhcpLease lease;
        if (currentLease == null) {
            if (isValidAddress(addr) && !mReservedAddrs.contains(addr)) {
                lease = new DhcpLease(clientId, hwAddr, addr, expTime, hostname);
            } else {
                throw new InvalidAddressException("Lease not found and address unavailable");
            }
        } else {
            lease = currentLease.renewedLease(expTime, hostname);
        }
        commitLease(lease);
        return lease;
    }

    private void commitLease(@NonNull DhcpLease lease) {
        mCommittedLeases.put(lease.getNetAddr(), lease);
        maybeUpdateEarliestExpiration(lease.getExpTime());
    }

    /**
     * Delete a committed lease from the repository.
     *
     * @return true if a lease matching parameters was found.
     */
    public boolean releaseLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr,
            @NonNull Inet4Address addr) {
        final DhcpLease currentLease = mCommittedLeases.getOrDefault(addr, null);
        if (currentLease == null) {
            mLog.w("Could not release unknown lease for " + inet4AddrToString(addr));
            return false;
        }
        if (currentLease.matchesClient(clientId, hwAddr)) {
            mCommittedLeases.remove(addr);
            mLog.log("Released lease " + currentLease);
            return true;
        }
        mLog.w(String.format("Not releasing lease %s: does not match client (cid %s, hwAddr %s)",
                currentLease, DhcpLease.clientIdToString(clientId), hwAddr));
        return false;
    }

    public void markLeaseDeclined(@NonNull Inet4Address addr) {
        if (mDeclinedAddrs.containsKey(addr) || !isValidAddress(addr)) {
            mLog.logf("Not marking %s as declined: already declined or not assignable",
                    inet4AddrToString(addr));
            return;
        }
        final long expTime = mClock.elapsedRealtime() + mLeaseTimeMs;
        mDeclinedAddrs.put(addr, expTime);
        mLog.logf("Marked %s as declined expiring %d", inet4AddrToString(addr), expTime);
        maybeUpdateEarliestExpiration(expTime);
    }

    /**
     * Get the list of currently valid committed leases in the repository.
     */
    @NonNull
    public List<DhcpLease> getCommittedLeases() {
        removeExpiredLeases(mClock.elapsedRealtime());
        return new ArrayList<>(mCommittedLeases.values());
    }

    /**
     * Get the set of addresses that have been marked as declined in the repository.
     */
    @NonNull
    public Set<Inet4Address> getDeclinedAddresses() {
        removeExpiredLeases(mClock.elapsedRealtime());
        return new HashSet<>(mDeclinedAddrs.keySet());
    }

    /**
     * Given the expiration time of a new committed lease or declined address, update
     * {@link #mNextExpirationCheck} so it stays lower than or equal to the time for the first lease
     * to expire.
     */
    private void maybeUpdateEarliestExpiration(long expTime) {
        if (expTime < mNextExpirationCheck) {
            mNextExpirationCheck = expTime;
        }
    }

    /**
     * Remove expired entries from a map keyed by {@link Inet4Address}.
     *
     * @param tag Type of lease in the map, for logging
     * @param getExpTime Functor returning the expiration time for an object in the map.
     *                   Must not return null.
     * @return The lowest expiration time among entries remaining in the map
     */
    private <T> long removeExpired(long currentTime, @NonNull Map<Inet4Address, T> map,
            @NonNull String tag, @NonNull Function<T, Long> getExpTime) {
        final Iterator<Entry<Inet4Address, T>> it = map.entrySet().iterator();
        long firstExpiration = EXPIRATION_NEVER;
        while (it.hasNext()) {
            final Entry<Inet4Address, T> lease = it.next();
            final long expTime = getExpTime.apply(lease.getValue());
            if (expTime <= currentTime) {
                mLog.logf("Removing expired %s lease for %s (expTime=%s, currentTime=%s)",
                        tag, lease.getKey(), expTime, currentTime);
                it.remove();
            } else {
                firstExpiration = min(firstExpiration, expTime);
            }
        }
        return firstExpiration;
    }

    /**
     * Go through committed and declined leases and remove the expired ones.
     */
    private void removeExpiredLeases(long currentTime) {
        if (currentTime < mNextExpirationCheck) {
            return;
        }

        final long commExp = removeExpired(
                currentTime, mCommittedLeases, "committed", DhcpLease::getExpTime);
        final long declExp = removeExpired(
                currentTime, mDeclinedAddrs, "declined", Function.identity());

        mNextExpirationCheck = min(commExp, declExp);
    }

    private boolean isAvailable(@NonNull Inet4Address addr) {
        return !mReservedAddrs.contains(addr) && !mCommittedLeases.containsKey(addr);
    }

    /**
     * Get the 0-based index of an address in the subnet.
     *
     * <p>Given ordering of addresses 5.6.7.8 < 5.6.7.9 < 5.6.8.0, the index on a subnet is defined
     * so that the first address is 0, the second 1, etc. For example on a /16, 192.168.0.0 -> 0,
     * 192.168.0.1 -> 1, 192.168.1.0 -> 256
     *
     */
    private int getAddrIndex(int addr) {
        return addr & ~mSubnetMask;
    }

    private int getAddrByIndex(int index) {
        return mSubnetAddr | index;
    }

    /**
     * Get a valid address starting from the supplied one.
     *
     * <p>This only checks that the address is numerically valid for assignment, not whether it is
     * already in use. The return value is always inside the configured prefix, even if the supplied
     * address is not.
     *
     * <p>If the provided address is valid, it is returned as-is. Otherwise, the next valid
     * address (with the ordering in {@link #getAddrIndex(int)}) is returned.
     */
    private int getValidAddress(int addr) {
        final int lastByteMask = 0xff;
        int addrIndex = getAddrIndex(addr); // 0-based index of the address in the subnet

        // Some OSes do not handle addresses in .255 or .0 correctly: avoid those.
        final int lastByte = getAddrByIndex(addrIndex) & lastByteMask;
        if (lastByte == lastByteMask) {
            // Avoid .255 address, and .0 address that follows
            addrIndex = (addrIndex + 2) % mNumAddresses;
        } else if (lastByte == 0) {
            // Avoid .0 address
            addrIndex = (addrIndex + 1) % mNumAddresses;
        }

        // Do not use first or last address of range
        if (addrIndex == 0 || addrIndex == mNumAddresses - 1) {
            // Always valid and not end of range since prefixLength is at most 30 in serving params
            addrIndex = 1;
        }
        return getAddrByIndex(addrIndex);
    }

    /**
     * Returns whether the address is in the configured subnet and part of the assignable range.
     */
    private boolean isValidAddress(Inet4Address addr) {
        final int intAddr = inet4AddressToIntHTH(addr);
        return getValidAddress(intAddr) == intAddr;
    }

    private int getNextAddress(int addr) {
        final int addrIndex = getAddrIndex(addr);
        final int nextAddress = getAddrByIndex((addrIndex + 1) % mNumAddresses);
        return getValidAddress(nextAddress);
    }

    /**
     * Calculate a first candidate address for a client by hashing the hardware address.
     *
     * <p>This will be a valid address as checked by {@link #getValidAddress(int)}, but may be
     * in use.
     *
     * @return An IPv4 address encoded as 32-bit int
     */
    private int getFirstClientAddress(MacAddress hwAddr) {
        // This follows dnsmasq behavior. Advantages are: clients will often get the same
        // offers for different DISCOVER even if the lease was not yet accepted or has expired,
        // and address generation will generally not need to loop through many allocated addresses
        // until it finds a free one.
        int hash = 0;
        for (byte b : hwAddr.toByteArray()) {
            hash += b + (b << 8) + (b << 16);
        }
        // This implementation will not always result in the same IPs as dnsmasq would give out in
        // Android <= P, because it includes invalid and reserved addresses in mNumAddresses while
        // the configured ranges for dnsmasq did not.
        final int addrIndex = hash % mNumAddresses;
        return getValidAddress(getAddrByIndex(addrIndex));
    }

    /**
     * Create a lease that can be offered to respond to a client DISCOVER.
     *
     * <p>This method always succeeds and returns the lease if it does not throw. If no non-declined
     * address is available, it will try to offer the oldest declined address if valid.
     *
     * @throws OutOfAddressesException The server has no address left to offer
     */
    private DhcpLease makeNewOffer(@Nullable byte[] clientId, @NonNull MacAddress hwAddr,
            long expTime, @Nullable String hostname) throws OutOfAddressesException {
        int intAddr = getFirstClientAddress(hwAddr);
        // Loop until a free address is found, or there are no more addresses.
        // There is slightly less than this many usable addresses, but some extra looping is OK
        for (int i = 0; i < mNumAddresses; i++) {
            final Inet4Address addr = intToInet4AddressHTH(intAddr);
            if (isAvailable(addr) && !mDeclinedAddrs.containsKey(addr)) {
                return new DhcpLease(clientId, hwAddr, addr, expTime, hostname);
            }
            intAddr = getNextAddress(intAddr);
        }

        // Try freeing DECLINEd addresses if out of addresses.
        final Iterator<Inet4Address> it = mDeclinedAddrs.keySet().iterator();
        while (it.hasNext()) {
            final Inet4Address addr = it.next();
            it.remove();
            mLog.logf("Out of addresses in address pool: dropped declined addr %s",
                    inet4AddrToString(addr));
            // isValidAddress() is always verified for entries in mDeclinedAddrs.
            // However declined addresses may have been requested (typically by the machine that was
            // already using the address) after being declined.
            if (isAvailable(addr)) {
                return new DhcpLease(clientId, hwAddr, addr, expTime, hostname);
            }
        }

        throw new OutOfAddressesException("No address available for offer");
    }
}