From eca5b4e7532e2c7ae114167df39327b5a83ab5a3 Mon Sep 17 00:00:00 2001 From: Remi NGUYEN VAN Date: Wed, 27 Jun 2018 17:20:36 +0900 Subject: [PATCH 1/6] Add DhcpLeaseRepository This is a first component to build the new DHCP server. Test: added tests pass Bug: b/109584964 Change-Id: I5657d89c3010a23e9289ac827bf78381477d1355 --- .../net/java/android/net/dhcp/DhcpLease.java | 138 +++++ .../android/net/dhcp/DhcpLeaseRepository.java | 538 ++++++++++++++++++ .../net/dhcp/DhcpLeaseRepositoryTest.java | 519 +++++++++++++++++ 3 files changed, 1195 insertions(+) create mode 100644 services/net/java/android/net/dhcp/DhcpLease.java create mode 100644 services/net/java/android/net/dhcp/DhcpLeaseRepository.java create mode 100644 tests/net/java/android/net/dhcp/DhcpLeaseRepositoryTest.java diff --git a/services/net/java/android/net/dhcp/DhcpLease.java b/services/net/java/android/net/dhcp/DhcpLease.java new file mode 100644 index 0000000000000..d2a15b37dcc0c --- /dev/null +++ b/services/net/java/android/net/dhcp/DhcpLease.java @@ -0,0 +1,138 @@ +/* + * 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 android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.MacAddress; +import android.os.SystemClock; +import android.text.TextUtils; + +import com.android.internal.util.HexDump; + +import java.net.Inet4Address; +import java.util.Arrays; +import java.util.Objects; + +/** + * An IPv4 address assignment done through DHCPv4. + * @hide + */ +public class DhcpLease { + public static final long EXPIRATION_NEVER = Long.MAX_VALUE; + public static final String HOSTNAME_NONE = null; + + @Nullable + private final byte[] mClientId; + @NonNull + private final MacAddress mHwAddr; + @NonNull + private final Inet4Address mNetAddr; + /** + * Expiration time for the lease, to compare with {@link SystemClock#elapsedRealtime()}. + */ + private final long mExpTime; + @Nullable + private final String mHostname; + + public DhcpLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, + @NonNull Inet4Address netAddr, long expTime, @Nullable String hostname) { + mClientId = (clientId == null ? null : Arrays.copyOf(clientId, clientId.length)); + mHwAddr = hwAddr; + mNetAddr = netAddr; + mExpTime = expTime; + mHostname = hostname; + } + + @Nullable + public byte[] getClientId() { + if (mClientId == null) { + return null; + } + return Arrays.copyOf(mClientId, mClientId.length); + } + + @NonNull + public MacAddress getHwAddr() { + return mHwAddr; + } + + @Nullable + public String getHostname() { + return mHostname; + } + + @NonNull + public Inet4Address getNetAddr() { + return mNetAddr; + } + + public long getExpTime() { + return mExpTime; + } + + /** + * Push back the expiration time of this lease. If the provided time is sooner than the original + * expiration time, the lease time will not be updated. + * + *

The lease hostname is updated with the provided one if set. + * @return A {@link DhcpLease} with expiration time set to max(expTime, currentExpTime) + */ + public DhcpLease renewedLease(long expTime, @Nullable String hostname) { + return new DhcpLease(mClientId, mHwAddr, mNetAddr, Math.max(expTime, mExpTime), + (hostname == null ? mHostname : hostname)); + } + + public boolean matchesClient(@Nullable byte[] clientId, @NonNull MacAddress hwAddr) { + if (mClientId != null) { + return Arrays.equals(mClientId, clientId); + } else { + return clientId == null && mHwAddr.equals(hwAddr); + } + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof DhcpLease)) { + return false; + } + final DhcpLease other = (DhcpLease)obj; + return Arrays.equals(mClientId, other.mClientId) + && mHwAddr.equals(other.mHwAddr) + && mNetAddr.equals(other.mNetAddr) + && mExpTime == other.mExpTime + && TextUtils.equals(mHostname, other.mHostname); + } + + @Override + public int hashCode() { + return Objects.hash(mClientId, mHwAddr, mNetAddr, mHostname, mExpTime); + } + + static String clientIdToString(byte[] bytes) { + if (bytes == null) { + return "null"; + } + return HexDump.toHexString(bytes); + } + + @Override + public String toString() { + return String.format("clientId: %s, hwAddr: %s, netAddr: %s, expTime: %d, hostname: %s", + clientIdToString(mClientId), mHwAddr.toString(), mNetAddr, mExpTime, mHostname); + } +} diff --git a/services/net/java/android/net/dhcp/DhcpLeaseRepository.java b/services/net/java/android/net/dhcp/DhcpLeaseRepository.java new file mode 100644 index 0000000000000..b7a55a47a6a6c --- /dev/null +++ b/services/net/java/android/net/dhcp/DhcpLeaseRepository.java @@ -0,0 +1,538 @@ +/* + * 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.NetworkUtils.inet4AddressToIntHTH; +import static android.net.NetworkUtils.intToInet4AddressHTH; +import static android.net.NetworkUtils.prefixLengthToV4NetmaskIntHTH; +import static android.net.dhcp.DhcpLease.EXPIRATION_NEVER; +import static android.net.util.NetworkConstants.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.util.SharedLog; +import android.os.SystemClock; +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. + * + *

This class is not thread-safe. All public methods should be called on a common thread or + * use some synchronization mechanism. + * + *

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 mReservedAddrs; + private int mSubnetAddr; + private int mSubnetMask; + private int mNumAddresses; + private long mLeaseTimeMs; + + public static class Clock { + /** + * @see SystemClock#elapsedRealtime() + */ + public long elapsedRealtime() { + return SystemClock.elapsedRealtime(); + } + } + + /** + * 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); + } + } + + /** + * Leases by IP address + */ + private final ArrayMap 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 mDeclinedAddrs = new LinkedHashMap<>(); + + public DhcpLeaseRepository(@NonNull IpPrefix prefix, @NonNull Set 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 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 void cleanMap(Map map) { + final Iterator> 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 InvalidAddressException 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, InvalidAddressException { + final long currentTime = mClock.elapsedRealtime(); + final long expTime = currentTime + mLeaseTimeMs; + + removeExpiredLeases(currentTime); + + // 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 InvalidAddressException("Lease requested by relay from outside of subnet"); + } + + 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 static boolean isIpAddrOutsidePrefix(IpPrefix prefix, Inet4Address addr) { + return addr != null && !addr.equals(Inet4Address.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. + * + *

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, @Nullable Inet4Address reqAddr, boolean sidSet, + @Nullable String hostname) throws InvalidAddressException { + final long currentTime = mClock.elapsedRealtime(); + removeExpiredLeases(currentTime); + 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, reqAddr, sidSet, lease); + return lease; + } + + /** + * Check that the client can request the specified address, make or renew the lease if yes, and + * commit it. + * + *

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 " + 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", addr); + return; + } + final long expTime = mClock.elapsedRealtime() + mLeaseTimeMs; + mDeclinedAddrs.put(addr, expTime); + mLog.logf("Marked %s as declined expiring %d", addr, expTime); + maybeUpdateEarliestExpiration(expTime); + } + + /** + * Get the list of currently valid committed leases in the repository. + */ + @NonNull + public List 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 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 long removeExpired(long currentTime, @NonNull Map map, + @NonNull String tag, @NonNull Function getExpTime) { + final Iterator> it = map.entrySet().iterator(); + long firstExpiration = EXPIRATION_NEVER; + while (it.hasNext()) { + final Entry 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. + * + *

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. + * + *

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. + * + *

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. + * + *

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. + * + *

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 = (Inet4Address) 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 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", 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"); + } +} diff --git a/tests/net/java/android/net/dhcp/DhcpLeaseRepositoryTest.java b/tests/net/java/android/net/dhcp/DhcpLeaseRepositoryTest.java new file mode 100644 index 0000000000000..edadd6ead667f --- /dev/null +++ b/tests/net/java/android/net/dhcp/DhcpLeaseRepositoryTest.java @@ -0,0 +1,519 @@ +/* + * 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.HOSTNAME_NONE; +import static android.net.dhcp.DhcpLeaseRepository.CLIENTID_UNSPEC; +import static android.net.dhcp.DhcpLeaseRepository.INETADDR_UNSPEC; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.when; + +import static java.lang.String.format; +import static java.net.InetAddress.parseNumericAddress; + +import android.annotation.NonNull; +import android.net.IpPrefix; +import android.net.MacAddress; +import android.net.dhcp.DhcpLeaseRepository.Clock; +import android.net.util.SharedLog; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.net.Inet4Address; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class DhcpLeaseRepositoryTest { + private static final Inet4Address INET4_ANY = (Inet4Address) Inet4Address.ANY; + private static final Inet4Address TEST_DEF_ROUTER = parseAddr4("192.168.42.247"); + private static final Inet4Address TEST_SERVER_ADDR = parseAddr4("192.168.42.241"); + private static final Inet4Address TEST_RESERVED_ADDR = parseAddr4("192.168.42.243"); + private static final MacAddress TEST_MAC_1 = MacAddress.fromBytes( + new byte[] { 5, 4, 3, 2, 1, 0 }); + private static final MacAddress TEST_MAC_2 = MacAddress.fromBytes( + new byte[] { 0, 1, 2, 3, 4, 5 }); + private static final MacAddress TEST_MAC_3 = MacAddress.fromBytes( + new byte[] { 0, 1, 2, 3, 4, 6 }); + private static final Inet4Address TEST_INETADDR_1 = parseAddr4("192.168.42.248"); + private static final Inet4Address TEST_INETADDR_2 = parseAddr4("192.168.42.249"); + private static final String TEST_HOSTNAME_1 = "hostname1"; + private static final String TEST_HOSTNAME_2 = "hostname2"; + private static final IpPrefix TEST_IP_PREFIX = new IpPrefix(TEST_SERVER_ADDR, 22); + private static final long TEST_TIME = 100L; + private static final int TEST_LEASE_TIME_MS = 3_600_000; + private static final Set TEST_EXCL_SET = + Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + TEST_SERVER_ADDR, TEST_DEF_ROUTER, TEST_RESERVED_ADDR))); + + @NonNull + private SharedLog mLog; + @NonNull @Mock + private Clock mClock; + @NonNull + private DhcpLeaseRepository mRepo; + + private static Inet4Address parseAddr4(String inet4Addr) { + return (Inet4Address) parseNumericAddress(inet4Addr); + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mLog = new SharedLog("DhcpLeaseRepositoryTest"); + when(mClock.elapsedRealtime()).thenReturn(TEST_TIME); + mRepo = new DhcpLeaseRepository( + TEST_IP_PREFIX, TEST_EXCL_SET, TEST_LEASE_TIME_MS, mLog, mClock); + } + + /** + * Request a number of addresses through offer/request. Useful to test address exhaustion. + * @param nAddr Number of addresses to request. + */ + private void requestAddresses(byte nAddr) throws Exception { + final HashSet addrs = new HashSet<>(); + byte[] hwAddrBytes = new byte[] { 8, 4, 3, 2, 1, 0 }; + for (byte i = 0; i < nAddr; i++) { + hwAddrBytes[5] = i; + MacAddress newMac = MacAddress.fromBytes(hwAddrBytes); + final String hostname = "host_" + i; + final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, newMac, + INETADDR_UNSPEC /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, hostname); + + assertNotNull(lease); + assertEquals(newMac, lease.getHwAddr()); + assertEquals(hostname, lease.getHostname()); + assertTrue(format("Duplicate address allocated: %s in %s", lease.getNetAddr(), addrs), + addrs.add(lease.getNetAddr())); + + mRepo.requestLease(null, newMac, null, lease.getNetAddr(), true, hostname); + } + } + + @Test + public void testAddressExhaustion() throws Exception { + // Use a /28 to quickly run out of addresses + mRepo.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), TEST_EXCL_SET, TEST_LEASE_TIME_MS); + + // /28 should have 16 addresses, 14 w/o the first/last, 11 w/o excluded addresses + requestAddresses((byte)11); + + try { + mRepo.getOffer(null, TEST_MAC_2, + null /* relayAddr */, null /* reqAddr */, HOSTNAME_NONE); + fail("Should be out of addresses"); + } catch (DhcpLeaseRepository.OutOfAddressesException e) { + // Expected + } + } + + @Test + public void testUpdateParams_LeaseCleanup() throws Exception { + // Inside /28: + final Inet4Address reqAddrIn28 = parseAddr4("192.168.42.242"); + final Inet4Address declinedAddrIn28 = parseAddr4("192.168.42.245"); + + // Inside /28, but not available there (first address of the range) + final Inet4Address declinedFirstAddrIn28 = parseAddr4("192.168.42.240"); + + final DhcpLease reqAddrIn28Lease = mRepo.requestLease( + CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, reqAddrIn28, false, HOSTNAME_NONE); + mRepo.markLeaseDeclined(declinedAddrIn28); + mRepo.markLeaseDeclined(declinedFirstAddrIn28); + + // Inside /22, but outside /28: + final Inet4Address reqAddrIn22 = parseAddr4("192.168.42.3"); + final Inet4Address declinedAddrIn22 = parseAddr4("192.168.42.4"); + + final DhcpLease reqAddrIn22Lease = mRepo.requestLease( + CLIENTID_UNSPEC, TEST_MAC_3, INET4_ANY, reqAddrIn22, false, HOSTNAME_NONE); + mRepo.markLeaseDeclined(declinedAddrIn22); + + // Address that will be reserved in the updateParams call below + final Inet4Address reservedAddr = parseAddr4("192.168.42.244"); + final DhcpLease reservedAddrLease = mRepo.requestLease( + CLIENTID_UNSPEC, TEST_MAC_2, INET4_ANY, reservedAddr, false, HOSTNAME_NONE); + + // Update from /22 to /28 and add another reserved address + Set newReserved = new HashSet<>(TEST_EXCL_SET); + newReserved.add(reservedAddr); + mRepo.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), newReserved, TEST_LEASE_TIME_MS); + + assertHasLease(reqAddrIn28Lease); + assertDeclined(declinedAddrIn28); + + assertNotDeclined(declinedFirstAddrIn28); + + assertNoLease(reqAddrIn22Lease); + assertNotDeclined(declinedAddrIn22); + + assertNoLease(reservedAddrLease); + } + + @Test + public void testGetOffer_StableAddress() throws Exception { + for (final MacAddress macAddr : new MacAddress[] { TEST_MAC_1, TEST_MAC_2, TEST_MAC_3 }) { + final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, macAddr, + INETADDR_UNSPEC /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + + // Same lease is offered twice + final DhcpLease newLease = mRepo.getOffer(CLIENTID_UNSPEC, macAddr, + INETADDR_UNSPEC /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + assertEquals(lease, newLease); + } + } + + @Test + public void testUpdateParams_UsesNewPrefix() throws Exception { + final IpPrefix newPrefix = new IpPrefix(parseAddr4("192.168.123.0"), 24); + mRepo.updateParams(newPrefix, TEST_EXCL_SET, TEST_LEASE_TIME_MS); + + DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, + INETADDR_UNSPEC, INETADDR_UNSPEC, HOSTNAME_NONE); + assertTrue(newPrefix.contains(lease.getNetAddr())); + } + + @Test + public void testGetOffer_ExistingLease() throws Exception { + mRepo.requestLease( + CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, TEST_INETADDR_1, false, TEST_HOSTNAME_1); + + DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, + INETADDR_UNSPEC, INETADDR_UNSPEC, HOSTNAME_NONE); + assertEquals(TEST_INETADDR_1, offer.getNetAddr()); + assertEquals(TEST_HOSTNAME_1, offer.getHostname()); + } + + @Test + public void testGetOffer_ClientIdHasExistingLease() throws Exception { + final byte[] clientId = new byte[] { 1, 2 }; + mRepo.requestLease(clientId, TEST_MAC_1, INET4_ANY, TEST_INETADDR_1, false, + TEST_HOSTNAME_1); + + // Different MAC, but same clientId + DhcpLease offer = mRepo.getOffer(clientId, TEST_MAC_2, + INETADDR_UNSPEC, INETADDR_UNSPEC, HOSTNAME_NONE); + assertEquals(TEST_INETADDR_1, offer.getNetAddr()); + assertEquals(TEST_HOSTNAME_1, offer.getHostname()); + } + + @Test + public void testGetOffer_DifferentClientId() throws Exception { + final byte[] clientId1 = new byte[] { 1, 2 }; + final byte[] clientId2 = new byte[] { 3, 4 }; + mRepo.requestLease(clientId1, TEST_MAC_1, INET4_ANY, TEST_INETADDR_1, false, + TEST_HOSTNAME_1); + + // Same MAC, different client ID + DhcpLease offer = mRepo.getOffer(clientId2, TEST_MAC_1, + INETADDR_UNSPEC, INETADDR_UNSPEC, HOSTNAME_NONE); + // Obtains a different address + assertNotEquals(TEST_INETADDR_1, offer.getNetAddr()); + assertEquals(HOSTNAME_NONE, offer.getHostname()); + assertEquals(TEST_MAC_1, offer.getHwAddr()); + } + + @Test + public void testGetOffer_RequestedAddress() throws Exception { + DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + TEST_INETADDR_1, TEST_HOSTNAME_1); + assertEquals(TEST_INETADDR_1, offer.getNetAddr()); + assertEquals(TEST_HOSTNAME_1, offer.getHostname()); + } + + @Test + public void testGetOffer_RequestedAddressInUse() throws Exception { + mRepo.requestLease( + CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, TEST_INETADDR_1, false, HOSTNAME_NONE); + DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_2, INET4_ANY, + TEST_INETADDR_1, HOSTNAME_NONE); + assertNotEquals(TEST_INETADDR_1, offer.getNetAddr()); + } + + @Test + public void testGetOffer_RequestedAddressReserved() throws Exception { + DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + TEST_RESERVED_ADDR, HOSTNAME_NONE); + assertNotEquals(TEST_RESERVED_ADDR, offer.getNetAddr()); + } + + @Test + public void testGetOffer_RequestedAddressInvalid() throws Exception { + final Inet4Address invalidAddr = parseAddr4("192.168.42.0"); + DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + invalidAddr, HOSTNAME_NONE); + assertNotEquals(invalidAddr, offer.getNetAddr()); + } + + @Test + public void testGetOffer_RequestedAddressOutsideSubnet() throws Exception { + final Inet4Address invalidAddr = parseAddr4("192.168.254.2"); + DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + invalidAddr, HOSTNAME_NONE); + assertNotEquals(invalidAddr, offer.getNetAddr()); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testGetOffer_RelayInInvalidSubnet() throws Exception { + mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, + parseAddr4("192.168.254.2") /* relayAddr */, INETADDR_UNSPEC, HOSTNAME_NONE); + } + + @Test + public void testRequestLease_SelectingTwice() throws Exception { + DhcpLease lease1 = mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + TEST_INETADDR_1, true /* sidSet */, TEST_HOSTNAME_1); + + // Second request from same client for a different address + DhcpLease lease2 = mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + TEST_INETADDR_2, true /* sidSet */, TEST_HOSTNAME_2); + + assertEquals(TEST_INETADDR_1, lease1.getNetAddr()); + assertEquals(TEST_HOSTNAME_1, lease1.getHostname()); + + assertEquals(TEST_INETADDR_2, lease2.getNetAddr()); + assertEquals(TEST_HOSTNAME_2, lease2.getHostname()); + + // First address freed when client requested a different one: another client can request it + DhcpLease lease3 = mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_2, INET4_ANY, + TEST_INETADDR_1, true /* sidSet */, HOSTNAME_NONE); + assertEquals(TEST_INETADDR_1, lease3.getNetAddr()); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_SelectingInvalid() throws Exception { + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + parseAddr4("192.168.254.5"), true /* sidSet */, HOSTNAME_NONE); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_SelectingInUse() throws Exception { + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + TEST_INETADDR_1, true /* sidSet */, HOSTNAME_NONE); + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_2, INET4_ANY, + TEST_INETADDR_1, true /* sidSet */, HOSTNAME_NONE); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_SelectingReserved() throws Exception { + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + TEST_RESERVED_ADDR, true /* sidSet */, HOSTNAME_NONE); + } + + @Test + public void testRequestLease_InitReboot() throws Exception { + // Request address once + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + TEST_INETADDR_1, true /* sidSet */, HOSTNAME_NONE); + + final long newTime = TEST_TIME + 100; + when(mClock.elapsedRealtime()).thenReturn(newTime); + + // init-reboot (sidSet == false): verify configuration + DhcpLease lease = mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + TEST_INETADDR_1, false, HOSTNAME_NONE); + assertEquals(TEST_INETADDR_1, lease.getNetAddr()); + assertEquals(newTime + TEST_LEASE_TIME_MS, lease.getExpTime()); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_InitRebootWrongAddr() throws Exception { + // Request address once + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + TEST_INETADDR_1, true /* sidSet */, HOSTNAME_NONE); + // init-reboot with different requested address + mRepo.requestLease( + CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, TEST_INETADDR_2, false, HOSTNAME_NONE); + } + + @Test + public void testRequestLease_InitRebootUnknownAddr() throws Exception { + // init-reboot with unknown requested address + DhcpLease lease = mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + TEST_INETADDR_2, false, HOSTNAME_NONE); + // RFC2131 says we should not reply to accommodate other servers, but since we are + // authoritative we allow creating the lease to avoid issues with lost lease DB (same as + // dnsmasq behavior) + assertEquals(TEST_INETADDR_2, lease.getNetAddr()); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_InitRebootWrongSubnet() throws Exception { + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + parseAddr4("192.168.254.2"), false /* sidSet */, HOSTNAME_NONE); + } + + @Test + public void testRequestLease_Renewing() throws Exception { + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, + INET4_ANY /* clientAddr */, TEST_INETADDR_1 /* reqAddr */, true, HOSTNAME_NONE); + + final long newTime = TEST_TIME + 100; + when(mClock.elapsedRealtime()).thenReturn(newTime); + + // Renewing: clientAddr filled in, no reqAddr + DhcpLease lease = mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, + TEST_INETADDR_1 /* clientAddr */, INETADDR_UNSPEC /* reqAddr */, false, + HOSTNAME_NONE); + + assertEquals(TEST_INETADDR_1, lease.getNetAddr()); + assertEquals(newTime + TEST_LEASE_TIME_MS, lease.getExpTime()); + } + + @Test + public void testRequestLease_RenewingUnknownAddr() throws Exception { + final long newTime = TEST_TIME + 100; + when(mClock.elapsedRealtime()).thenReturn(newTime); + DhcpLease lease = mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, + TEST_INETADDR_1 /* clientAddr */, INETADDR_UNSPEC /* reqAddr */, false, + HOSTNAME_NONE); + // Allows renewing an unknown address if available + assertEquals(TEST_INETADDR_1, lease.getNetAddr()); + assertEquals(newTime + TEST_LEASE_TIME_MS, lease.getExpTime()); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_RenewingAddrInUse() throws Exception { + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_2, + INET4_ANY /* clientAddr */, TEST_INETADDR_1 /* reqAddr */, true, HOSTNAME_NONE); + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, + TEST_INETADDR_1 /* clientAddr */, INETADDR_UNSPEC /* reqAddr */, false, + HOSTNAME_NONE); + } + + @Test(expected = DhcpLeaseRepository.InvalidAddressException.class) + public void testRequestLease_RenewingInvalidAddr() throws Exception { + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, parseAddr4("192.168.254.2") /* clientAddr */, + INETADDR_UNSPEC /* reqAddr */, false, HOSTNAME_NONE); + } + + @Test + public void testReleaseLease() throws Exception { + DhcpLease lease1 = mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, + TEST_INETADDR_1, true /* sidSet */, HOSTNAME_NONE); + + assertHasLease(lease1); + assertTrue(mRepo.releaseLease(CLIENTID_UNSPEC, TEST_MAC_1, TEST_INETADDR_1)); + assertNoLease(lease1); + + DhcpLease lease2 = mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_2, INET4_ANY, + TEST_INETADDR_1, true /* sidSet */, HOSTNAME_NONE); + + assertEquals(TEST_INETADDR_1, lease2.getNetAddr()); + } + + @Test + public void testReleaseLease_UnknownLease() { + assertFalse(mRepo.releaseLease(CLIENTID_UNSPEC, TEST_MAC_1, TEST_INETADDR_1)); + } + + @Test + public void testReleaseLease_StableOffer() throws Exception { + for (MacAddress mac : new MacAddress[] { TEST_MAC_1, TEST_MAC_2, TEST_MAC_3 }) { + final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, mac, + INETADDR_UNSPEC /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + mRepo.requestLease( + CLIENTID_UNSPEC, mac, INET4_ANY, lease.getNetAddr(), true, + HOSTNAME_NONE); + mRepo.releaseLease(CLIENTID_UNSPEC, mac, lease.getNetAddr()); + + // Same lease is offered after it was released + final DhcpLease newLease = mRepo.getOffer(CLIENTID_UNSPEC, mac, + INETADDR_UNSPEC /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + assertEquals(lease.getNetAddr(), newLease.getNetAddr()); + } + } + + @Test + public void testMarkLeaseDeclined() throws Exception { + final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, + INETADDR_UNSPEC /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + + mRepo.markLeaseDeclined(lease.getNetAddr()); + + // Same lease is not offered again + final DhcpLease newLease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, + INETADDR_UNSPEC /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + assertNotEquals(lease.getNetAddr(), newLease.getNetAddr()); + } + + @Test + public void testMarkLeaseDeclined_UsedIfOutOfAddresses() throws Exception { + // Use a /28 to quickly run out of addresses + mRepo.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), TEST_EXCL_SET, TEST_LEASE_TIME_MS); + + mRepo.markLeaseDeclined(TEST_INETADDR_1); + mRepo.markLeaseDeclined(TEST_INETADDR_2); + + // /28 should have 16 addresses, 14 w/o the first/last, 11 w/o excluded addresses + requestAddresses((byte)9); + + // Last 2 addresses: addresses marked declined should be used + final DhcpLease firstLease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, + INETADDR_UNSPEC /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, TEST_HOSTNAME_1); + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY, firstLease.getNetAddr(), true, + HOSTNAME_NONE); + + final DhcpLease secondLease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_2, + INETADDR_UNSPEC /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, TEST_HOSTNAME_2); + mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_2, INET4_ANY, secondLease.getNetAddr(), true, + HOSTNAME_NONE); + + // Now out of addresses + try { + mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_3, INETADDR_UNSPEC /* relayAddr */, + INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE); + fail("Repository should be out of addresses and throw"); + } catch (DhcpLeaseRepository.OutOfAddressesException e) { /* expected */ } + + assertEquals(TEST_INETADDR_1, firstLease.getNetAddr()); + assertEquals(TEST_HOSTNAME_1, firstLease.getHostname()); + assertEquals(TEST_INETADDR_2, secondLease.getNetAddr()); + assertEquals(TEST_HOSTNAME_2, secondLease.getHostname()); + } + + private void assertNoLease(DhcpLease lease) { + assertFalse("Leases contain " + lease, mRepo.getCommittedLeases().contains(lease)); + } + + private void assertHasLease(DhcpLease lease) { + assertTrue("Leases do not contain " + lease, mRepo.getCommittedLeases().contains(lease)); + } + + private void assertNotDeclined(Inet4Address addr) { + assertFalse("Address is declined: " + addr, mRepo.getDeclinedAddresses().contains(addr)); + } + + private void assertDeclined(Inet4Address addr) { + assertTrue("Address is not declined: " + addr, mRepo.getDeclinedAddresses().contains(addr)); + } +} From c1413d0a278d64a05e5da9d269327408f795fed0 Mon Sep 17 00:00:00 2001 From: Remi NGUYEN VAN Date: Wed, 6 Jun 2018 15:47:07 +0900 Subject: [PATCH 2/6] Add DHCP utils extracted from DhcpClient DhcpClient can then be migrated to at least DhcpSocketFactory, and eventually DhcpPacketListener. These classes will be used to implement the new DhcpServer. FdEventsReader is PacketReader with generic T instead of byte[], to allow reading both received payload and source IP from a UDP socket with Os.recvfrom(). Bug: 109584964 Test: runtest --no-hidden-api-checks frameworks-net Change-Id: Idd7dc36938748af701b45f50bde76a2592c9bfdd --- .../android/net/dhcp/DhcpPacketListener.java | 84 ++++++ .../java/android/net/util/FdEventsReader.java | 254 ++++++++++++++++++ .../java/android/net/util/PacketReader.java | 210 +-------------- 3 files changed, 348 insertions(+), 200 deletions(-) create mode 100644 services/net/java/android/net/dhcp/DhcpPacketListener.java create mode 100644 services/net/java/android/net/util/FdEventsReader.java diff --git a/services/net/java/android/net/dhcp/DhcpPacketListener.java b/services/net/java/android/net/dhcp/DhcpPacketListener.java new file mode 100644 index 0000000000000..498fd93fff599 --- /dev/null +++ b/services/net/java/android/net/dhcp/DhcpPacketListener.java @@ -0,0 +1,84 @@ +/* + * 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 android.annotation.Nullable; +import android.net.util.FdEventsReader; +import android.net.util.PacketReader; +import android.os.Handler; +import android.system.Os; + +import java.io.FileDescriptor; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; + +/** + * A {@link FdEventsReader} to receive and parse {@link DhcpPacket}. + * @hide + */ +abstract class DhcpPacketListener extends FdEventsReader { + static final class Payload { + final byte[] bytes = new byte[DhcpPacket.MAX_LENGTH]; + Inet4Address srcAddr; + } + + public DhcpPacketListener(Handler handler) { + super(handler, new Payload()); + } + + @Override + protected int recvBufSize(Payload buffer) { + return buffer.bytes.length; + } + + @Override + protected final void handlePacket(Payload recvbuf, int length) { + if (recvbuf.srcAddr == null) { + return; + } + + try { + final DhcpPacket packet = DhcpPacket.decodeFullPacket(recvbuf.bytes, length, + DhcpPacket.ENCAP_BOOTP); + onReceive(packet, recvbuf.srcAddr); + } catch (DhcpPacket.ParseException e) { + logParseError(recvbuf.bytes, length, e); + } + } + + @Override + protected int readPacket(FileDescriptor fd, Payload packetBuffer) throws Exception { + final InetSocketAddress addr = new InetSocketAddress(); + final int read = Os.recvfrom( + fd, packetBuffer.bytes, 0, packetBuffer.bytes.length, 0 /* flags */, addr); + + // Buffers with null srcAddr will be dropped in handlePacket() + packetBuffer.srcAddr = inet4AddrOrNull(addr); + return read; + } + + @Nullable + private static Inet4Address inet4AddrOrNull(InetSocketAddress addr) { + return addr.getAddress() instanceof Inet4Address + ? (Inet4Address) addr.getAddress() + : null; + } + + protected abstract void onReceive(DhcpPacket packet, Inet4Address srcAddr); + protected abstract void logParseError(byte[] packet, int length, DhcpPacket.ParseException e); +} diff --git a/services/net/java/android/net/util/FdEventsReader.java b/services/net/java/android/net/util/FdEventsReader.java new file mode 100644 index 0000000000000..575444f89bc33 --- /dev/null +++ b/services/net/java/android/net/util/FdEventsReader.java @@ -0,0 +1,254 @@ +/* + * Copyright (C) 2016 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.util; + +import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT; +import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Handler; +import android.os.Looper; +import android.os.MessageQueue; +import android.system.ErrnoException; +import android.system.OsConstants; + +import libcore.io.IoUtils; + +import java.io.FileDescriptor; + + +/** + * This class encapsulates the mechanics of registering a file descriptor + * with a thread's Looper and handling read events (and errors). + * + * Subclasses MUST implement createFd() and SHOULD override handlePacket(). They MAY override + * onStop() and onStart(). + * + * Subclasses can expect a call life-cycle like the following: + * + * [1] when a client calls start(), createFd() is called, followed by the onStart() hook if all + * goes well. Implementations may override onStart() for additional initialization. + * + * [2] yield, waiting for read event or error notification: + * + * [a] readPacket() && handlePacket() + * + * [b] if (no error): + * goto 2 + * else: + * goto 3 + * + * [3] when a client calls stop(), the onStop() hook is called (unless already stopped or never + * started). Implementations may override onStop() for additional cleanup. + * + * The packet receive buffer is recycled on every read call, so subclasses + * should make any copies they would like inside their handlePacket() + * implementation. + * + * All public methods MUST only be called from the same thread with which + * the Handler constructor argument is associated. + * + * @hide + */ +public abstract class FdEventsReader { + private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR; + private static final int UNREGISTER_THIS_FD = 0; + + @NonNull + private final Handler mHandler; + @NonNull + private final MessageQueue mQueue; + @NonNull + private final BufferType mBuffer; + @Nullable + private FileDescriptor mFd; + private long mPacketsReceived; + + protected static void closeFd(FileDescriptor fd) { + IoUtils.closeQuietly(fd); + } + + protected FdEventsReader(@NonNull Handler h, @NonNull BufferType buffer) { + mHandler = h; + mQueue = mHandler.getLooper().getQueue(); + mBuffer = buffer; + } + + public final void start() { + if (onCorrectThread()) { + createAndRegisterFd(); + } else { + mHandler.post(() -> { + logError("start() called from off-thread", null); + createAndRegisterFd(); + }); + } + } + + public final void stop() { + if (onCorrectThread()) { + unregisterAndDestroyFd(); + } else { + mHandler.post(() -> { + logError("stop() called from off-thread", null); + unregisterAndDestroyFd(); + }); + } + } + + @NonNull + public Handler getHandler() { return mHandler; } + + protected abstract int recvBufSize(@NonNull BufferType buffer); + + public int recvBufSize() { return recvBufSize(mBuffer); } + + /** + * Get the number of successful calls to {@link #readPacket(FileDescriptor, Object)}. + * + *

A call was successful if {@link #readPacket(FileDescriptor, Object)} returned a value > 0. + */ + public final long numPacketsReceived() { return mPacketsReceived; } + + /** + * Subclasses MUST create the listening socket here, including setting + * all desired socket options, interface or address/port binding, etc. + */ + @Nullable + protected abstract FileDescriptor createFd(); + + /** + * Implementations MUST return the bytes read or throw an Exception. + * + *

The caller may throw a {@link ErrnoException} with {@link OsConstants#EAGAIN} or + * {@link OsConstants#EINTR}, in which case {@link FdEventsReader} will ignore the buffer + * contents and respectively wait for further input or retry the read immediately. For all other + * exceptions, the {@link FdEventsReader} will be stopped with no more interactions with this + * method. + */ + protected abstract int readPacket(@NonNull FileDescriptor fd, @NonNull BufferType buffer) + throws Exception; + + /** + * Called by the main loop for every packet. Any desired copies of + * |recvbuf| should be made in here, as the underlying byte array is + * reused across all reads. + */ + protected void handlePacket(@NonNull BufferType recvbuf, int length) {} + + /** + * Called by the main loop to log errors. In some cases |e| may be null. + */ + protected void logError(@NonNull String msg, @Nullable Exception e) {} + + /** + * Called by start(), if successful, just prior to returning. + */ + protected void onStart() {} + + /** + * Called by stop() just prior to returning. + */ + protected void onStop() {} + + private void createAndRegisterFd() { + if (mFd != null) return; + + try { + mFd = createFd(); + if (mFd != null) { + // Force the socket to be non-blocking. + IoUtils.setBlocking(mFd, false); + } + } catch (Exception e) { + logError("Failed to create socket: ", e); + closeFd(mFd); + mFd = null; + } + + if (mFd == null) return; + + mQueue.addOnFileDescriptorEventListener( + mFd, + FD_EVENTS, + (fd, events) -> { + // Always call handleInput() so read/recvfrom are given + // a proper chance to encounter a meaningful errno and + // perhaps log a useful error message. + if (!isRunning() || !handleInput()) { + unregisterAndDestroyFd(); + return UNREGISTER_THIS_FD; + } + return FD_EVENTS; + }); + onStart(); + } + + private boolean isRunning() { return (mFd != null) && mFd.valid(); } + + // Keep trying to read until we get EAGAIN/EWOULDBLOCK or some fatal error. + private boolean handleInput() { + while (isRunning()) { + final int bytesRead; + + try { + bytesRead = readPacket(mFd, mBuffer); + if (bytesRead < 1) { + if (isRunning()) logError("Socket closed, exiting", null); + break; + } + mPacketsReceived++; + } catch (ErrnoException e) { + if (e.errno == OsConstants.EAGAIN) { + // We've read everything there is to read this time around. + return true; + } else if (e.errno == OsConstants.EINTR) { + continue; + } else { + if (isRunning()) logError("readPacket error: ", e); + break; + } + } catch (Exception e) { + if (isRunning()) logError("readPacket error: ", e); + break; + } + + try { + handlePacket(mBuffer, bytesRead); + } catch (Exception e) { + logError("handlePacket error: ", e); + break; + } + } + + return false; + } + + private void unregisterAndDestroyFd() { + if (mFd == null) return; + + mQueue.removeOnFileDescriptorEventListener(mFd); + closeFd(mFd); + mFd = null; + onStop(); + } + + private boolean onCorrectThread() { + return (mHandler.getLooper() == Looper.myLooper()); + } +} diff --git a/services/net/java/android/net/util/PacketReader.java b/services/net/java/android/net/util/PacketReader.java index 10da2a551e217..4aec6b6753a69 100644 --- a/services/net/java/android/net/util/PacketReader.java +++ b/services/net/java/android/net/util/PacketReader.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 The Android Open Source Project + * 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. @@ -16,236 +16,46 @@ package android.net.util; -import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT; -import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR; +import static java.lang.Math.max; -import android.annotation.Nullable; import android.os.Handler; -import android.os.Looper; -import android.os.MessageQueue; -import android.os.MessageQueue.OnFileDescriptorEventListener; -import android.system.ErrnoException; import android.system.Os; -import android.system.OsConstants; - -import libcore.io.IoUtils; import java.io.FileDescriptor; -import java.io.IOException; - /** - * This class encapsulates the mechanics of registering a file descriptor - * with a thread's Looper and handling read events (and errors). - * - * Subclasses MUST implement createFd() and SHOULD override handlePacket(). - - * Subclasses can expect a call life-cycle like the following: - * - * [1] start() calls createFd() and (if all goes well) onStart() - * - * [2] yield, waiting for read event or error notification: - * - * [a] readPacket() && handlePacket() - * - * [b] if (no error): - * goto 2 - * else: - * goto 3 - * - * [3] stop() calls onStop() if not previously stopped - * - * The packet receive buffer is recycled on every read call, so subclasses - * should make any copies they would like inside their handlePacket() - * implementation. - * - * All public methods MUST only be called from the same thread with which - * the Handler constructor argument is associated. + * Specialization of {@link FdEventsReader} that reads packets into a byte array. * * TODO: rename this class to something more correctly descriptive (something * like [or less horrible than] FdReadEventsHandler?). * * @hide */ -public abstract class PacketReader { - private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR; - private static final int UNREGISTER_THIS_FD = 0; +public abstract class PacketReader extends FdEventsReader { public static final int DEFAULT_RECV_BUF_SIZE = 2 * 1024; - private final Handler mHandler; - private final MessageQueue mQueue; - private final byte[] mPacket; - private FileDescriptor mFd; - private long mPacketsReceived; - - protected static void closeFd(FileDescriptor fd) { - IoUtils.closeQuietly(fd); - } - protected PacketReader(Handler h) { this(h, DEFAULT_RECV_BUF_SIZE); } - protected PacketReader(Handler h, int recvbufsize) { - mHandler = h; - mQueue = mHandler.getLooper().getQueue(); - mPacket = new byte[Math.max(recvbufsize, DEFAULT_RECV_BUF_SIZE)]; + protected PacketReader(Handler h, int recvBufSize) { + super(h, new byte[max(recvBufSize, DEFAULT_RECV_BUF_SIZE)]); } - public final void start() { - if (onCorrectThread()) { - createAndRegisterFd(); - } else { - mHandler.post(() -> { - logError("start() called from off-thread", null); - createAndRegisterFd(); - }); - } + @Override + protected final int recvBufSize(byte[] buffer) { + return buffer.length; } - public final void stop() { - if (onCorrectThread()) { - unregisterAndDestroyFd(); - } else { - mHandler.post(() -> { - logError("stop() called from off-thread", null); - unregisterAndDestroyFd(); - }); - } - } - - public Handler getHandler() { return mHandler; } - - public final int recvBufSize() { return mPacket.length; } - - public final long numPacketsReceived() { return mPacketsReceived; } - - /** - * Subclasses MUST create the listening socket here, including setting - * all desired socket options, interface or address/port binding, etc. - */ - protected abstract FileDescriptor createFd(); - /** * Subclasses MAY override this to change the default read() implementation * in favour of, say, recvfrom(). * * Implementations MUST return the bytes read or throw an Exception. */ + @Override protected int readPacket(FileDescriptor fd, byte[] packetBuffer) throws Exception { return Os.read(fd, packetBuffer, 0, packetBuffer.length); } - - /** - * Called by the main loop for every packet. Any desired copies of - * |recvbuf| should be made in here, as the underlying byte array is - * reused across all reads. - */ - protected void handlePacket(byte[] recvbuf, int length) {} - - /** - * Called by the main loop to log errors. In some cases |e| may be null. - */ - protected void logError(String msg, Exception e) {} - - /** - * Called by start(), if successful, just prior to returning. - */ - protected void onStart() {} - - /** - * Called by stop() just prior to returning. - */ - protected void onStop() {} - - private void createAndRegisterFd() { - if (mFd != null) return; - - try { - mFd = createFd(); - if (mFd != null) { - // Force the socket to be non-blocking. - IoUtils.setBlocking(mFd, false); - } - } catch (Exception e) { - logError("Failed to create socket: ", e); - closeFd(mFd); - mFd = null; - return; - } - - if (mFd == null) return; - - mQueue.addOnFileDescriptorEventListener( - mFd, - FD_EVENTS, - new OnFileDescriptorEventListener() { - @Override - public int onFileDescriptorEvents(FileDescriptor fd, int events) { - // Always call handleInput() so read/recvfrom are given - // a proper chance to encounter a meaningful errno and - // perhaps log a useful error message. - if (!isRunning() || !handleInput()) { - unregisterAndDestroyFd(); - return UNREGISTER_THIS_FD; - } - return FD_EVENTS; - } - }); - onStart(); - } - - private boolean isRunning() { return (mFd != null) && mFd.valid(); } - - // Keep trying to read until we get EAGAIN/EWOULDBLOCK or some fatal error. - private boolean handleInput() { - while (isRunning()) { - final int bytesRead; - - try { - bytesRead = readPacket(mFd, mPacket); - if (bytesRead < 1) { - if (isRunning()) logError("Socket closed, exiting", null); - break; - } - mPacketsReceived++; - } catch (ErrnoException e) { - if (e.errno == OsConstants.EAGAIN) { - // We've read everything there is to read this time around. - return true; - } else if (e.errno == OsConstants.EINTR) { - continue; - } else { - if (isRunning()) logError("readPacket error: ", e); - break; - } - } catch (Exception e) { - if (isRunning()) logError("readPacket error: ", e); - break; - } - - try { - handlePacket(mPacket, bytesRead); - } catch (Exception e) { - logError("handlePacket error: ", e); - break; - } - } - - return false; - } - - private void unregisterAndDestroyFd() { - if (mFd == null) return; - - mQueue.removeOnFileDescriptorEventListener(mFd); - closeFd(mFd); - mFd = null; - onStop(); - } - - private boolean onCorrectThread() { - return (mHandler.getLooper() == Looper.myLooper()); - } } From 12da4a5efc6379056370ae3cc0bf37233669bfdf Mon Sep 17 00:00:00 2001 From: Remi NGUYEN VAN Date: Wed, 4 Jul 2018 11:15:56 +0900 Subject: [PATCH 3/6] Add util to add an ARP table entry This is to be used by the new DhcpServer to add ARP entries with new addresses before sending unicast responses. Test: manual: cat /proc/net/arp with implementation based on this Bug: b/109584964 Change-Id: I3559893583aa3c49b188ad689a41ee2f3e9d9bf3 --- core/java/android/net/NetworkUtils.java | 12 ++++++ core/jni/android_net_NetUtils.cpp | 50 +++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java index 599ccb287a267..451435c909977 100644 --- a/core/java/android/net/NetworkUtils.java +++ b/core/java/android/net/NetworkUtils.java @@ -22,6 +22,7 @@ import android.util.Log; import android.util.Pair; import java.io.FileDescriptor; +import java.io.IOException; import java.math.BigInteger; import java.net.Inet4Address; import java.net.Inet6Address; @@ -130,6 +131,17 @@ public class NetworkUtils { */ public native static boolean queryUserAccess(int uid, int netId); + /** + * Add an entry into the ARP cache. + */ + public static void addArpEntry(Inet4Address ipv4Addr, MacAddress ethAddr, String ifname, + FileDescriptor fd) throws IOException { + addArpEntry(ethAddr.toByteArray(), ipv4Addr.getAddress(), ifname, fd); + } + + private static native void addArpEntry(byte[] ethAddr, byte[] netAddr, String ifname, + FileDescriptor fd) throws IOException; + /** * @see #intToInet4AddressHTL(int) * @deprecated Use either {@link #intToInet4AddressHTH(int)} diff --git a/core/jni/android_net_NetUtils.cpp b/core/jni/android_net_NetUtils.cpp index 823f1cc362250..9b138ebb760ab 100644 --- a/core/jni/android_net_NetUtils.cpp +++ b/core/jni/android_net_NetUtils.cpp @@ -323,6 +323,55 @@ static jboolean android_net_utils_queryUserAccess(JNIEnv *env, jobject thiz, jin return (jboolean) !queryUserAccess(uid, netId); } +static bool checkLenAndCopy(JNIEnv* env, const jbyteArray& addr, int len, void* dst) +{ + if (env->GetArrayLength(addr) != len) { + return false; + } + env->GetByteArrayRegion(addr, 0, len, reinterpret_cast(dst)); + return true; +} + +static void android_net_utils_addArpEntry(JNIEnv *env, jobject thiz, jbyteArray ethAddr, + jbyteArray ipv4Addr, jstring ifname, jobject javaFd) +{ + struct arpreq req = {}; + struct sockaddr_in& netAddrStruct = *reinterpret_cast(&req.arp_pa); + struct sockaddr& ethAddrStruct = req.arp_ha; + + ethAddrStruct.sa_family = ARPHRD_ETHER; + if (!checkLenAndCopy(env, ethAddr, ETH_ALEN, ethAddrStruct.sa_data)) { + jniThrowException(env, "java/io/IOException", "Invalid ethAddr length"); + return; + } + + netAddrStruct.sin_family = AF_INET; + if (!checkLenAndCopy(env, ipv4Addr, sizeof(in_addr), &netAddrStruct.sin_addr)) { + jniThrowException(env, "java/io/IOException", "Invalid ipv4Addr length"); + return; + } + + int ifLen = env->GetStringLength(ifname); + // IFNAMSIZ includes the terminating NULL character + if (ifLen >= IFNAMSIZ) { + jniThrowException(env, "java/io/IOException", "ifname too long"); + return; + } + env->GetStringUTFRegion(ifname, 0, ifLen, req.arp_dev); + + req.arp_flags = ATF_COM; // Completed entry (ha valid) + int fd = jniGetFDFromFileDescriptor(env, javaFd); + if (fd < 0) { + jniThrowExceptionFmt(env, "java/io/IOException", "Invalid file descriptor"); + return; + } + // See also: man 7 arp + if (ioctl(fd, SIOCSARP, &req)) { + jniThrowExceptionFmt(env, "java/io/IOException", "ioctl error: %s", strerror(errno)); + return; + } +} + // ---------------------------------------------------------------------------- @@ -337,6 +386,7 @@ static const JNINativeMethod gNetworkUtilMethods[] = { { "bindSocketToNetwork", "(II)I", (void*) android_net_utils_bindSocketToNetwork }, { "protectFromVpn", "(I)Z", (void*)android_net_utils_protectFromVpn }, { "queryUserAccess", "(II)Z", (void*)android_net_utils_queryUserAccess }, + { "addArpEntry", "([B[BLjava/lang/String;Ljava/io/FileDescriptor;)V", (void*) android_net_utils_addArpEntry }, { "attachDhcpFilter", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_attachDhcpFilter }, { "attachRaFilter", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_attachRaFilter }, { "attachControlPacketFilter", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_attachControlPacketFilter }, From 1885805aac5091ed7b734524292b7d7a75e347bb Mon Sep 17 00:00:00 2001 From: Remi NGUYEN VAN Date: Wed, 4 Jul 2018 12:58:10 +0900 Subject: [PATCH 4/6] Add fields to DHCP packets for server use-case Also add DhcpReleasePacket Test: runtest -x DhcpPacketTest.java, manual: still obtains IP Bug: b/109584964 Change-Id: I19e68e8857646555ea56995880979a8a722757d7 --- .../android/net/dhcp/DhcpDiscoverPacket.java | 15 ++- .../java/android/net/dhcp/DhcpNakPacket.java | 16 ++- .../net/java/android/net/dhcp/DhcpPacket.java | 102 ++++++++++++------ .../android/net/dhcp/DhcpReleasePacket.java | 58 ++++++++++ .../android/net/dhcp/DhcpRequestPacket.java | 6 +- 5 files changed, 150 insertions(+), 47 deletions(-) create mode 100644 services/net/java/android/net/dhcp/DhcpReleasePacket.java diff --git a/services/net/java/android/net/dhcp/DhcpDiscoverPacket.java b/services/net/java/android/net/dhcp/DhcpDiscoverPacket.java index 91e6bd6469d80..11f2b6118e24f 100644 --- a/services/net/java/android/net/dhcp/DhcpDiscoverPacket.java +++ b/services/net/java/android/net/dhcp/DhcpDiscoverPacket.java @@ -23,11 +23,18 @@ import java.nio.ByteBuffer; * This class implements the DHCP-DISCOVER packet. */ class DhcpDiscoverPacket extends DhcpPacket { + /** + * The IP address of the client which sent this packet. + */ + final Inet4Address mSrcIp; + /** * Generates a DISCOVER packet with the specified parameters. */ - DhcpDiscoverPacket(int transId, short secs, byte[] clientMac, boolean broadcast) { - super(transId, secs, INADDR_ANY, INADDR_ANY, INADDR_ANY, INADDR_ANY, clientMac, broadcast); + DhcpDiscoverPacket(int transId, short secs, Inet4Address relayIp, byte[] clientMac, + boolean broadcast, Inet4Address srcIp) { + super(transId, secs, INADDR_ANY, INADDR_ANY, INADDR_ANY, relayIp, clientMac, broadcast); + mSrcIp = srcIp; } public String toString() { @@ -41,8 +48,8 @@ class DhcpDiscoverPacket extends DhcpPacket { */ public ByteBuffer buildPacket(int encap, short destUdp, short srcUdp) { ByteBuffer result = ByteBuffer.allocate(MAX_LENGTH); - fillInPacket(encap, INADDR_BROADCAST, INADDR_ANY, destUdp, - srcUdp, result, DHCP_BOOTREQUEST, mBroadcast); + fillInPacket(encap, INADDR_BROADCAST, mSrcIp, destUdp, srcUdp, result, DHCP_BOOTREQUEST, + mBroadcast); result.flip(); return result; } diff --git a/services/net/java/android/net/dhcp/DhcpNakPacket.java b/services/net/java/android/net/dhcp/DhcpNakPacket.java index 6458232b8fa0c..ef9af527d0634 100644 --- a/services/net/java/android/net/dhcp/DhcpNakPacket.java +++ b/services/net/java/android/net/dhcp/DhcpNakPacket.java @@ -26,11 +26,9 @@ class DhcpNakPacket extends DhcpPacket { /** * Generates a NAK packet with the specified parameters. */ - DhcpNakPacket(int transId, short secs, Inet4Address clientIp, Inet4Address yourIp, - Inet4Address nextIp, Inet4Address relayIp, - byte[] clientMac) { - super(transId, secs, INADDR_ANY, INADDR_ANY, nextIp, relayIp, - clientMac, false); + DhcpNakPacket(int transId, short secs, Inet4Address nextIp, Inet4Address relayIp, + byte[] clientMac, boolean broadcast) { + super(transId, secs, INADDR_ANY, INADDR_ANY, nextIp, relayIp, clientMac, broadcast); } public String toString() { @@ -43,11 +41,11 @@ class DhcpNakPacket extends DhcpPacket { */ public ByteBuffer buildPacket(int encap, short destUdp, short srcUdp) { ByteBuffer result = ByteBuffer.allocate(MAX_LENGTH); - Inet4Address destIp = mClientIp; - Inet4Address srcIp = mYourIp; + // Constructor does not set values for layers <= 3: use empty values + Inet4Address destIp = INADDR_ANY; + Inet4Address srcIp = INADDR_ANY; - fillInPacket(encap, destIp, srcIp, destUdp, srcUdp, result, - DHCP_BOOTREPLY, mBroadcast); + fillInPacket(encap, destIp, srcIp, destUdp, srcUdp, result, DHCP_BOOTREPLY, mBroadcast); result.flip(); return result; } diff --git a/services/net/java/android/net/dhcp/DhcpPacket.java b/services/net/java/android/net/dhcp/DhcpPacket.java index d90a4a235972f..888821a8ad928 100644 --- a/services/net/java/android/net/dhcp/DhcpPacket.java +++ b/services/net/java/android/net/dhcp/DhcpPacket.java @@ -1,5 +1,6 @@ package android.net.dhcp; +import android.annotation.Nullable; import android.net.DhcpResults; import android.net.LinkAddress; import android.net.NetworkUtils; @@ -204,6 +205,7 @@ public abstract class DhcpPacket { protected static final byte DHCP_MESSAGE_TYPE_DECLINE = 4; protected static final byte DHCP_MESSAGE_TYPE_ACK = 5; protected static final byte DHCP_MESSAGE_TYPE_NAK = 6; + protected static final byte DHCP_MESSAGE_TYPE_RELEASE = 7; protected static final byte DHCP_MESSAGE_TYPE_INFORM = 8; /** @@ -252,6 +254,7 @@ public abstract class DhcpPacket { * DHCP Optional Type: DHCP Client Identifier */ protected static final byte DHCP_CLIENT_IDENTIFIER = 61; + protected byte[] mClientId; /** * DHCP zero-length option code: pad @@ -281,7 +284,7 @@ public abstract class DhcpPacket { protected final Inet4Address mClientIp; protected final Inet4Address mYourIp; private final Inet4Address mNextIp; - private final Inet4Address mRelayIp; + protected final Inet4Address mRelayIp; /** * Does the client request a broadcast response? @@ -338,13 +341,28 @@ public abstract class DhcpPacket { return mClientMac; } + // TODO: refactor DhcpClient to set clientId when constructing packets and remove + // hasExplicitClientId logic /** - * Returns the client ID. This follows RFC 2132 and is based on the hardware address. + * Returns whether a client ID was set in the options for this packet. + */ + public boolean hasExplicitClientId() { + return mClientId != null; + } + + /** + * Returns the client ID. If not set explicitly, this follows RFC 2132 and creates a client ID + * based on the hardware address. */ public byte[] getClientId() { - byte[] clientId = new byte[mClientMac.length + 1]; - clientId[0] = CLIENT_ID_ETHER; - System.arraycopy(mClientMac, 0, clientId, 1, mClientMac.length); + final byte[] clientId; + if (hasExplicitClientId()) { + clientId = Arrays.copyOf(mClientId, mClientId.length); + } else { + clientId = new byte[mClientMac.length + 1]; + clientId[0] = CLIENT_ID_ETHER; + System.arraycopy(mClientMac, 0, clientId, 1, mClientMac.length); + } return clientId; } @@ -531,8 +549,10 @@ public abstract class DhcpPacket { /** * Adds an optional parameter containing an array of bytes. + * + *

This method is a no-op if the payload argument is null. */ - protected static void addTlv(ByteBuffer buf, byte type, byte[] payload) { + protected static void addTlv(ByteBuffer buf, byte type, @Nullable byte[] payload) { if (payload != null) { if (payload.length > MAX_OPTION_LEN) { throw new IllegalArgumentException("DHCP option too long: " @@ -546,8 +566,10 @@ public abstract class DhcpPacket { /** * Adds an optional parameter containing an IP address. + * + *

This method is a no-op if the address argument is null. */ - protected static void addTlv(ByteBuffer buf, byte type, Inet4Address addr) { + protected static void addTlv(ByteBuffer buf, byte type, @Nullable Inet4Address addr) { if (addr != null) { addTlv(buf, type, addr.getAddress()); } @@ -555,8 +577,10 @@ public abstract class DhcpPacket { /** * Adds an optional parameter containing a list of IP addresses. + * + *

This method is a no-op if the addresses argument is null or empty. */ - protected static void addTlv(ByteBuffer buf, byte type, List addrs) { + protected static void addTlv(ByteBuffer buf, byte type, @Nullable List addrs) { if (addrs == null || addrs.size() == 0) return; int optionLen = 4 * addrs.size(); @@ -574,9 +598,11 @@ public abstract class DhcpPacket { } /** - * Adds an optional parameter containing a short integer + * Adds an optional parameter containing a short integer. + * + *

This method is a no-op if the value argument is null. */ - protected static void addTlv(ByteBuffer buf, byte type, Short value) { + protected static void addTlv(ByteBuffer buf, byte type, @Nullable Short value) { if (value != null) { buf.put(type); buf.put((byte) 2); @@ -585,9 +611,11 @@ public abstract class DhcpPacket { } /** - * Adds an optional parameter containing a simple integer + * Adds an optional parameter containing a simple integer. + * + *

This method is a no-op if the value argument is null. */ - protected static void addTlv(ByteBuffer buf, byte type, Integer value) { + protected static void addTlv(ByteBuffer buf, byte type, @Nullable Integer value) { if (value != null) { buf.put(type); buf.put((byte) 4); @@ -597,12 +625,16 @@ public abstract class DhcpPacket { /** * Adds an optional parameter containing an ASCII string. + * + *

This method is a no-op if the string argument is null. */ - protected static void addTlv(ByteBuffer buf, byte type, String str) { - try { - addTlv(buf, type, str.getBytes("US-ASCII")); - } catch (UnsupportedEncodingException e) { - throw new IllegalArgumentException("String is not US-ASCII: " + str); + protected static void addTlv(ByteBuffer buf, byte type, @Nullable String str) { + if (str != null) { + try { + addTlv(buf, type, str.getBytes("US-ASCII")); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("String is not US-ASCII: " + str); + } } } @@ -740,6 +772,7 @@ public abstract class DhcpPacket { Inet4Address nextIp; Inet4Address relayIp; byte[] clientMac; + byte[] clientId = null; List dnsServers = new ArrayList<>(); List gateways = new ArrayList<>(); // aka router Inet4Address serverIdentifier = null; @@ -1038,8 +1071,8 @@ public abstract class DhcpPacket { throw new ParseException(DhcpErrorEvent.DHCP_NO_MSG_TYPE, "No DHCP message type option"); case DHCP_MESSAGE_TYPE_DISCOVER: - newPacket = new DhcpDiscoverPacket( - transactionId, secs, clientMac, broadcast); + newPacket = new DhcpDiscoverPacket(transactionId, secs, relayIp, clientMac, + broadcast, ipSrc); break; case DHCP_MESSAGE_TYPE_OFFER: newPacket = new DhcpOfferPacket( @@ -1047,7 +1080,7 @@ public abstract class DhcpPacket { break; case DHCP_MESSAGE_TYPE_REQUEST: newPacket = new DhcpRequestPacket( - transactionId, secs, clientIp, clientMac, broadcast); + transactionId, secs, clientIp, relayIp, clientMac, broadcast); break; case DHCP_MESSAGE_TYPE_DECLINE: newPacket = new DhcpDeclinePacket( @@ -1060,8 +1093,15 @@ public abstract class DhcpPacket { break; case DHCP_MESSAGE_TYPE_NAK: newPacket = new DhcpNakPacket( - transactionId, secs, clientIp, yourIp, nextIp, relayIp, - clientMac); + transactionId, secs, nextIp, relayIp, clientMac, broadcast); + break; + case DHCP_MESSAGE_TYPE_RELEASE: + if (serverIdentifier == null) { + throw new ParseException(DhcpErrorEvent.MISC_ERROR, + "DHCPRELEASE without server identifier"); + } + newPacket = new DhcpReleasePacket( + transactionId, serverIdentifier, clientIp, relayIp, clientMac); break; case DHCP_MESSAGE_TYPE_INFORM: newPacket = new DhcpInformPacket( @@ -1074,6 +1114,7 @@ public abstract class DhcpPacket { } newPacket.mBroadcastAddress = bcAddr; + newPacket.mClientId = clientId; newPacket.mDnsServers = dnsServers; newPacket.mDomainName = domainName; newPacket.mGateways = gateways; @@ -1173,8 +1214,8 @@ public abstract class DhcpPacket { */ public static ByteBuffer buildDiscoverPacket(int encap, int transactionId, short secs, byte[] clientMac, boolean broadcast, byte[] expectedParams) { - DhcpPacket pkt = new DhcpDiscoverPacket( - transactionId, secs, clientMac, broadcast); + DhcpPacket pkt = new DhcpDiscoverPacket(transactionId, secs, INADDR_ANY /* relayIp */, + clientMac, broadcast, INADDR_ANY /* srcIp */); pkt.mRequestedParams = expectedParams; return pkt.buildPacket(encap, DHCP_SERVER, DHCP_CLIENT); } @@ -1223,12 +1264,11 @@ public abstract class DhcpPacket { /** * Builds a DHCP-NAK packet from the required specified parameters. */ - public static ByteBuffer buildNakPacket(int encap, int transactionId, - Inet4Address serverIpAddr, Inet4Address clientIpAddr, byte[] mac) { - DhcpPacket pkt = new DhcpNakPacket(transactionId, (short) 0, clientIpAddr, - serverIpAddr, serverIpAddr, serverIpAddr, mac); - pkt.mMessage = "requested address not available"; - pkt.mRequestedIp = clientIpAddr; + public static ByteBuffer buildNakPacket(int encap, int transactionId, Inet4Address serverIpAddr, + byte[] mac, boolean broadcast, String message) { + DhcpPacket pkt = new DhcpNakPacket( + transactionId, (short) 0, serverIpAddr, serverIpAddr, mac, broadcast); + pkt.mMessage = message; return pkt.buildPacket(encap, DHCP_CLIENT, DHCP_SERVER); } @@ -1240,7 +1280,7 @@ public abstract class DhcpPacket { byte[] clientMac, Inet4Address requestedIpAddress, Inet4Address serverIdentifier, byte[] requestedParams, String hostName) { DhcpPacket pkt = new DhcpRequestPacket(transactionId, secs, clientIp, - clientMac, broadcast); + INADDR_ANY /* relayIp */, clientMac, broadcast); pkt.mRequestedIp = requestedIpAddress; pkt.mServerIdentifier = serverIdentifier; pkt.mHostName = hostName; diff --git a/services/net/java/android/net/dhcp/DhcpReleasePacket.java b/services/net/java/android/net/dhcp/DhcpReleasePacket.java new file mode 100644 index 0000000000000..39583032c20de --- /dev/null +++ b/services/net/java/android/net/dhcp/DhcpReleasePacket.java @@ -0,0 +1,58 @@ +/* + * 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 java.net.Inet4Address; +import java.nio.ByteBuffer; + +/** + * Implements DHCP-RELEASE + */ +class DhcpReleasePacket extends DhcpPacket { + + final Inet4Address mClientAddr; + + /** + * Generates a RELEASE packet with the specified parameters. + */ + public DhcpReleasePacket(int transId, Inet4Address serverId, Inet4Address clientAddr, + Inet4Address relayIp, byte[] clientMac) { + super(transId, (short)0, clientAddr, INADDR_ANY /* yourIp */, INADDR_ANY /* nextIp */, + relayIp, clientMac, false /* broadcast */); + mServerIdentifier = serverId; + mClientAddr = clientAddr; + } + + + @Override + public ByteBuffer buildPacket(int encap, short destUdp, short srcUdp) { + ByteBuffer result = ByteBuffer.allocate(MAX_LENGTH); + fillInPacket(encap, mServerIdentifier /* destIp */, mClientIp /* srcIp */, destUdp, srcUdp, + result, DHCP_BOOTREPLY, mBroadcast); + result.flip(); + return result; + } + + @Override + void finishPacket(ByteBuffer buffer) { + addTlv(buffer, DHCP_MESSAGE_TYPE, DHCP_MESSAGE_TYPE_RELEASE); + addTlv(buffer, DHCP_CLIENT_IDENTIFIER, getClientId()); + addTlv(buffer, DHCP_SERVER_IDENTIFIER, mServerIdentifier); + addCommonClientTlvs(buffer); + addTlvEnd(buffer); + } +} diff --git a/services/net/java/android/net/dhcp/DhcpRequestPacket.java b/services/net/java/android/net/dhcp/DhcpRequestPacket.java index 4f9aa01151ca2..231d04576c28c 100644 --- a/services/net/java/android/net/dhcp/DhcpRequestPacket.java +++ b/services/net/java/android/net/dhcp/DhcpRequestPacket.java @@ -28,9 +28,9 @@ class DhcpRequestPacket extends DhcpPacket { /** * Generates a REQUEST packet with the specified parameters. */ - DhcpRequestPacket(int transId, short secs, Inet4Address clientIp, byte[] clientMac, - boolean broadcast) { - super(transId, secs, clientIp, INADDR_ANY, INADDR_ANY, INADDR_ANY, clientMac, broadcast); + DhcpRequestPacket(int transId, short secs, Inet4Address clientIp, Inet4Address relayIp, + byte[] clientMac, boolean broadcast) { + super(transId, secs, clientIp, INADDR_ANY, INADDR_ANY, relayIp, clientMac, broadcast); } public String toString() { From a420b57a6b6682f48af56eb1dcdac2fa07924e7e Mon Sep 17 00:00:00 2001 From: Remi NGUYEN VAN Date: Wed, 4 Jul 2018 15:09:42 +0900 Subject: [PATCH 5/6] Add DhcpServingParams Those parameters will be used to start DhcpServer or update its configuration. Test: runtest DhcpServingParamsTest.java Bug: b/109584964 Change-Id: Id8d3dcf62d66dcb02accffa8d8500e30f07af452 --- core/java/android/net/NetworkUtils.java | 28 +- .../android/net/dhcp/DhcpLeaseRepository.java | 2 +- .../android/net/dhcp/DhcpServingParams.java | 262 ++++++++++++++++++ .../android/net/util/NetworkConstants.java | 4 +- .../java/android/net/NetworkUtilsTest.java | 43 ++- .../net/dhcp/DhcpServingParamsTest.java | 177 ++++++++++++ 6 files changed, 510 insertions(+), 6 deletions(-) create mode 100644 services/net/java/android/net/dhcp/DhcpServingParams.java create mode 100644 tests/net/java/android/net/dhcp/DhcpServingParamsTest.java diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java index 451435c909977..34e9476b3e08f 100644 --- a/core/java/android/net/NetworkUtils.java +++ b/core/java/android/net/NetworkUtils.java @@ -161,7 +161,7 @@ public class NetworkUtils { * @param hostAddress an int coding for an IPv4 address, where higher-order int byte is * lower-order IPv4 address byte */ - public static InetAddress intToInet4AddressHTL(int hostAddress) { + public static Inet4Address intToInet4AddressHTL(int hostAddress) { return intToInet4AddressHTH(Integer.reverseBytes(hostAddress)); } @@ -169,14 +169,14 @@ public class NetworkUtils { * Convert a IPv4 address from an integer to an InetAddress (0x01020304 -> 1.2.3.4) * @param hostAddress an int coding for an IPv4 address */ - public static InetAddress intToInet4AddressHTH(int hostAddress) { + public static Inet4Address intToInet4AddressHTH(int hostAddress) { byte[] addressBytes = { (byte) (0xff & (hostAddress >> 24)), (byte) (0xff & (hostAddress >> 16)), (byte) (0xff & (hostAddress >> 8)), (byte) (0xff & hostAddress) }; try { - return InetAddress.getByAddress(addressBytes); + return (Inet4Address) InetAddress.getByAddress(addressBytes); } catch (UnknownHostException e) { throw new AssertionError(); } @@ -408,6 +408,28 @@ public class NetworkUtils { return new Pair(address, prefixLength); } + /** + * Get a prefix mask as Inet4Address for a given prefix length. + * + *

For example 20 -> 255.255.240.0 + */ + public static Inet4Address getPrefixMaskAsInet4Address(int prefixLength) + throws IllegalArgumentException { + return intToInet4AddressHTH(prefixLengthToV4NetmaskIntHTH(prefixLength)); + } + + /** + * Get the broadcast address for a given prefix. + * + *

For example 192.168.0.1/24 -> 192.168.0.255 + */ + public static Inet4Address getBroadcastAddress(Inet4Address addr, int prefixLength) + throws IllegalArgumentException { + final int intBroadcastAddr = inet4AddressToIntHTH(addr) + | ~prefixLengthToV4NetmaskIntHTH(prefixLength); + return intToInet4AddressHTH(intBroadcastAddr); + } + /** * Check if IP address type is consistent between two InetAddress. * @return true if both are the same type. False otherwise. diff --git a/services/net/java/android/net/dhcp/DhcpLeaseRepository.java b/services/net/java/android/net/dhcp/DhcpLeaseRepository.java index b7a55a47a6a6c..7e57c9f463503 100644 --- a/services/net/java/android/net/dhcp/DhcpLeaseRepository.java +++ b/services/net/java/android/net/dhcp/DhcpLeaseRepository.java @@ -512,7 +512,7 @@ class DhcpLeaseRepository { // 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 = (Inet4Address) intToInet4AddressHTH(intAddr); + final Inet4Address addr = intToInet4AddressHTH(intAddr); if (isAvailable(addr) && !mDeclinedAddrs.containsKey(addr)) { return new DhcpLease(clientId, hwAddr, addr, expTime, hostname); } diff --git a/services/net/java/android/net/dhcp/DhcpServingParams.java b/services/net/java/android/net/dhcp/DhcpServingParams.java new file mode 100644 index 0000000000000..ba9d116d0bd51 --- /dev/null +++ b/services/net/java/android/net/dhcp/DhcpServingParams.java @@ -0,0 +1,262 @@ +/* + * 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.NetworkUtils.getPrefixMaskAsInet4Address; +import static android.net.dhcp.DhcpPacket.INFINITE_LEASE; +import static android.net.util.NetworkConstants.IPV4_MAX_MTU; +import static android.net.util.NetworkConstants.IPV4_MIN_MTU; + +import static java.lang.Integer.toUnsignedLong; + +import android.annotation.NonNull; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.NetworkUtils; + +import java.net.Inet4Address; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Parameters used by the DhcpServer to serve requests. + * + *

Instances are immutable. Use {@link DhcpServingParams.Builder} to instantiate. + * @hide + */ +public class DhcpServingParams { + public static final int MTU_UNSET = 0; + public static final int MIN_PREFIX_LENGTH = 16; + public static final int MAX_PREFIX_LENGTH = 30; + + /** Server inet address and prefix to serve */ + @NonNull + public final LinkAddress serverAddr; + + /** + * Default routers to be advertised to DHCP clients. May be empty. + * This set is provided by {@link DhcpServingParams.Builder} and is immutable. + */ + @NonNull + public final Set defaultRouters; + + /** + * DNS servers to be advertised to DHCP clients. May be empty. + * This set is provided by {@link DhcpServingParams.Builder} and is immutable. + */ + @NonNull + public final Set dnsServers; + + /** + * Excluded addresses that the DHCP server is not allowed to assign to clients. + * This set is provided by {@link DhcpServingParams.Builder} and is immutable. + */ + @NonNull + public final Set excludedAddrs; + + // DHCP uses uint32. Use long for clearer code, and check range when building. + public final long dhcpLeaseTimeSecs; + public final int linkMtu; + + /** + * Checked exception thrown when some parameters used to build {@link DhcpServingParams} are + * missing or invalid. + */ + public static class InvalidParameterException extends Exception { + public InvalidParameterException(String message) { + super(message); + } + } + + private DhcpServingParams(@NonNull LinkAddress serverAddr, + @NonNull Set defaultRouters, + @NonNull Set dnsServers, @NonNull Set excludedAddrs, + long dhcpLeaseTimeSecs, int linkMtu) { + this.serverAddr = serverAddr; + this.defaultRouters = defaultRouters; + this.dnsServers = dnsServers; + this.excludedAddrs = excludedAddrs; + this.dhcpLeaseTimeSecs = dhcpLeaseTimeSecs; + this.linkMtu = linkMtu; + } + + @NonNull + public Inet4Address getServerInet4Addr() { + return (Inet4Address) serverAddr.getAddress(); + } + + /** + * Get the served prefix mask as an IPv4 address. + * + *

For example, if the served prefix is 192.168.42.0/24, this will return 255.255.255.0. + */ + @NonNull + public Inet4Address getPrefixMaskAsAddress() { + return getPrefixMaskAsInet4Address(serverAddr.getPrefixLength()); + } + + /** + * Get the server broadcast address. + * + *

For example, if the server {@link LinkAddress} is 192.168.42.1/24, this will return + * 192.168.42.255. + */ + @NonNull + public Inet4Address getBroadcastAddress() { + return NetworkUtils.getBroadcastAddress(getServerInet4Addr(), serverAddr.getPrefixLength()); + } + + /** + * Utility class to create new instances of {@link DhcpServingParams} while checking validity + * of the parameters. + */ + public static class Builder { + private LinkAddress serverAddr; + private Set defaultRouters; + private Set dnsServers; + private Set excludedAddrs; + private long dhcpLeaseTimeSecs; + private int linkMtu = MTU_UNSET; + + /** + * Set the server address and served prefix for the DHCP server. + * + *

This parameter is required. + */ + public Builder setServerAddr(@NonNull LinkAddress serverAddr) { + this.serverAddr = serverAddr; + return this; + } + + /** + * Set the default routers to be advertised to DHCP clients. + * + *

Each router must be inside the served prefix. This may be an empty set, but it must + * always be set explicitly before building the {@link DhcpServingParams}. + */ + public Builder setDefaultRouters(@NonNull Set defaultRouters) { + this.defaultRouters = defaultRouters; + return this; + } + + /** + * Set the DNS servers to be advertised to DHCP clients. + * + *

This may be an empty set, but it must always be set explicitly before building the + * {@link DhcpServingParams}. + */ + public Builder setDnsServers(@NonNull Set dnsServers) { + this.dnsServers = dnsServers; + return this; + } + + /** + * Set excluded addresses that the DHCP server is not allowed to assign to clients. + * + *

This parameter is optional. DNS servers and default routers are always excluded + * and do not need to be set here. + */ + public Builder setExcludedAddrs(@NonNull Set excludedAddrs) { + this.excludedAddrs = excludedAddrs; + return this; + } + + /** + * Set the lease time for leases assigned by the DHCP server. + * + *

This parameter is required. + */ + public Builder setDhcpLeaseTimeSecs(long dhcpLeaseTimeSecs) { + this.dhcpLeaseTimeSecs = dhcpLeaseTimeSecs; + return this; + } + + /** + * Set the link MTU to be advertised to DHCP clients. + * + *

If set to {@link #MTU_UNSET}, no MTU will be advertised to clients. This parameter + * is optional and defaults to {@link #MTU_UNSET}. + */ + public Builder setLinkMtu(int linkMtu) { + this.linkMtu = linkMtu; + return this; + } + + /** + * Create a new {@link DhcpServingParams} instance based on parameters set in the builder. + * + *

This method has no side-effects. If it does not throw, a valid + * {@link DhcpServingParams} is returned. + * @return The constructed parameters. + * @throws InvalidParameterException At least one parameter is missing or invalid. + */ + @NonNull + public DhcpServingParams build() throws InvalidParameterException { + if (serverAddr == null) { + throw new InvalidParameterException("Missing serverAddr"); + } + if (defaultRouters == null) { + throw new InvalidParameterException("Missing defaultRouters"); + } + if (dnsServers == null) { + // Empty set is OK, but enforce explicitly setting it + throw new InvalidParameterException("Missing dnsServers"); + } + if (dhcpLeaseTimeSecs <= 0 || dhcpLeaseTimeSecs > toUnsignedLong(INFINITE_LEASE)) { + throw new InvalidParameterException("Invalid lease time: " + dhcpLeaseTimeSecs); + } + if (linkMtu != MTU_UNSET && (linkMtu < IPV4_MIN_MTU || linkMtu > IPV4_MAX_MTU)) { + throw new InvalidParameterException("Invalid link MTU: " + linkMtu); + } + if (!serverAddr.isIPv4()) { + throw new InvalidParameterException("serverAddr must be IPv4"); + } + if (serverAddr.getPrefixLength() < MIN_PREFIX_LENGTH + || serverAddr.getPrefixLength() > MAX_PREFIX_LENGTH) { + throw new InvalidParameterException("Prefix length is not in supported range"); + } + + final IpPrefix prefix = makeIpPrefix(serverAddr); + for (Inet4Address addr : defaultRouters) { + if (!prefix.contains(addr)) { + throw new InvalidParameterException(String.format( + "Default router %s is not in server prefix %s", addr, serverAddr)); + } + } + + final Set excl = new HashSet<>(); + if (excludedAddrs != null) { + excl.addAll(excludedAddrs); + } + excl.add((Inet4Address) serverAddr.getAddress()); + excl.addAll(defaultRouters); + excl.addAll(dnsServers); + + return new DhcpServingParams(serverAddr, + Collections.unmodifiableSet(new HashSet<>(defaultRouters)), + Collections.unmodifiableSet(new HashSet<>(dnsServers)), + Collections.unmodifiableSet(excl), + dhcpLeaseTimeSecs, linkMtu); + } + } + + @NonNull + static IpPrefix makeIpPrefix(@NonNull LinkAddress addr) { + return new IpPrefix(addr.getAddress(), addr.getPrefixLength()); + } +} diff --git a/services/net/java/android/net/util/NetworkConstants.java b/services/net/java/android/net/util/NetworkConstants.java index de04fd0ced04d..3defe56939f5e 100644 --- a/services/net/java/android/net/util/NetworkConstants.java +++ b/services/net/java/android/net/util/NetworkConstants.java @@ -77,10 +77,12 @@ public final class NetworkConstants { /** * IPv4 constants. * - * See als: + * See also: * - https://tools.ietf.org/html/rfc791 */ public static final int IPV4_HEADER_MIN_LEN = 20; + public static final int IPV4_MIN_MTU = 68; + public static final int IPV4_MAX_MTU = 65_535; public static final int IPV4_IHL_MASK = 0xf; public static final int IPV4_FLAGS_OFFSET = 6; public static final int IPV4_FRAGMENT_MASK = 0x1fff; diff --git a/tests/net/java/android/net/NetworkUtilsTest.java b/tests/net/java/android/net/NetworkUtilsTest.java index 2b172dac4865e..3452819835f5b 100644 --- a/tests/net/java/android/net/NetworkUtilsTest.java +++ b/tests/net/java/android/net/NetworkUtilsTest.java @@ -24,6 +24,8 @@ import static android.net.NetworkUtils.intToInet4AddressHTL; import static android.net.NetworkUtils.netmaskToPrefixLength; import static android.net.NetworkUtils.prefixLengthToV4NetmaskIntHTH; import static android.net.NetworkUtils.prefixLengthToV4NetmaskIntHTL; +import static android.net.NetworkUtils.getBroadcastAddress; +import static android.net.NetworkUtils.getPrefixMaskAsInet4Address; import static junit.framework.Assert.assertEquals; @@ -125,7 +127,6 @@ public class NetworkUtilsTest { assertInvalidNetworkMask(IPv4Address("255.255.0.255")); } - @Test public void testPrefixLengthToV4NetmaskIntHTL() { assertEquals(0, prefixLengthToV4NetmaskIntHTL(0)); @@ -266,4 +267,44 @@ public class NetworkUtilsTest { assertEquals(BigInteger.valueOf(7l - 4 + 4 + 16 + 65536), NetworkUtils.routedIPv6AddressCount(set)); } + + @Test + public void testGetPrefixMaskAsAddress() { + assertEquals("255.255.240.0", getPrefixMaskAsInet4Address(20).getHostAddress()); + assertEquals("255.0.0.0", getPrefixMaskAsInet4Address(8).getHostAddress()); + assertEquals("0.0.0.0", getPrefixMaskAsInet4Address(0).getHostAddress()); + assertEquals("255.255.255.255", getPrefixMaskAsInet4Address(32).getHostAddress()); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetPrefixMaskAsAddress_PrefixTooLarge() { + getPrefixMaskAsInet4Address(33); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetPrefixMaskAsAddress_NegativePrefix() { + getPrefixMaskAsInet4Address(-1); + } + + @Test + public void testGetBroadcastAddress() { + assertEquals("192.168.15.255", + getBroadcastAddress(IPv4Address("192.168.0.123"), 20).getHostAddress()); + assertEquals("192.255.255.255", + getBroadcastAddress(IPv4Address("192.168.0.123"), 8).getHostAddress()); + assertEquals("192.168.0.123", + getBroadcastAddress(IPv4Address("192.168.0.123"), 32).getHostAddress()); + assertEquals("255.255.255.255", + getBroadcastAddress(IPv4Address("192.168.0.123"), 0).getHostAddress()); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetBroadcastAddress_PrefixTooLarge() { + getBroadcastAddress(IPv4Address("192.168.0.123"), 33); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetBroadcastAddress_NegativePrefix() { + getBroadcastAddress(IPv4Address("192.168.0.123"), -1); + } } diff --git a/tests/net/java/android/net/dhcp/DhcpServingParamsTest.java b/tests/net/java/android/net/dhcp/DhcpServingParamsTest.java new file mode 100644 index 0000000000000..dfa09a9f205c6 --- /dev/null +++ b/tests/net/java/android/net/dhcp/DhcpServingParamsTest.java @@ -0,0 +1,177 @@ +/* + * 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.DhcpServingParams.MTU_UNSET; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import static java.net.InetAddress.parseNumericAddress; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.LinkAddress; +import android.net.dhcp.DhcpServingParams.InvalidParameterException; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.Inet4Address; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class DhcpServingParamsTest { + @NonNull + private DhcpServingParams.Builder mBuilder; + + private static final Set TEST_DEFAULT_ROUTERS = new HashSet<>( + Arrays.asList(parseAddr("192.168.0.123"), parseAddr("192.168.0.124"))); + private static final long TEST_LEASE_TIME_SECS = 3600L; + private static final Set TEST_DNS_SERVERS = new HashSet<>( + Arrays.asList(parseAddr("192.168.0.126"), parseAddr("192.168.0.127"))); + private static final Inet4Address TEST_SERVER_ADDR = parseAddr("192.168.0.2"); + private static final LinkAddress TEST_LINKADDR = new LinkAddress(TEST_SERVER_ADDR, 20); + private static final int TEST_MTU = 1500; + private static final Set TEST_EXCLUDED_ADDRS = new HashSet<>( + Arrays.asList(parseAddr("192.168.0.200"), parseAddr("192.168.0.201"))); + + @Before + public void setUp() { + mBuilder = new DhcpServingParams.Builder() + .setDefaultRouters(TEST_DEFAULT_ROUTERS) + .setDhcpLeaseTimeSecs(TEST_LEASE_TIME_SECS) + .setDnsServers(TEST_DNS_SERVERS) + .setServerAddr(TEST_LINKADDR) + .setLinkMtu(TEST_MTU) + .setExcludedAddrs(TEST_EXCLUDED_ADDRS); + } + + @Test + public void testBuild_Immutable() throws InvalidParameterException { + final Set routers = new HashSet<>(TEST_DEFAULT_ROUTERS); + final Set dnsServers = new HashSet<>(TEST_DNS_SERVERS); + final Set excludedAddrs = new HashSet<>(TEST_EXCLUDED_ADDRS); + + final DhcpServingParams params = mBuilder + .setDefaultRouters(routers) + .setDnsServers(dnsServers) + .setExcludedAddrs(excludedAddrs) + .build(); + + // Modifications to source objects should not affect builder or final parameters + final Inet4Address addedAddr = parseAddr("192.168.0.223"); + routers.add(addedAddr); + dnsServers.add(addedAddr); + excludedAddrs.add(addedAddr); + + assertEquals(TEST_DEFAULT_ROUTERS, params.defaultRouters); + assertEquals(TEST_LEASE_TIME_SECS, params.dhcpLeaseTimeSecs); + assertEquals(TEST_DNS_SERVERS, params.dnsServers); + assertEquals(TEST_LINKADDR, params.serverAddr); + assertEquals(TEST_MTU, params.linkMtu); + + assertContains(params.excludedAddrs, TEST_EXCLUDED_ADDRS); + assertContains(params.excludedAddrs, TEST_DEFAULT_ROUTERS); + assertContains(params.excludedAddrs, TEST_DNS_SERVERS); + assertContains(params.excludedAddrs, TEST_SERVER_ADDR); + + assertFalse("excludedAddrs should not contain " + addedAddr, + params.excludedAddrs.contains(addedAddr)); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_NegativeLeaseTime() throws InvalidParameterException { + mBuilder.setDhcpLeaseTimeSecs(-1).build(); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_LeaseTimeTooLarge() throws InvalidParameterException { + // Set lease time larger than max value for uint32 + mBuilder.setDhcpLeaseTimeSecs(1L << 32).build(); + } + + @Test + public void testBuild_InfiniteLeaseTime() throws InvalidParameterException { + final long infiniteLeaseTime = 0xffffffffL; + final DhcpServingParams params = mBuilder + .setDhcpLeaseTimeSecs(infiniteLeaseTime).build(); + assertEquals(infiniteLeaseTime, params.dhcpLeaseTimeSecs); + assertTrue(params.dhcpLeaseTimeSecs > 0L); + } + + @Test + public void testBuild_UnsetMtu() throws InvalidParameterException { + final DhcpServingParams params = mBuilder.setLinkMtu(MTU_UNSET).build(); + assertEquals(MTU_UNSET, params.linkMtu); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_MtuTooSmall() throws InvalidParameterException { + mBuilder.setLinkMtu(20).build(); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_MtuTooLarge() throws InvalidParameterException { + mBuilder.setLinkMtu(65_536).build(); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_IPv6Addr() throws InvalidParameterException { + mBuilder.setServerAddr(new LinkAddress(parseNumericAddress("fe80::1111"), 120)).build(); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_PrefixTooLarge() throws InvalidParameterException { + mBuilder.setServerAddr(new LinkAddress(TEST_SERVER_ADDR, 15)).build(); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_PrefixTooSmall() throws InvalidParameterException { + mBuilder.setDefaultRouters(Collections.singleton(parseAddr("192.168.0.254"))) + .setServerAddr(new LinkAddress(TEST_SERVER_ADDR, 31)) + .build(); + } + + @Test(expected = InvalidParameterException.class) + public void testBuild_RouterNotInPrefix() throws InvalidParameterException { + mBuilder.setDefaultRouters(Collections.singleton(parseAddr("192.168.254.254"))).build(); + } + + private static void assertContains(@NonNull Set set, @NonNull Set subset) { + for (final T elem : subset) { + assertContains(set, elem); + } + } + + private static void assertContains(@NonNull Set set, @Nullable T elem) { + assertTrue("Set does not contain " + elem, set.contains(elem)); + } + + @NonNull + private static Inet4Address parseAddr(@NonNull String inet4Addr) { + return (Inet4Address) parseNumericAddress(inet4Addr); + } +} From bd0cc31c12c7e209421abd42f61f200d0c3e6bef Mon Sep 17 00:00:00 2001 From: Remi NGUYEN VAN Date: Mon, 13 Aug 2018 11:26:49 +0900 Subject: [PATCH 6/6] Add a SharedLog method to log errors w/ stacktrace Test: manual: ran code using this method and checked log. Change-Id: I2cea553ae0dd8a8f2f629718e92aa642c62eb120 --- services/net/java/android/net/util/SharedLog.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/services/net/java/android/net/util/SharedLog.java b/services/net/java/android/net/util/SharedLog.java index bbd3d13efbd6b..f7bf393f367b1 100644 --- a/services/net/java/android/net/util/SharedLog.java +++ b/services/net/java/android/net/util/SharedLog.java @@ -16,6 +16,7 @@ package android.net.util; +import android.annotation.NonNull; import android.text.TextUtils; import android.util.LocalLog; import android.util.Log; @@ -90,6 +91,13 @@ public class SharedLog { Log.e(mTag, record(Category.ERROR, msg)); } + /** + * Log an error due to an exception, with the exception stacktrace. + */ + public void e(@NonNull String msg, @NonNull Throwable e) { + Log.e(mTag, record(Category.ERROR, msg + ": " + e.getMessage()), e); + } + public void i(String msg) { Log.i(mTag, record(Category.NONE, msg)); }