Move DhcpServer to NetworkStack app

Test: atest FrameworksNetTests && atest NetworkStackTests
Bug: b/112869080

Change-Id: I96c40e63e9ceb37b67705bdd4d120307e114715b
diff --git a/packages/NetworkStack/src/android/net/dhcp/DhcpLease.java b/packages/NetworkStack/src/android/net/dhcp/DhcpLease.java
new file mode 100644
index 0000000..6849cfa
--- /dev/null
+++ b/packages/NetworkStack/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/packages/NetworkStack/src/android/net/dhcp/DhcpLeaseRepository.java b/packages/NetworkStack/src/android/net/dhcp/DhcpLeaseRepository.java
new file mode 100644
index 0000000..0d298de
--- /dev/null
+++ b/packages/NetworkStack/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/packages/NetworkStack/src/android/net/dhcp/DhcpPacketListener.java b/packages/NetworkStack/src/android/net/dhcp/DhcpPacketListener.java
new file mode 100644
index 0000000..dce8b61
--- /dev/null
+++ b/packages/NetworkStack/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/packages/NetworkStack/src/android/net/dhcp/DhcpServer.java b/packages/NetworkStack/src/android/net/dhcp/DhcpServer.java
new file mode 100644
index 0000000..14e2936
--- /dev/null
+++ b/packages/NetworkStack/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/packages/NetworkStack/src/android/net/dhcp/DhcpServingParams.java b/packages/NetworkStack/src/android/net/dhcp/DhcpServingParams.java
new file mode 100644
index 0000000..f38888a
--- /dev/null
+++ b/packages/NetworkStack/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/packages/NetworkStack/src/android/net/util/SharedLog.java b/packages/NetworkStack/src/android/net/util/SharedLog.java
new file mode 100644
index 0000000..74bc147
--- /dev/null
+++ b/packages/NetworkStack/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/packages/NetworkStack/src/com/android/server/NetworkStackService.java b/packages/NetworkStack/src/com/android/server/NetworkStackService.java
index 5afaf58..7fea1e0 100644
--- a/packages/NetworkStack/src/com/android/server/NetworkStackService.java
+++ b/packages/NetworkStack/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 @@
     }
 
     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/packages/NetworkStack/src/com/android/server/util/NetworkStackConstants.java b/packages/NetworkStack/src/com/android/server/util/NetworkStackConstants.java
new file mode 100644
index 0000000..bb5900c
--- /dev/null
+++ b/packages/NetworkStack/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/packages/NetworkStack/src/com/android/server/util/PermissionUtil.java b/packages/NetworkStack/src/com/android/server/util/PermissionUtil.java
new file mode 100644
index 0000000..733f873
--- /dev/null
+++ b/packages/NetworkStack/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");
+    }
+}