diff options
author | Remi NGUYEN VAN <reminv@google.com> | 2018-12-04 12:13:09 +0900 |
---|---|---|
committer | Remi NGUYEN VAN <reminv@google.com> | 2019-01-09 15:42:16 +0900 |
commit | 057bf20e86b62e744f823818c2741f659106e04d (patch) | |
tree | d22f2ea10c09e3925ed7d5d7aed47034b5c09054 /src | |
parent | 952842e5447f311e52ea90b1d7ea9bb15a396e73 (diff) |
Move DhcpServer to NetworkStack app
Test: atest FrameworksNetTests && atest NetworkStackTests
Bug: b/112869080
Change-Id: I96c40e63e9ceb37b67705bdd4d120307e114715b
Diffstat (limited to 'src')
-rw-r--r-- | src/android/net/dhcp/DhcpLease.java | 153 | ||||
-rw-r--r-- | src/android/net/dhcp/DhcpLeaseRepository.java | 545 | ||||
-rw-r--r-- | src/android/net/dhcp/DhcpPacketListener.java | 88 | ||||
-rw-r--r-- | src/android/net/dhcp/DhcpServer.java | 651 | ||||
-rw-r--r-- | src/android/net/dhcp/DhcpServingParams.java | 370 | ||||
-rw-r--r-- | src/android/net/util/SharedLog.java | 197 | ||||
-rw-r--r-- | src/com/android/server/NetworkStackService.java | 49 | ||||
-rw-r--r-- | src/com/android/server/util/NetworkStackConstants.java | 45 | ||||
-rw-r--r-- | src/com/android/server/util/PermissionUtil.java | 42 |
9 files changed, 2128 insertions, 12 deletions
diff --git a/src/android/net/dhcp/DhcpLease.java b/src/android/net/dhcp/DhcpLease.java new file mode 100644 index 0000000..6849cfa --- /dev/null +++ b/src/android/net/dhcp/DhcpLease.java @@ -0,0 +1,153 @@ +/* + * 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; + } + + /** + * Get the clientId associated with this lease, if any. + * + * <p>If the lease is not associated to a clientId, this returns null. + */ + @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. + * + * <p>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)); + } + + /** + * Determine whether this lease matches a client with the specified parameters. + * @param clientId clientId of the client if any, or null otherwise. + * @param hwAddr Hardware address of the client. + */ + 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); + } + + static String inet4AddrToString(@Nullable Inet4Address addr) { + return (addr == null) ? "null" : addr.getHostAddress(); + } + + @Override + public String toString() { + return String.format("clientId: %s, hwAddr: %s, netAddr: %s, expTime: %d, hostname: %s", + clientIdToString(mClientId), mHwAddr.toString(), inet4AddrToString(mNetAddr), + mExpTime, mHostname); + } +} diff --git a/src/android/net/dhcp/DhcpLeaseRepository.java b/src/android/net/dhcp/DhcpLeaseRepository.java new file mode 100644 index 0000000..0d298de --- /dev/null +++ b/src/android/net/dhcp/DhcpLeaseRepository.java @@ -0,0 +1,545 @@ +/* + * 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.dhcp.DhcpLease.inet4AddrToString; + +import static com.android.server.util.NetworkStackConstants.IPV4_ADDR_BITS; + +import static java.lang.Math.min; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.IpPrefix; +import android.net.MacAddress; +import android.net.dhcp.DhcpServer.Clock; +import android.net.util.SharedLog; +import android.util.ArrayMap; + +import java.net.Inet4Address; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.Function; + +/** + * A repository managing IPv4 address assignments through DHCPv4. + * + * <p>This class is not thread-safe. All public methods should be called on a common thread or + * use some synchronization mechanism. + * + * <p>Methods are optimized for a small number of allocated leases, assuming that most of the time + * only 2~10 addresses will be allocated, which is the common case. Managing a large number of + * addresses is supported but will be slower: some operations have complexity in O(num_leases). + * @hide + */ +class DhcpLeaseRepository { + public static final byte[] CLIENTID_UNSPEC = null; + public static final Inet4Address INETADDR_UNSPEC = null; + + @NonNull + private final SharedLog mLog; + @NonNull + private final Clock mClock; + + @NonNull + private IpPrefix mPrefix; + @NonNull + private Set<Inet4Address> mReservedAddrs; + private int mSubnetAddr; + private int mSubnetMask; + private int mNumAddresses; + private long mLeaseTimeMs; + + /** + * Next timestamp when committed or declined leases should be checked for expired ones. This + * will always be lower than or equal to the time for the first lease to expire: it's OK not to + * update this when removing entries, but it must always be updated when adding/updating. + */ + private long mNextExpirationCheck = EXPIRATION_NEVER; + + static class DhcpLeaseException extends Exception { + DhcpLeaseException(String message) { + super(message); + } + } + + static class OutOfAddressesException extends DhcpLeaseException { + OutOfAddressesException(String message) { + super(message); + } + } + + static class InvalidAddressException extends DhcpLeaseException { + InvalidAddressException(String message) { + super(message); + } + } + + static class InvalidSubnetException extends DhcpLeaseException { + InvalidSubnetException(String message) { + super(message); + } + } + + /** + * Leases by IP address + */ + private final ArrayMap<Inet4Address, DhcpLease> mCommittedLeases = new ArrayMap<>(); + + /** + * Map address -> expiration timestamp in ms. Addresses are guaranteed to be valid as defined + * by {@link #isValidAddress(Inet4Address)}, but are not necessarily otherwise available for + * assignment. + */ + private final LinkedHashMap<Inet4Address, Long> mDeclinedAddrs = new LinkedHashMap<>(); + + DhcpLeaseRepository(@NonNull IpPrefix prefix, @NonNull Set<Inet4Address> reservedAddrs, + long leaseTimeMs, @NonNull SharedLog log, @NonNull Clock clock) { + updateParams(prefix, reservedAddrs, leaseTimeMs); + mLog = log; + mClock = clock; + } + + public void updateParams(@NonNull IpPrefix prefix, @NonNull Set<Inet4Address> reservedAddrs, + long leaseTimeMs) { + mPrefix = prefix; + mReservedAddrs = Collections.unmodifiableSet(new HashSet<>(reservedAddrs)); + mSubnetMask = prefixLengthToV4NetmaskIntHTH(prefix.getPrefixLength()); + mSubnetAddr = inet4AddressToIntHTH((Inet4Address) prefix.getAddress()) & mSubnetMask; + mNumAddresses = 1 << (IPV4_ADDR_BITS - prefix.getPrefixLength()); + mLeaseTimeMs = leaseTimeMs; + + cleanMap(mCommittedLeases); + cleanMap(mDeclinedAddrs); + } + + /** + * From a map keyed by {@link Inet4Address}, remove entries where the key is invalid (as + * specified by {@link #isValidAddress(Inet4Address)}), or is a reserved address. + */ + private <T> void cleanMap(Map<Inet4Address, T> map) { + final Iterator<Entry<Inet4Address, T>> it = map.entrySet().iterator(); + while (it.hasNext()) { + final Inet4Address addr = it.next().getKey(); + if (!isValidAddress(addr) || mReservedAddrs.contains(addr)) { + it.remove(); + } + } + } + + /** + * Get a DHCP offer, to reply to a DHCPDISCOVER. Follows RFC2131 #4.3.1. + * + * @param clientId Client identifier option if specified, or {@link #CLIENTID_UNSPEC} + * @param relayAddr Internet address of the relay (giaddr), can be {@link Inet4Address#ANY} + * @param reqAddr Requested address by the client (option 50), or {@link #INETADDR_UNSPEC} + * @param hostname Client-provided hostname, or {@link DhcpLease#HOSTNAME_NONE} + * @throws OutOfAddressesException The server does not have any available address + * @throws InvalidSubnetException The lease was requested from an unsupported subnet + */ + @NonNull + public DhcpLease getOffer(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, + @NonNull Inet4Address relayAddr, @Nullable Inet4Address reqAddr, + @Nullable String hostname) throws OutOfAddressesException, InvalidSubnetException { + final long currentTime = mClock.elapsedRealtime(); + final long expTime = currentTime + mLeaseTimeMs; + + removeExpiredLeases(currentTime); + checkValidRelayAddr(relayAddr); + + final DhcpLease currentLease = findByClient(clientId, hwAddr); + final DhcpLease newLease; + if (currentLease != null) { + newLease = currentLease.renewedLease(expTime, hostname); + mLog.log("Offering extended lease " + newLease); + // Do not update lease time in the map: the offer is not committed yet. + } else if (reqAddr != null && isValidAddress(reqAddr) && isAvailable(reqAddr)) { + newLease = new DhcpLease(clientId, hwAddr, reqAddr, expTime, hostname); + mLog.log("Offering requested lease " + newLease); + } else { + newLease = makeNewOffer(clientId, hwAddr, expTime, hostname); + mLog.log("Offering new generated lease " + newLease); + } + return newLease; + } + + private void checkValidRelayAddr(@Nullable Inet4Address relayAddr) + throws InvalidSubnetException { + // As per #4.3.1, addresses are assigned based on the relay address if present. This + // implementation only assigns addresses if the relayAddr is inside our configured subnet. + // This also applies when the client requested a specific address for consistency between + // requests, and with older behavior. + if (isIpAddrOutsidePrefix(mPrefix, relayAddr)) { + throw new InvalidSubnetException("Lease requested by relay from outside of subnet"); + } + } + + private static boolean isIpAddrOutsidePrefix(@NonNull IpPrefix prefix, + @Nullable Inet4Address addr) { + return addr != null && !addr.equals(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. + * + * <p>This method always succeeds and commits the lease if it does not throw, and has no side + * effects if it throws. + * + * @param clientId Client identifier option if specified, or {@link #CLIENTID_UNSPEC} + * @param reqAddr Requested address by the client (option 50), or {@link #INETADDR_UNSPEC} + * @param sidSet Whether the server identifier was set in the request + * @return The newly created or renewed lease + * @throws InvalidAddressException The client provided an address that conflicts with its + * current configuration, or other committed/reserved leases. + */ + @NonNull + public DhcpLease requestLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, + @NonNull Inet4Address clientAddr, @NonNull Inet4Address relayAddr, + @Nullable Inet4Address reqAddr, boolean sidSet, @Nullable String hostname) + throws InvalidAddressException, InvalidSubnetException { + final long currentTime = mClock.elapsedRealtime(); + removeExpiredLeases(currentTime); + checkValidRelayAddr(relayAddr); + final DhcpLease assignedLease = findByClient(clientId, hwAddr); + + final Inet4Address leaseAddr = reqAddr != null ? reqAddr : clientAddr; + if (assignedLease != null) { + if (sidSet && reqAddr != null) { + // Client in SELECTING state; remove any current lease before creating a new one. + mCommittedLeases.remove(assignedLease.getNetAddr()); + } else if (!assignedLease.getNetAddr().equals(leaseAddr)) { + // reqAddr null (RENEWING/REBINDING): client renewing its own lease for clientAddr. + // reqAddr set with sid not set (INIT-REBOOT): client verifying configuration. + // In both cases, throw if clientAddr or reqAddr does not match the known lease. + throw new InvalidAddressException("Incorrect address for client in " + + (reqAddr != null ? "INIT-REBOOT" : "RENEWING/REBINDING")); + } + } + + // In the init-reboot case, RFC2131 #4.3.2 says that the server must not reply if + // assignedLease == null, but dnsmasq will let the client use the requested address if + // available, when configured with --dhcp-authoritative. This is preferable to avoid issues + // if the server lost the lease DB: the client would not get a reply because the server + // does not know their lease. + // Similarly in RENEWING/REBINDING state, create a lease when possible if the + // client-provided lease is unknown. + final DhcpLease lease = + checkClientAndMakeLease(clientId, hwAddr, leaseAddr, hostname, currentTime); + mLog.logf("DHCPREQUEST assignedLease %s, reqAddr=%s, sidSet=%s: created/renewed lease %s", + assignedLease, inet4AddrToString(reqAddr), sidSet, lease); + return lease; + } + + /** + * Check that the client can request the specified address, make or renew the lease if yes, and + * commit it. + * + * <p>This method always succeeds and returns the lease if it does not throw, and has no + * side-effect if it throws. + * + * @return The newly created or renewed, committed lease + * @throws InvalidAddressException The client provided an address that conflicts with its + * current configuration, or other committed/reserved leases. + */ + private DhcpLease checkClientAndMakeLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, + @NonNull Inet4Address addr, @Nullable String hostname, long currentTime) + throws InvalidAddressException { + final long expTime = currentTime + mLeaseTimeMs; + final DhcpLease currentLease = mCommittedLeases.getOrDefault(addr, null); + if (currentLease != null && !currentLease.matchesClient(clientId, hwAddr)) { + throw new InvalidAddressException("Address in use"); + } + + final DhcpLease lease; + if (currentLease == null) { + if (isValidAddress(addr) && !mReservedAddrs.contains(addr)) { + lease = new DhcpLease(clientId, hwAddr, addr, expTime, hostname); + } else { + throw new InvalidAddressException("Lease not found and address unavailable"); + } + } else { + lease = currentLease.renewedLease(expTime, hostname); + } + commitLease(lease); + return lease; + } + + private void commitLease(@NonNull DhcpLease lease) { + mCommittedLeases.put(lease.getNetAddr(), lease); + maybeUpdateEarliestExpiration(lease.getExpTime()); + } + + /** + * Delete a committed lease from the repository. + * + * @return true if a lease matching parameters was found. + */ + public boolean releaseLease(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, + @NonNull Inet4Address addr) { + final DhcpLease currentLease = mCommittedLeases.getOrDefault(addr, null); + if (currentLease == null) { + mLog.w("Could not release unknown lease for " + inet4AddrToString(addr)); + return false; + } + if (currentLease.matchesClient(clientId, hwAddr)) { + mCommittedLeases.remove(addr); + mLog.log("Released lease " + currentLease); + return true; + } + mLog.w(String.format("Not releasing lease %s: does not match client (cid %s, hwAddr %s)", + currentLease, DhcpLease.clientIdToString(clientId), hwAddr)); + return false; + } + + public void markLeaseDeclined(@NonNull Inet4Address addr) { + if (mDeclinedAddrs.containsKey(addr) || !isValidAddress(addr)) { + mLog.logf("Not marking %s as declined: already declined or not assignable", + inet4AddrToString(addr)); + return; + } + final long expTime = mClock.elapsedRealtime() + mLeaseTimeMs; + mDeclinedAddrs.put(addr, expTime); + mLog.logf("Marked %s as declined expiring %d", inet4AddrToString(addr), expTime); + maybeUpdateEarliestExpiration(expTime); + } + + /** + * Get the list of currently valid committed leases in the repository. + */ + @NonNull + public List<DhcpLease> getCommittedLeases() { + removeExpiredLeases(mClock.elapsedRealtime()); + return new ArrayList<>(mCommittedLeases.values()); + } + + /** + * Get the set of addresses that have been marked as declined in the repository. + */ + @NonNull + public Set<Inet4Address> getDeclinedAddresses() { + removeExpiredLeases(mClock.elapsedRealtime()); + return new HashSet<>(mDeclinedAddrs.keySet()); + } + + /** + * Given the expiration time of a new committed lease or declined address, update + * {@link #mNextExpirationCheck} so it stays lower than or equal to the time for the first lease + * to expire. + */ + private void maybeUpdateEarliestExpiration(long expTime) { + if (expTime < mNextExpirationCheck) { + mNextExpirationCheck = expTime; + } + } + + /** + * Remove expired entries from a map keyed by {@link Inet4Address}. + * + * @param tag Type of lease in the map, for logging + * @param getExpTime Functor returning the expiration time for an object in the map. + * Must not return null. + * @return The lowest expiration time among entries remaining in the map + */ + private <T> long removeExpired(long currentTime, @NonNull Map<Inet4Address, T> map, + @NonNull String tag, @NonNull Function<T, Long> getExpTime) { + final Iterator<Entry<Inet4Address, T>> it = map.entrySet().iterator(); + long firstExpiration = EXPIRATION_NEVER; + while (it.hasNext()) { + final Entry<Inet4Address, T> lease = it.next(); + final long expTime = getExpTime.apply(lease.getValue()); + if (expTime <= currentTime) { + mLog.logf("Removing expired %s lease for %s (expTime=%s, currentTime=%s)", + tag, lease.getKey(), expTime, currentTime); + it.remove(); + } else { + firstExpiration = min(firstExpiration, expTime); + } + } + return firstExpiration; + } + + /** + * Go through committed and declined leases and remove the expired ones. + */ + private void removeExpiredLeases(long currentTime) { + if (currentTime < mNextExpirationCheck) { + return; + } + + final long commExp = removeExpired( + currentTime, mCommittedLeases, "committed", DhcpLease::getExpTime); + final long declExp = removeExpired( + currentTime, mDeclinedAddrs, "declined", Function.identity()); + + mNextExpirationCheck = min(commExp, declExp); + } + + private boolean isAvailable(@NonNull Inet4Address addr) { + return !mReservedAddrs.contains(addr) && !mCommittedLeases.containsKey(addr); + } + + /** + * Get the 0-based index of an address in the subnet. + * + * <p>Given ordering of addresses 5.6.7.8 < 5.6.7.9 < 5.6.8.0, the index on a subnet is defined + * so that the first address is 0, the second 1, etc. For example on a /16, 192.168.0.0 -> 0, + * 192.168.0.1 -> 1, 192.168.1.0 -> 256 + * + */ + private int getAddrIndex(int addr) { + return addr & ~mSubnetMask; + } + + private int getAddrByIndex(int index) { + return mSubnetAddr | index; + } + + /** + * Get a valid address starting from the supplied one. + * + * <p>This only checks that the address is numerically valid for assignment, not whether it is + * already in use. The return value is always inside the configured prefix, even if the supplied + * address is not. + * + * <p>If the provided address is valid, it is returned as-is. Otherwise, the next valid + * address (with the ordering in {@link #getAddrIndex(int)}) is returned. + */ + private int getValidAddress(int addr) { + final int lastByteMask = 0xff; + int addrIndex = getAddrIndex(addr); // 0-based index of the address in the subnet + + // Some OSes do not handle addresses in .255 or .0 correctly: avoid those. + final int lastByte = getAddrByIndex(addrIndex) & lastByteMask; + if (lastByte == lastByteMask) { + // Avoid .255 address, and .0 address that follows + addrIndex = (addrIndex + 2) % mNumAddresses; + } else if (lastByte == 0) { + // Avoid .0 address + addrIndex = (addrIndex + 1) % mNumAddresses; + } + + // Do not use first or last address of range + if (addrIndex == 0 || addrIndex == mNumAddresses - 1) { + // Always valid and not end of range since prefixLength is at most 30 in serving params + addrIndex = 1; + } + return getAddrByIndex(addrIndex); + } + + /** + * Returns whether the address is in the configured subnet and part of the assignable range. + */ + private boolean isValidAddress(Inet4Address addr) { + final int intAddr = inet4AddressToIntHTH(addr); + return getValidAddress(intAddr) == intAddr; + } + + private int getNextAddress(int addr) { + final int addrIndex = getAddrIndex(addr); + final int nextAddress = getAddrByIndex((addrIndex + 1) % mNumAddresses); + return getValidAddress(nextAddress); + } + + /** + * Calculate a first candidate address for a client by hashing the hardware address. + * + * <p>This will be a valid address as checked by {@link #getValidAddress(int)}, but may be + * in use. + * + * @return An IPv4 address encoded as 32-bit int + */ + private int getFirstClientAddress(MacAddress hwAddr) { + // This follows dnsmasq behavior. Advantages are: clients will often get the same + // offers for different DISCOVER even if the lease was not yet accepted or has expired, + // and address generation will generally not need to loop through many allocated addresses + // until it finds a free one. + int hash = 0; + for (byte b : hwAddr.toByteArray()) { + hash += b + (b << 8) + (b << 16); + } + // This implementation will not always result in the same IPs as dnsmasq would give out in + // Android <= P, because it includes invalid and reserved addresses in mNumAddresses while + // the configured ranges for dnsmasq did not. + final int addrIndex = hash % mNumAddresses; + return getValidAddress(getAddrByIndex(addrIndex)); + } + + /** + * Create a lease that can be offered to respond to a client DISCOVER. + * + * <p>This method always succeeds and returns the lease if it does not throw. If no non-declined + * address is available, it will try to offer the oldest declined address if valid. + * + * @throws OutOfAddressesException The server has no address left to offer + */ + private DhcpLease makeNewOffer(@Nullable byte[] clientId, @NonNull MacAddress hwAddr, + long expTime, @Nullable String hostname) throws OutOfAddressesException { + int intAddr = getFirstClientAddress(hwAddr); + // Loop until a free address is found, or there are no more addresses. + // There is slightly less than this many usable addresses, but some extra looping is OK + for (int i = 0; i < mNumAddresses; i++) { + final Inet4Address addr = intToInet4AddressHTH(intAddr); + if (isAvailable(addr) && !mDeclinedAddrs.containsKey(addr)) { + return new DhcpLease(clientId, hwAddr, addr, expTime, hostname); + } + intAddr = getNextAddress(intAddr); + } + + // Try freeing DECLINEd addresses if out of addresses. + final Iterator<Inet4Address> it = mDeclinedAddrs.keySet().iterator(); + while (it.hasNext()) { + final Inet4Address addr = it.next(); + it.remove(); + mLog.logf("Out of addresses in address pool: dropped declined addr %s", + inet4AddrToString(addr)); + // isValidAddress() is always verified for entries in mDeclinedAddrs. + // However declined addresses may have been requested (typically by the machine that was + // already using the address) after being declined. + if (isAvailable(addr)) { + return new DhcpLease(clientId, hwAddr, addr, expTime, hostname); + } + } + + throw new OutOfAddressesException("No address available for offer"); + } +} diff --git a/src/android/net/dhcp/DhcpPacketListener.java b/src/android/net/dhcp/DhcpPacketListener.java new file mode 100644 index 0000000..dce8b61 --- /dev/null +++ b/src/android/net/dhcp/DhcpPacketListener.java @@ -0,0 +1,88 @@ +/* + * 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.util.FdEventsReader; +import android.os.Handler; +import android.system.Os; + +import java.io.FileDescriptor; +import java.net.Inet4Address; +import java.net.InetSocketAddress; + +/** + * A {@link FdEventsReader} to receive and parse {@link DhcpPacket}. + * @hide + */ +abstract class DhcpPacketListener extends FdEventsReader<DhcpPacketListener.Payload> { + static final class Payload { + protected final byte[] mBytes = new byte[DhcpPacket.MAX_LENGTH]; + protected Inet4Address mSrcAddr; + protected int mSrcPort; + } + + DhcpPacketListener(@NonNull Handler handler) { + super(handler, new Payload()); + } + + @Override + protected int recvBufSize(@NonNull Payload buffer) { + return buffer.mBytes.length; + } + + @Override + protected final void handlePacket(@NonNull Payload recvbuf, int length) { + if (recvbuf.mSrcAddr == null) { + return; + } + + try { + final DhcpPacket packet = DhcpPacket.decodeFullPacket(recvbuf.mBytes, length, + DhcpPacket.ENCAP_BOOTP); + onReceive(packet, recvbuf.mSrcAddr, recvbuf.mSrcPort); + } catch (DhcpPacket.ParseException e) { + logParseError(recvbuf.mBytes, length, e); + } + } + + @Override + protected int readPacket(@NonNull FileDescriptor fd, @NonNull Payload packetBuffer) + throws Exception { + final InetSocketAddress addr = new InetSocketAddress(); + final int read = Os.recvfrom( + fd, packetBuffer.mBytes, 0, packetBuffer.mBytes.length, 0 /* flags */, addr); + + // Buffers with null srcAddr will be dropped in handlePacket() + packetBuffer.mSrcAddr = inet4AddrOrNull(addr); + packetBuffer.mSrcPort = addr.getPort(); + return read; + } + + @Nullable + private static Inet4Address inet4AddrOrNull(@NonNull InetSocketAddress addr) { + return addr.getAddress() instanceof Inet4Address + ? (Inet4Address) addr.getAddress() + : null; + } + + protected abstract void onReceive(@NonNull DhcpPacket packet, @NonNull Inet4Address srcAddr, + int srcPort); + protected abstract void logParseError(@NonNull byte[] packet, int length, + @NonNull DhcpPacket.ParseException e); +} diff --git a/src/android/net/dhcp/DhcpServer.java b/src/android/net/dhcp/DhcpServer.java new file mode 100644 index 0000000..14e2936 --- /dev/null +++ b/src/android/net/dhcp/DhcpServer.java @@ -0,0 +1,651 @@ +/* + * 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.getBroadcastAddress; +import static android.net.NetworkUtils.getPrefixMaskAsInet4Address; +import static android.net.TrafficStats.TAG_SYSTEM_DHCP_SERVER; +import static android.net.dhcp.DhcpPacket.DHCP_CLIENT; +import static android.net.dhcp.DhcpPacket.DHCP_HOST_NAME; +import static android.net.dhcp.DhcpPacket.DHCP_SERVER; +import static android.net.dhcp.DhcpPacket.ENCAP_BOOTP; +import static android.net.dhcp.IDhcpServer.STATUS_INVALID_ARGUMENT; +import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS; +import static android.system.OsConstants.AF_INET; +import static android.system.OsConstants.IPPROTO_UDP; +import static android.system.OsConstants.SOCK_DGRAM; +import static android.system.OsConstants.SOL_SOCKET; +import static android.system.OsConstants.SO_BINDTODEVICE; +import static android.system.OsConstants.SO_BROADCAST; +import static android.system.OsConstants.SO_REUSEADDR; + +import static com.android.server.util.NetworkStackConstants.INFINITE_LEASE; +import static com.android.server.util.PermissionUtil.checkNetworkStackCallingPermission; + +import static java.lang.Integer.toUnsignedLong; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.INetworkStackStatusCallback; +import android.net.MacAddress; +import android.net.NetworkUtils; +import android.net.TrafficStats; +import android.net.util.SharedLog; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.SystemClock; +import android.system.ErrnoException; +import android.system.Os; +import android.text.TextUtils; +import android.util.Pair; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.HexDump; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; + +/** + * A DHCPv4 server. + * + * <p>This server listens for and responds to packets on a single interface. It considers itself + * authoritative for all leases on the subnet, which means that DHCP requests for unknown leases of + * unknown hosts receive a reply instead of being ignored. + * + * <p>The server is single-threaded (including send/receive operations): all internal operations are + * done on the provided {@link Looper}. Public methods are thread-safe and will schedule operations + * on the looper asynchronously. + * @hide + */ +public class DhcpServer extends IDhcpServer.Stub { + private static final String REPO_TAG = "Repository"; + + // Lease time to transmit to client instead of a negative time in case a lease expired before + // the server could send it (if the server process is suspended for example). + private static final int EXPIRED_FALLBACK_LEASE_TIME_SECS = 120; + + private static final int CMD_START_DHCP_SERVER = 1; + private static final int CMD_STOP_DHCP_SERVER = 2; + private static final int CMD_UPDATE_PARAMS = 3; + + @NonNull + private final HandlerThread mHandlerThread; + @NonNull + private final String mIfName; + @NonNull + private final DhcpLeaseRepository mLeaseRepo; + @NonNull + private final SharedLog mLog; + @NonNull + private final Dependencies mDeps; + @NonNull + private final Clock mClock; + + @Nullable + private volatile ServerHandler mHandler; + + // Accessed only on the handler thread + @Nullable + private DhcpPacketListener mPacketListener; + @Nullable + private FileDescriptor mSocket; + @NonNull + private DhcpServingParams mServingParams; + + /** + * Clock to be used by DhcpServer to track time for lease expiration. + * + * <p>The clock should track time as may be measured by clients obtaining a lease. It does not + * need to be monotonous across restarts of the server as long as leases are cleared when the + * server is stopped. + */ + public static class Clock { + /** + * @see SystemClock#elapsedRealtime() + */ + public long elapsedRealtime() { + return SystemClock.elapsedRealtime(); + } + } + + /** + * Dependencies for the DhcpServer. Useful to be mocked in tests. + */ + public interface Dependencies { + /** + * Send a packet to the specified datagram socket. + * + * @param fd File descriptor of the socket. + * @param buffer Data to be sent. + * @param dst Destination address of the packet. + */ + void sendPacket(@NonNull FileDescriptor fd, @NonNull ByteBuffer buffer, + @NonNull InetAddress dst) throws ErrnoException, IOException; + + /** + * Create a DhcpLeaseRepository for the server. + * @param servingParams Parameters used to serve DHCP requests. + * @param log Log to be used by the repository. + * @param clock Clock that the repository must use to track time. + */ + DhcpLeaseRepository makeLeaseRepository(@NonNull DhcpServingParams servingParams, + @NonNull SharedLog log, @NonNull Clock clock); + + /** + * Create a packet listener that will send packets to be processed. + */ + DhcpPacketListener makePacketListener(); + + /** + * Create a clock that the server will use to track time. + */ + Clock makeClock(); + + /** + * Add an entry to the ARP cache table. + * @param fd Datagram socket file descriptor that must use the new entry. + */ + void addArpEntry(@NonNull Inet4Address ipv4Addr, @NonNull MacAddress ethAddr, + @NonNull String ifname, @NonNull FileDescriptor fd) throws IOException; + + /** + * Verify that the caller is allowed to call public methods on DhcpServer. + * @throws SecurityException The caller is not allowed to call public methods on DhcpServer. + */ + void checkCaller() throws SecurityException; + } + + private class DependenciesImpl implements Dependencies { + @Override + public void sendPacket(@NonNull FileDescriptor fd, @NonNull ByteBuffer buffer, + @NonNull InetAddress dst) throws ErrnoException, IOException { + Os.sendto(fd, buffer, 0, dst, DhcpPacket.DHCP_CLIENT); + } + + @Override + public DhcpLeaseRepository makeLeaseRepository(@NonNull DhcpServingParams servingParams, + @NonNull SharedLog log, @NonNull Clock clock) { + return new DhcpLeaseRepository( + DhcpServingParams.makeIpPrefix(servingParams.serverAddr), + servingParams.excludedAddrs, + servingParams.dhcpLeaseTimeSecs * 1000, log.forSubComponent(REPO_TAG), clock); + } + + @Override + public DhcpPacketListener makePacketListener() { + return new PacketListener(); + } + + @Override + public Clock makeClock() { + return new Clock(); + } + + @Override + public void addArpEntry(@NonNull Inet4Address ipv4Addr, @NonNull MacAddress ethAddr, + @NonNull String ifname, @NonNull FileDescriptor fd) throws IOException { + NetworkUtils.addArpEntry(ipv4Addr, ethAddr, ifname, fd); + } + + @Override + public void checkCaller() { + checkNetworkStackCallingPermission(); + } + } + + private static class MalformedPacketException extends Exception { + MalformedPacketException(String message, Throwable t) { + super(message, t); + } + } + + public DhcpServer(@NonNull String ifName, + @NonNull DhcpServingParams params, @NonNull SharedLog log) { + this(new HandlerThread(DhcpServer.class.getSimpleName() + "." + ifName), + ifName, params, log, null); + } + + @VisibleForTesting + DhcpServer(@NonNull HandlerThread handlerThread, @NonNull String ifName, + @NonNull DhcpServingParams params, @NonNull SharedLog log, + @Nullable Dependencies deps) { + if (deps == null) { + deps = new DependenciesImpl(); + } + mHandlerThread = handlerThread; + mIfName = ifName; + mServingParams = params; + mLog = log; + mDeps = deps; + mClock = deps.makeClock(); + mLeaseRepo = deps.makeLeaseRepository(mServingParams, mLog, mClock); + } + + /** + * Start listening for and responding to packets. + * + * <p>It is not legal to call this method more than once; in particular the server cannot be + * restarted after being stopped. + */ + @Override + public void start(@Nullable INetworkStackStatusCallback cb) { + mDeps.checkCaller(); + mHandlerThread.start(); + mHandler = new ServerHandler(mHandlerThread.getLooper()); + sendMessage(CMD_START_DHCP_SERVER, cb); + } + + /** + * Update serving parameters. All subsequently received requests will be handled with the new + * parameters, and current leases that are incompatible with the new parameters are dropped. + */ + @Override + public void updateParams(@Nullable DhcpServingParamsParcel params, + @Nullable INetworkStackStatusCallback cb) throws RemoteException { + mDeps.checkCaller(); + final DhcpServingParams parsedParams; + try { + // throws InvalidParameterException with null params + parsedParams = DhcpServingParams.fromParcelableObject(params); + } catch (DhcpServingParams.InvalidParameterException e) { + mLog.e("Invalid parameters sent to DhcpServer", e); + if (cb != null) { + cb.onStatusAvailable(STATUS_INVALID_ARGUMENT); + } + return; + } + sendMessage(CMD_UPDATE_PARAMS, new Pair<>(parsedParams, cb)); + } + + /** + * Stop listening for packets. + * + * <p>As the server is stopped asynchronously, some packets may still be processed shortly after + * calling this method. + */ + @Override + public void stop(@Nullable INetworkStackStatusCallback cb) { + mDeps.checkCaller(); + sendMessage(CMD_STOP_DHCP_SERVER, cb); + } + + private void sendMessage(int what, @Nullable Object obj) { + if (mHandler == null) { + mLog.e("Attempting to send a command to stopped DhcpServer: " + what); + return; + } + mHandler.sendMessage(mHandler.obtainMessage(what, obj)); + } + + private class ServerHandler extends Handler { + ServerHandler(@NonNull Looper looper) { + super(looper); + } + + @Override + public void handleMessage(@NonNull Message msg) { + final INetworkStackStatusCallback cb; + switch (msg.what) { + case CMD_UPDATE_PARAMS: + final Pair<DhcpServingParams, INetworkStackStatusCallback> pair = + (Pair<DhcpServingParams, INetworkStackStatusCallback>) msg.obj; + final DhcpServingParams params = pair.first; + mServingParams = params; + mLeaseRepo.updateParams( + DhcpServingParams.makeIpPrefix(mServingParams.serverAddr), + params.excludedAddrs, + params.dhcpLeaseTimeSecs); + + cb = pair.second; + break; + case CMD_START_DHCP_SERVER: + mPacketListener = mDeps.makePacketListener(); + mPacketListener.start(); + cb = (INetworkStackStatusCallback) msg.obj; + break; + case CMD_STOP_DHCP_SERVER: + if (mPacketListener != null) { + mPacketListener.stop(); + mPacketListener = null; + } + mHandlerThread.quitSafely(); + cb = (INetworkStackStatusCallback) msg.obj; + break; + default: + return; + } + if (cb != null) { + try { + cb.onStatusAvailable(STATUS_SUCCESS); + } catch (RemoteException e) { + mLog.e("Could not send status back to caller", e); + } + } + } + } + + @VisibleForTesting + void processPacket(@NonNull DhcpPacket packet, int srcPort) { + final String packetType = packet.getClass().getSimpleName(); + if (srcPort != DHCP_CLIENT) { + mLog.logf("Ignored packet of type %s sent from client port %d", packetType, srcPort); + return; + } + + mLog.log("Received packet of type " + packetType); + final Inet4Address sid = packet.mServerIdentifier; + if (sid != null && !sid.equals(mServingParams.serverAddr.getAddress())) { + mLog.log("Packet ignored due to wrong server identifier: " + sid); + return; + } + + try { + if (packet instanceof DhcpDiscoverPacket) { + processDiscover((DhcpDiscoverPacket) packet); + } else if (packet instanceof DhcpRequestPacket) { + processRequest((DhcpRequestPacket) packet); + } else if (packet instanceof DhcpReleasePacket) { + processRelease((DhcpReleasePacket) packet); + } else { + mLog.e("Unknown packet type: " + packet.getClass().getSimpleName()); + } + } catch (MalformedPacketException e) { + // Not an internal error: only logging exception message, not stacktrace + mLog.e("Ignored malformed packet: " + e.getMessage()); + } + } + + private void logIgnoredPacketInvalidSubnet(DhcpLeaseRepository.InvalidSubnetException e) { + // Not an internal error: only logging exception message, not stacktrace + mLog.e("Ignored packet from invalid subnet: " + e.getMessage()); + } + + private void processDiscover(@NonNull DhcpDiscoverPacket packet) + throws MalformedPacketException { + final DhcpLease lease; + final MacAddress clientMac = getMacAddr(packet); + try { + lease = mLeaseRepo.getOffer(packet.getExplicitClientIdOrNull(), clientMac, + packet.mRelayIp, packet.mRequestedIp, packet.mHostName); + } catch (DhcpLeaseRepository.OutOfAddressesException e) { + transmitNak(packet, "Out of addresses to offer"); + return; + } catch (DhcpLeaseRepository.InvalidSubnetException e) { + logIgnoredPacketInvalidSubnet(e); + return; + } + + transmitOffer(packet, lease, clientMac); + } + + private void processRequest(@NonNull DhcpRequestPacket packet) throws MalformedPacketException { + // If set, packet SID matches with this server's ID as checked in processPacket(). + final boolean sidSet = packet.mServerIdentifier != null; + final DhcpLease lease; + final MacAddress clientMac = getMacAddr(packet); + try { + lease = mLeaseRepo.requestLease(packet.getExplicitClientIdOrNull(), clientMac, + packet.mClientIp, packet.mRelayIp, packet.mRequestedIp, sidSet, + packet.mHostName); + } catch (DhcpLeaseRepository.InvalidAddressException e) { + transmitNak(packet, "Invalid requested address"); + return; + } catch (DhcpLeaseRepository.InvalidSubnetException e) { + logIgnoredPacketInvalidSubnet(e); + return; + } + + transmitAck(packet, lease, clientMac); + } + + private void processRelease(@NonNull DhcpReleasePacket packet) + throws MalformedPacketException { + final byte[] clientId = packet.getExplicitClientIdOrNull(); + final MacAddress macAddr = getMacAddr(packet); + // Don't care about success (there is no ACK/NAK); logging is already done in the repository + mLeaseRepo.releaseLease(clientId, macAddr, packet.mClientIp); + } + + private Inet4Address getAckOrOfferDst(@NonNull DhcpPacket request, @NonNull DhcpLease lease, + boolean broadcastFlag) { + // Unless relayed or broadcast, send to client IP if already configured on the client, or to + // the lease address if the client has no configured address + if (!isEmpty(request.mRelayIp)) { + return request.mRelayIp; + } else if (broadcastFlag) { + return (Inet4Address) Inet4Address.ALL; + } else if (!isEmpty(request.mClientIp)) { + return request.mClientIp; + } else { + return lease.getNetAddr(); + } + } + + /** + * Determine whether the broadcast flag should be set in the BOOTP packet flags. This does not + * apply to NAK responses, which should always have it set. + */ + private static boolean getBroadcastFlag(@NonNull DhcpPacket request, @NonNull DhcpLease lease) { + // No broadcast flag if the client already has a configured IP to unicast to. RFC2131 #4.1 + // has some contradictions regarding broadcast behavior if a client already has an IP + // configured and sends a request with both ciaddr (renew/rebind) and the broadcast flag + // set. Sending a unicast response to ciaddr matches previous behavior and is more + // efficient. + // If the client has no configured IP, broadcast if requested by the client or if the lease + // address cannot be used to send a unicast reply either. + return isEmpty(request.mClientIp) && (request.mBroadcast || isEmpty(lease.getNetAddr())); + } + + /** + * Get the hostname from a lease if non-empty and requested in the incoming request. + * @param request The incoming request. + * @return The hostname, or null if not requested or empty. + */ + @Nullable + private static String getHostnameIfRequested(@NonNull DhcpPacket request, + @NonNull DhcpLease lease) { + return request.hasRequestedParam(DHCP_HOST_NAME) && !TextUtils.isEmpty(lease.getHostname()) + ? lease.getHostname() + : null; + } + + private boolean transmitOffer(@NonNull DhcpPacket request, @NonNull DhcpLease lease, + @NonNull MacAddress clientMac) { + final boolean broadcastFlag = getBroadcastFlag(request, lease); + final int timeout = getLeaseTimeout(lease); + final Inet4Address prefixMask = + getPrefixMaskAsInet4Address(mServingParams.serverAddr.getPrefixLength()); + final Inet4Address broadcastAddr = getBroadcastAddress( + mServingParams.getServerInet4Addr(), mServingParams.serverAddr.getPrefixLength()); + final String hostname = getHostnameIfRequested(request, lease); + final ByteBuffer offerPacket = DhcpPacket.buildOfferPacket( + ENCAP_BOOTP, request.mTransId, broadcastFlag, mServingParams.getServerInet4Addr(), + request.mRelayIp, lease.getNetAddr(), request.mClientMac, timeout, prefixMask, + broadcastAddr, new ArrayList<>(mServingParams.defaultRouters), + new ArrayList<>(mServingParams.dnsServers), + mServingParams.getServerInet4Addr(), null /* domainName */, hostname, + mServingParams.metered, (short) mServingParams.linkMtu); + + return transmitOfferOrAckPacket(offerPacket, request, lease, clientMac, broadcastFlag); + } + + private boolean transmitAck(@NonNull DhcpPacket request, @NonNull DhcpLease lease, + @NonNull MacAddress clientMac) { + // TODO: replace DhcpPacket's build methods with real builders and use common code with + // transmitOffer above + final boolean broadcastFlag = getBroadcastFlag(request, lease); + final int timeout = getLeaseTimeout(lease); + final String hostname = getHostnameIfRequested(request, lease); + final ByteBuffer ackPacket = DhcpPacket.buildAckPacket(ENCAP_BOOTP, request.mTransId, + broadcastFlag, mServingParams.getServerInet4Addr(), request.mRelayIp, + lease.getNetAddr(), request.mClientIp, request.mClientMac, timeout, + mServingParams.getPrefixMaskAsAddress(), mServingParams.getBroadcastAddress(), + new ArrayList<>(mServingParams.defaultRouters), + new ArrayList<>(mServingParams.dnsServers), + mServingParams.getServerInet4Addr(), null /* domainName */, hostname, + mServingParams.metered, (short) mServingParams.linkMtu); + + return transmitOfferOrAckPacket(ackPacket, request, lease, clientMac, broadcastFlag); + } + + private boolean transmitNak(DhcpPacket request, String message) { + mLog.w("Transmitting NAK: " + message); + // Always set broadcast flag for NAK: client may not have a correct IP + final ByteBuffer nakPacket = DhcpPacket.buildNakPacket( + ENCAP_BOOTP, request.mTransId, mServingParams.getServerInet4Addr(), + request.mRelayIp, request.mClientMac, true /* broadcast */, message); + + final Inet4Address dst = isEmpty(request.mRelayIp) + ? (Inet4Address) Inet4Address.ALL + : request.mRelayIp; + return transmitPacket(nakPacket, DhcpNakPacket.class.getSimpleName(), dst); + } + + private boolean transmitOfferOrAckPacket(@NonNull ByteBuffer buf, @NonNull DhcpPacket request, + @NonNull DhcpLease lease, @NonNull MacAddress clientMac, boolean broadcastFlag) { + mLog.logf("Transmitting %s with lease %s", request.getClass().getSimpleName(), lease); + // Client may not yet respond to ARP for the lease address, which may be the destination + // address. Add an entry to the ARP cache to save future ARP probes and make sure the + // packet reaches its destination. + if (!addArpEntry(clientMac, lease.getNetAddr())) { + // Logging for error already done + return false; + } + final Inet4Address dst = getAckOrOfferDst(request, lease, broadcastFlag); + return transmitPacket(buf, request.getClass().getSimpleName(), dst); + } + + private boolean transmitPacket(@NonNull ByteBuffer buf, @NonNull String packetTypeTag, + @NonNull Inet4Address dst) { + try { + mDeps.sendPacket(mSocket, buf, dst); + } catch (ErrnoException | IOException e) { + mLog.e("Can't send packet " + packetTypeTag, e); + return false; + } + return true; + } + + private boolean addArpEntry(@NonNull MacAddress macAddr, @NonNull Inet4Address inetAddr) { + try { + mDeps.addArpEntry(inetAddr, macAddr, mIfName, mSocket); + return true; + } catch (IOException e) { + mLog.e("Error adding client to ARP table", e); + return false; + } + } + + /** + * Get the remaining lease time in seconds, starting from {@link Clock#elapsedRealtime()}. + * + * <p>This is an unsigned 32-bit integer, so it cannot be read as a standard (signed) Java int. + * The return value is only intended to be used to populate the lease time field in a DHCP + * response, considering that lease time is an unsigned 32-bit integer field in DHCP packets. + * + * <p>Lease expiration times are tracked internally with millisecond precision: this method + * returns a rounded down value. + */ + private int getLeaseTimeout(@NonNull DhcpLease lease) { + final long remainingTimeSecs = (lease.getExpTime() - mClock.elapsedRealtime()) / 1000; + if (remainingTimeSecs < 0) { + mLog.e("Processing expired lease " + lease); + return EXPIRED_FALLBACK_LEASE_TIME_SECS; + } + + if (remainingTimeSecs >= toUnsignedLong(INFINITE_LEASE)) { + return INFINITE_LEASE; + } + + return (int) remainingTimeSecs; + } + + /** + * Get the client MAC address from a packet. + * + * @throws MalformedPacketException The address in the packet uses an unsupported format. + */ + @NonNull + private MacAddress getMacAddr(@NonNull DhcpPacket packet) throws MalformedPacketException { + try { + return MacAddress.fromBytes(packet.getClientMac()); + } catch (IllegalArgumentException e) { + final String message = "Invalid MAC address in packet: " + + HexDump.dumpHexString(packet.getClientMac()); + throw new MalformedPacketException(message, e); + } + } + + private static boolean isEmpty(@Nullable Inet4Address address) { + return address == null || Inet4Address.ANY.equals(address); + } + + private class PacketListener extends DhcpPacketListener { + PacketListener() { + super(mHandler); + } + + @Override + protected void onReceive(@NonNull DhcpPacket packet, @NonNull Inet4Address srcAddr, + int srcPort) { + processPacket(packet, srcPort); + } + + @Override + protected void logError(@NonNull String msg, Exception e) { + mLog.e("Error receiving packet: " + msg, e); + } + + @Override + protected void logParseError(@NonNull byte[] packet, int length, + @NonNull DhcpPacket.ParseException e) { + mLog.e("Error parsing packet", e); + } + + @Override + protected FileDescriptor createFd() { + // TODO: have and use an API to set a socket tag without going through the thread tag + final int oldTag = TrafficStats.getAndSetThreadStatsTag(TAG_SYSTEM_DHCP_SERVER); + try { + mSocket = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + Os.setsockoptInt(mSocket, SOL_SOCKET, SO_REUSEADDR, 1); + // SO_BINDTODEVICE actually takes a string. This works because the first member + // of struct ifreq is a NULL-terminated interface name. + // TODO: add a setsockoptString() + Os.setsockoptIfreq(mSocket, SOL_SOCKET, SO_BINDTODEVICE, mIfName); + Os.setsockoptInt(mSocket, SOL_SOCKET, SO_BROADCAST, 1); + Os.bind(mSocket, Inet4Address.ANY, DHCP_SERVER); + NetworkUtils.protectFromVpn(mSocket); + + return mSocket; + } catch (IOException | ErrnoException e) { + mLog.e("Error creating UDP socket", e); + DhcpServer.this.stop(null); + return null; + } finally { + TrafficStats.setThreadStatsTag(oldTag); + } + } + } +} diff --git a/src/android/net/dhcp/DhcpServingParams.java b/src/android/net/dhcp/DhcpServingParams.java new file mode 100644 index 0000000..f38888a --- /dev/null +++ b/src/android/net/dhcp/DhcpServingParams.java @@ -0,0 +1,370 @@ +/* + * 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.NetworkUtils.intToInet4AddressHTH; + +import static com.android.server.util.NetworkStackConstants.INFINITE_LEASE; +import static com.android.server.util.NetworkStackConstants.IPV4_MAX_MTU; +import static com.android.server.util.NetworkStackConstants.IPV4_MIN_MTU; + +import static java.lang.Integer.toUnsignedLong; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.IpPrefix; +import android.net.LinkAddress; +import android.net.NetworkUtils; + +import com.google.android.collect.Sets; + +import java.net.Inet4Address; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Parameters used by the DhcpServer to serve requests. + * + * <p>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<Inet4Address> 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<Inet4Address> 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<Inet4Address> excludedAddrs; + + // DHCP uses uint32. Use long for clearer code, and check range when building. + public final long dhcpLeaseTimeSecs; + public final int linkMtu; + + /** + * Indicates whether the DHCP server should send the ANDROID_METERED vendor-specific option. + */ + public final boolean metered; + + /** + * 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<Inet4Address> defaultRouters, + @NonNull Set<Inet4Address> dnsServers, @NonNull Set<Inet4Address> excludedAddrs, + long dhcpLeaseTimeSecs, int linkMtu, boolean metered) { + this.serverAddr = serverAddr; + this.defaultRouters = defaultRouters; + this.dnsServers = dnsServers; + this.excludedAddrs = excludedAddrs; + this.dhcpLeaseTimeSecs = dhcpLeaseTimeSecs; + this.linkMtu = linkMtu; + this.metered = metered; + } + + /** + * Create parameters from a stable AIDL-compatible parcel. + * @throws InvalidParameterException The parameters parcelable is null or invalid. + */ + public static DhcpServingParams fromParcelableObject(@Nullable DhcpServingParamsParcel parcel) + throws InvalidParameterException { + if (parcel == null) { + throw new InvalidParameterException("Null serving parameters"); + } + final LinkAddress serverAddr = new LinkAddress( + intToInet4AddressHTH(parcel.serverAddr), + parcel.serverAddrPrefixLength); + return new Builder() + .setServerAddr(serverAddr) + .setDefaultRouters(toInet4AddressSet(parcel.defaultRouters)) + .setDnsServers(toInet4AddressSet(parcel.dnsServers)) + .setExcludedAddrs(toInet4AddressSet(parcel.excludedAddrs)) + .setDhcpLeaseTimeSecs(parcel.dhcpLeaseTimeSecs) + .setLinkMtu(parcel.linkMtu) + .setMetered(parcel.metered) + .build(); + } + + private static Set<Inet4Address> toInet4AddressSet(@Nullable int[] addrs) { + if (addrs == null) { + return new HashSet<>(0); + } + + final HashSet<Inet4Address> res = new HashSet<>(); + for (int addr : addrs) { + res.add(intToInet4AddressHTH(addr)); + } + return res; + } + + @NonNull + public Inet4Address getServerInet4Addr() { + return (Inet4Address) serverAddr.getAddress(); + } + + /** + * Get the served prefix mask as an IPv4 address. + * + * <p>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. + * + * <p>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 mServerAddr; + private Set<Inet4Address> mDefaultRouters; + private Set<Inet4Address> mDnsServers; + private Set<Inet4Address> mExcludedAddrs; + private long mDhcpLeaseTimeSecs; + private int mLinkMtu = MTU_UNSET; + private boolean mMetered; + + /** + * Set the server address and served prefix for the DHCP server. + * + * <p>This parameter is required. + */ + public Builder setServerAddr(@NonNull LinkAddress serverAddr) { + this.mServerAddr = serverAddr; + return this; + } + + /** + * Set the default routers to be advertised to DHCP clients. + * + * <p>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<Inet4Address> defaultRouters) { + this.mDefaultRouters = defaultRouters; + return this; + } + + /** + * Set the default routers to be advertised to DHCP clients. + * + * <p>Each router must be inside the served prefix. This may be an empty list of routers, + * but it must always be set explicitly before building the {@link DhcpServingParams}. + */ + public Builder setDefaultRouters(@NonNull Inet4Address... defaultRouters) { + return setDefaultRouters(Sets.newArraySet(defaultRouters)); + } + + /** + * Convenience method to build the parameters with no default router. + * + * <p>Equivalent to calling {@link #setDefaultRouters(Inet4Address...)} with no address. + */ + public Builder withNoDefaultRouter() { + return setDefaultRouters(); + } + + /** + * Set the DNS servers to be advertised to DHCP clients. + * + * <p>This may be an empty set, but it must always be set explicitly before building the + * {@link DhcpServingParams}. + */ + public Builder setDnsServers(@NonNull Set<Inet4Address> dnsServers) { + this.mDnsServers = dnsServers; + return this; + } + + /** + * Set the DNS servers to be advertised to DHCP clients. + * + * <p>This may be an empty list of servers, but it must always be set explicitly before + * building the {@link DhcpServingParams}. + */ + public Builder setDnsServers(@NonNull Inet4Address... dnsServers) { + return setDnsServers(Sets.newArraySet(dnsServers)); + } + + /** + * Convenience method to build the parameters with no DNS server. + * + * <p>Equivalent to calling {@link #setDnsServers(Inet4Address...)} with no address. + */ + public Builder withNoDnsServer() { + return setDnsServers(); + } + + /** + * Set excluded addresses that the DHCP server is not allowed to assign to clients. + * + * <p>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<Inet4Address> excludedAddrs) { + this.mExcludedAddrs = excludedAddrs; + return this; + } + + /** + * Set excluded addresses that the DHCP server is not allowed to assign to clients. + * + * <p>This parameter is optional. DNS servers and default routers are always excluded + * and do not need to be set here. + */ + public Builder setExcludedAddrs(@NonNull Inet4Address... excludedAddrs) { + return setExcludedAddrs(Sets.newArraySet(excludedAddrs)); + } + + /** + * Set the lease time for leases assigned by the DHCP server. + * + * <p>This parameter is required. + */ + public Builder setDhcpLeaseTimeSecs(long dhcpLeaseTimeSecs) { + this.mDhcpLeaseTimeSecs = dhcpLeaseTimeSecs; + return this; + } + + /** + * Set the link MTU to be advertised to DHCP clients. + * + * <p>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.mLinkMtu = linkMtu; + return this; + } + + /** + * Set whether the DHCP server should send the ANDROID_METERED vendor-specific option. + * + * <p>If not set, the default value is false. + */ + public Builder setMetered(boolean metered) { + this.mMetered = metered; + return this; + } + + /** + * Create a new {@link DhcpServingParams} instance based on parameters set in the builder. + * + * <p>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 (mServerAddr == null) { + throw new InvalidParameterException("Missing serverAddr"); + } + if (mDefaultRouters == null) { + throw new InvalidParameterException("Missing defaultRouters"); + } + if (mDnsServers == null) { + // Empty set is OK, but enforce explicitly setting it + throw new InvalidParameterException("Missing dnsServers"); + } + if (mDhcpLeaseTimeSecs <= 0 || mDhcpLeaseTimeSecs > toUnsignedLong(INFINITE_LEASE)) { + throw new InvalidParameterException("Invalid lease time: " + mDhcpLeaseTimeSecs); + } + if (mLinkMtu != MTU_UNSET && (mLinkMtu < IPV4_MIN_MTU || mLinkMtu > IPV4_MAX_MTU)) { + throw new InvalidParameterException("Invalid link MTU: " + mLinkMtu); + } + if (!mServerAddr.isIPv4()) { + throw new InvalidParameterException("serverAddr must be IPv4"); + } + if (mServerAddr.getPrefixLength() < MIN_PREFIX_LENGTH + || mServerAddr.getPrefixLength() > MAX_PREFIX_LENGTH) { + throw new InvalidParameterException("Prefix length is not in supported range"); + } + + final IpPrefix prefix = makeIpPrefix(mServerAddr); + for (Inet4Address addr : mDefaultRouters) { + if (!prefix.contains(addr)) { + throw new InvalidParameterException(String.format( + "Default router %s is not in server prefix %s", addr, mServerAddr)); + } + } + + final Set<Inet4Address> excl = new HashSet<>(); + if (mExcludedAddrs != null) { + excl.addAll(mExcludedAddrs); + } + excl.add((Inet4Address) mServerAddr.getAddress()); + excl.addAll(mDefaultRouters); + excl.addAll(mDnsServers); + + return new DhcpServingParams(mServerAddr, + Collections.unmodifiableSet(new HashSet<>(mDefaultRouters)), + Collections.unmodifiableSet(new HashSet<>(mDnsServers)), + Collections.unmodifiableSet(excl), + mDhcpLeaseTimeSecs, mLinkMtu, mMetered); + } + } + + /** + * Utility method to create an IpPrefix with the address and prefix length of a LinkAddress. + */ + @NonNull + static IpPrefix makeIpPrefix(@NonNull LinkAddress addr) { + return new IpPrefix(addr.getAddress(), addr.getPrefixLength()); + } +} diff --git a/src/android/net/util/SharedLog.java b/src/android/net/util/SharedLog.java new file mode 100644 index 0000000..74bc147 --- /dev/null +++ b/src/android/net/util/SharedLog.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2017 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 android.annotation.NonNull; +import android.annotation.Nullable; +import android.text.TextUtils; +import android.util.LocalLog; +import android.util.Log; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.StringJoiner; + + +/** + * Class to centralize logging functionality for tethering. + * + * All access to class methods other than dump() must be on the same thread. + * + * @hide + */ +public class SharedLog { + private static final int DEFAULT_MAX_RECORDS = 500; + private static final String COMPONENT_DELIMITER = "."; + + private enum Category { + NONE, + ERROR, + MARK, + WARN, + }; + + private final LocalLog mLocalLog; + // The tag to use for output to the system log. This is not output to the + // LocalLog because that would be redundant. + private final String mTag; + // The component (or subcomponent) of a system that is sharing this log. + // This can grow in depth if components call forSubComponent() to obtain + // their SharedLog instance. The tag is not included in the component for + // brevity. + private final String mComponent; + + public SharedLog(String tag) { + this(DEFAULT_MAX_RECORDS, tag); + } + + public SharedLog(int maxRecords, String tag) { + this(new LocalLog(maxRecords), tag, tag); + } + + private SharedLog(LocalLog localLog, String tag, String component) { + mLocalLog = localLog; + mTag = tag; + mComponent = component; + } + + /** + * Create a SharedLog based on this log with an additional component prefix on each logged line. + */ + public SharedLog forSubComponent(String component) { + if (!isRootLogInstance()) { + component = mComponent + COMPONENT_DELIMITER + component; + } + return new SharedLog(mLocalLog, mTag, component); + } + + /** + * Dump the contents of this log. + * + * <p>This method may be called on any thread. + */ + public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + mLocalLog.readOnlyLocalLog().dump(fd, writer, args); + } + + ////// + // Methods that both log an entry and emit it to the system log. + ////// + + /** + * Log an error due to an exception. This does not include the exception stacktrace. + * + * <p>The log entry will be also added to the system log. + * @see #e(String, Throwable) + */ + public void e(Exception e) { + Log.e(mTag, record(Category.ERROR, e.toString())); + } + + /** + * Log an error message. + * + * <p>The log entry will be also added to the system log. + */ + public void e(String msg) { + Log.e(mTag, record(Category.ERROR, msg)); + } + + /** + * Log an error due to an exception, with the exception stacktrace if provided. + * + * <p>The error and exception message appear in the shared log, but the stacktrace is only + * logged in general log output (logcat). The log entry will be also added to the system log. + */ + public void e(@NonNull String msg, @Nullable Throwable exception) { + if (exception == null) { + e(msg); + return; + } + Log.e(mTag, record(Category.ERROR, msg + ": " + exception.getMessage()), exception); + } + + /** + * Log an informational message. + * + * <p>The log entry will be also added to the system log. + */ + public void i(String msg) { + Log.i(mTag, record(Category.NONE, msg)); + } + + /** + * Log a warning message. + * + * <p>The log entry will be also added to the system log. + */ + public void w(String msg) { + Log.w(mTag, record(Category.WARN, msg)); + } + + ////// + // Methods that only log an entry (and do NOT emit to the system log). + ////// + + /** + * Log a general message to be only included in the in-memory log. + * + * <p>The log entry will *not* be added to the system log. + */ + public void log(String msg) { + record(Category.NONE, msg); + } + + /** + * Log a general, formatted message to be only included in the in-memory log. + * + * <p>The log entry will *not* be added to the system log. + * @see String#format(String, Object...) + */ + public void logf(String fmt, Object... args) { + log(String.format(fmt, args)); + } + + /** + * Log a message with MARK level. + * + * <p>The log entry will *not* be added to the system log. + */ + public void mark(String msg) { + record(Category.MARK, msg); + } + + private String record(Category category, String msg) { + final String entry = logLine(category, msg); + mLocalLog.log(entry); + return entry; + } + + private String logLine(Category category, String msg) { + final StringJoiner sj = new StringJoiner(" "); + if (!isRootLogInstance()) sj.add("[" + mComponent + "]"); + if (category != Category.NONE) sj.add(category.toString()); + return sj.add(msg).toString(); + } + + // Check whether this SharedLog instance is nominally the top level in + // a potential hierarchy of shared logs (the root of a tree), + // or is a subcomponent within the hierarchy. + private boolean isRootLogInstance() { + return TextUtils.isEmpty(mComponent) || mComponent.equals(mTag); + } +} diff --git a/src/com/android/server/NetworkStackService.java b/src/com/android/server/NetworkStackService.java index 5afaf58..7fea1e0 100644 --- a/src/com/android/server/NetworkStackService.java +++ b/src/com/android/server/NetworkStackService.java @@ -16,15 +16,24 @@ package com.android.server; -import static android.os.Binder.getCallingUid; +import static android.net.dhcp.IDhcpServer.STATUS_INVALID_ARGUMENT; +import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS; +import static android.net.dhcp.IDhcpServer.STATUS_UNKNOWN_ERROR; + +import static com.android.server.util.PermissionUtil.checkNetworkStackCallingPermission; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Service; import android.content.Intent; import android.net.INetworkStackConnector; +import android.net.dhcp.DhcpServer; +import android.net.dhcp.DhcpServingParams; +import android.net.dhcp.DhcpServingParamsParcel; +import android.net.dhcp.IDhcpServerCallbacks; +import android.net.util.SharedLog; import android.os.IBinder; -import android.os.Process; +import android.os.RemoteException; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -54,21 +63,37 @@ public class NetworkStackService extends Service { } private static class NetworkStackConnector extends INetworkStackConnector.Stub { - // TODO: makeDhcpServer(), etc. will go here. + @NonNull + private final SharedLog mLog = new SharedLog(TAG); + + @Override + public void makeDhcpServer(@NonNull String ifName, @NonNull DhcpServingParamsParcel params, + @NonNull IDhcpServerCallbacks cb) throws RemoteException { + checkNetworkStackCallingPermission(); + final DhcpServer server; + try { + server = new DhcpServer( + ifName, + DhcpServingParams.fromParcelableObject(params), + mLog.forSubComponent(ifName + ".DHCP")); + } catch (DhcpServingParams.InvalidParameterException e) { + mLog.e("Invalid DhcpServingParams", e); + cb.onDhcpServerCreated(STATUS_INVALID_ARGUMENT, null); + return; + } catch (Exception e) { + mLog.e("Unknown error starting DhcpServer", e); + cb.onDhcpServerCreated(STATUS_UNKNOWN_ERROR, null); + return; + } + cb.onDhcpServerCreated(STATUS_SUCCESS, server); + } @Override protected void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter fout, @Nullable String[] args) { - checkCaller(); + checkNetworkStackCallingPermission(); fout.println("NetworkStack logs:"); - // TODO: dump logs here - } - } - - private static void checkCaller() { - // TODO: check that the calling PID is the system server. - if (getCallingUid() != Process.SYSTEM_UID && getCallingUid() != Process.ROOT_UID) { - throw new SecurityException("Invalid caller: " + getCallingUid()); + mLog.dump(fd, fout, args); } } } diff --git a/src/com/android/server/util/NetworkStackConstants.java b/src/com/android/server/util/NetworkStackConstants.java new file mode 100644 index 0000000..bb5900c --- /dev/null +++ b/src/com/android/server/util/NetworkStackConstants.java @@ -0,0 +1,45 @@ +/* + * 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 com.android.server.util; + +/** + * Network constants used by the network stack. + */ +public final class NetworkStackConstants { + + /** + * IPv4 constants. + * + * See also: + * - https://tools.ietf.org/html/rfc791 + */ + public static final int IPV4_ADDR_BITS = 32; + public static final int IPV4_MIN_MTU = 68; + public static final int IPV4_MAX_MTU = 65_535; + + /** + * DHCP constants. + * + * See also: + * - https://tools.ietf.org/html/rfc2131 + */ + public static final int INFINITE_LEASE = 0xffffffff; + + private NetworkStackConstants() { + throw new UnsupportedOperationException("This class is not to be instantiated"); + } +} diff --git a/src/com/android/server/util/PermissionUtil.java b/src/com/android/server/util/PermissionUtil.java new file mode 100644 index 0000000..733f873 --- /dev/null +++ b/src/com/android/server/util/PermissionUtil.java @@ -0,0 +1,42 @@ +/* + * 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 com.android.server.util; + +import static android.os.Binder.getCallingUid; + +import android.os.Process; + +/** + * Utility class to check calling permissions on the network stack. + */ +public final class PermissionUtil { + + /** + * Check that the caller is allowed to communicate with the network stack. + * @throws SecurityException The caller is not allowed to communicate with the network stack. + */ + public static void checkNetworkStackCallingPermission() { + // TODO: check that the calling PID is the system server. + if (getCallingUid() != Process.SYSTEM_UID && getCallingUid() != Process.ROOT_UID) { + throw new SecurityException("Invalid caller: " + getCallingUid()); + } + } + + private PermissionUtil() { + throw new UnsupportedOperationException("This class is not to be instantiated"); + } +} |