Move DhcpServer to NetworkStack app

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

Change-Id: I96c40e63e9ceb37b67705bdd4d120307e114715b
diff --git a/packages/NetworkStack/tests/src/android/net/dhcp/DhcpLeaseRepositoryTest.java b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpLeaseRepositoryTest.java
new file mode 100644
index 0000000..51d50d9
--- /dev/null
+++ b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpLeaseRepositoryTest.java
@@ -0,0 +1,539 @@
+/*
+ * 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.InetAddresses.parseNumericAddress;
+import static android.net.dhcp.DhcpLease.HOSTNAME_NONE;
+import static android.net.dhcp.DhcpLeaseRepository.CLIENTID_UNSPEC;
+import static android.net.dhcp.DhcpLeaseRepository.INETADDR_UNSPEC;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.when;
+
+import static java.lang.String.format;
+
+import 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.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.Inet4Address;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DhcpLeaseRepositoryTest {
+    private static final Inet4Address INET4_ANY = (Inet4Address) Inet4Address.ANY;
+    private static final Inet4Address TEST_DEF_ROUTER = parseAddr4("192.168.42.247");
+    private static final Inet4Address TEST_SERVER_ADDR = parseAddr4("192.168.42.241");
+    private static final Inet4Address TEST_RESERVED_ADDR = parseAddr4("192.168.42.243");
+    private static final MacAddress TEST_MAC_1 = MacAddress.fromBytes(
+            new byte[] { 5, 4, 3, 2, 1, 0 });
+    private static final MacAddress TEST_MAC_2 = MacAddress.fromBytes(
+            new byte[] { 0, 1, 2, 3, 4, 5 });
+    private static final MacAddress TEST_MAC_3 = MacAddress.fromBytes(
+            new byte[] { 0, 1, 2, 3, 4, 6 });
+    private static final Inet4Address TEST_INETADDR_1 = parseAddr4("192.168.42.248");
+    private static final Inet4Address TEST_INETADDR_2 = parseAddr4("192.168.42.249");
+    private static final String TEST_HOSTNAME_1 = "hostname1";
+    private static final String TEST_HOSTNAME_2 = "hostname2";
+    private static final IpPrefix TEST_IP_PREFIX = new IpPrefix(TEST_SERVER_ADDR, 22);
+    private static final long TEST_TIME = 100L;
+    private static final int TEST_LEASE_TIME_MS = 3_600_000;
+    private static final Set<Inet4Address> TEST_EXCL_SET =
+            Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+                TEST_SERVER_ADDR, TEST_DEF_ROUTER, TEST_RESERVED_ADDR)));
+
+    @NonNull
+    private SharedLog mLog;
+    @NonNull @Mock
+    private Clock mClock;
+    @NonNull
+    private DhcpLeaseRepository mRepo;
+
+    private static Inet4Address parseAddr4(String inet4Addr) {
+        return (Inet4Address) parseNumericAddress(inet4Addr);
+    }
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mLog = new SharedLog("DhcpLeaseRepositoryTest");
+        when(mClock.elapsedRealtime()).thenReturn(TEST_TIME);
+        mRepo = new DhcpLeaseRepository(
+                TEST_IP_PREFIX, TEST_EXCL_SET, TEST_LEASE_TIME_MS, mLog, mClock);
+    }
+
+    /**
+     * Request a number of addresses through offer/request. Useful to test address exhaustion.
+     * @param nAddr Number of addresses to request.
+     */
+    private void requestAddresses(byte nAddr) throws Exception {
+        final HashSet<Inet4Address> addrs = new HashSet<>();
+        byte[] hwAddrBytes = new byte[] { 8, 4, 3, 2, 1, 0 };
+        for (byte i = 0; i < nAddr; i++) {
+            hwAddrBytes[5] = i;
+            MacAddress newMac = MacAddress.fromBytes(hwAddrBytes);
+            final String hostname = "host_" + i;
+            final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, newMac,
+                    INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, hostname);
+
+            assertNotNull(lease);
+            assertEquals(newMac, lease.getHwAddr());
+            assertEquals(hostname, lease.getHostname());
+            assertTrue(format("Duplicate address allocated: %s in %s", lease.getNetAddr(), addrs),
+                    addrs.add(lease.getNetAddr()));
+
+            requestLeaseSelecting(newMac, lease.getNetAddr(), hostname);
+        }
+    }
+
+    @Test
+    public void testAddressExhaustion() throws Exception {
+        // Use a /28 to quickly run out of addresses
+        mRepo.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), TEST_EXCL_SET, TEST_LEASE_TIME_MS);
+
+        // /28 should have 16 addresses, 14 w/o the first/last, 11 w/o excluded addresses
+        requestAddresses((byte) 11);
+
+        try {
+            mRepo.getOffer(null, TEST_MAC_2,
+                    INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+            fail("Should be out of addresses");
+        } catch (DhcpLeaseRepository.OutOfAddressesException e) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void testUpdateParams_LeaseCleanup() throws Exception {
+        // Inside /28:
+        final Inet4Address reqAddrIn28 = parseAddr4("192.168.42.242");
+        final Inet4Address declinedAddrIn28 = parseAddr4("192.168.42.245");
+
+        // Inside /28, but not available there (first address of the range)
+        final Inet4Address declinedFirstAddrIn28 = parseAddr4("192.168.42.240");
+
+        final DhcpLease reqAddrIn28Lease = requestLeaseSelecting(TEST_MAC_1, reqAddrIn28);
+        mRepo.markLeaseDeclined(declinedAddrIn28);
+        mRepo.markLeaseDeclined(declinedFirstAddrIn28);
+
+        // Inside /22, but outside /28:
+        final Inet4Address reqAddrIn22 = parseAddr4("192.168.42.3");
+        final Inet4Address declinedAddrIn22 = parseAddr4("192.168.42.4");
+
+        final DhcpLease reqAddrIn22Lease = requestLeaseSelecting(TEST_MAC_3, reqAddrIn22);
+        mRepo.markLeaseDeclined(declinedAddrIn22);
+
+        // Address that will be reserved in the updateParams call below
+        final Inet4Address reservedAddr = parseAddr4("192.168.42.244");
+        final DhcpLease reservedAddrLease = requestLeaseSelecting(TEST_MAC_2, reservedAddr);
+
+        // Update from /22 to /28 and add another reserved address
+        Set<Inet4Address> newReserved = new HashSet<>(TEST_EXCL_SET);
+        newReserved.add(reservedAddr);
+        mRepo.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), newReserved, TEST_LEASE_TIME_MS);
+
+        assertHasLease(reqAddrIn28Lease);
+        assertDeclined(declinedAddrIn28);
+
+        assertNotDeclined(declinedFirstAddrIn28);
+
+        assertNoLease(reqAddrIn22Lease);
+        assertNotDeclined(declinedAddrIn22);
+
+        assertNoLease(reservedAddrLease);
+    }
+
+    @Test
+    public void testGetOffer_StableAddress() throws Exception {
+        for (final MacAddress macAddr : new MacAddress[] { TEST_MAC_1, TEST_MAC_2, TEST_MAC_3 }) {
+            final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, macAddr,
+                    INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+
+            // Same lease is offered twice
+            final DhcpLease newLease = mRepo.getOffer(CLIENTID_UNSPEC, macAddr,
+                    INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+            assertEquals(lease, newLease);
+        }
+    }
+
+    @Test
+    public void testUpdateParams_UsesNewPrefix() throws Exception {
+        final IpPrefix newPrefix = new IpPrefix(parseAddr4("192.168.123.0"), 24);
+        mRepo.updateParams(newPrefix, TEST_EXCL_SET, TEST_LEASE_TIME_MS);
+
+        DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1,
+                INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+        assertTrue(newPrefix.contains(lease.getNetAddr()));
+    }
+
+    @Test
+    public void testGetOffer_ExistingLease() throws Exception {
+        requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1, TEST_HOSTNAME_1);
+
+        DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1,
+                INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+        assertEquals(TEST_INETADDR_1, offer.getNetAddr());
+        assertEquals(TEST_HOSTNAME_1, offer.getHostname());
+    }
+
+    @Test
+    public void testGetOffer_ClientIdHasExistingLease() throws Exception {
+        final byte[] clientId = new byte[] { 1, 2 };
+        mRepo.requestLease(clientId, TEST_MAC_1, INET4_ANY /* clientAddr */,
+                INET4_ANY /* relayAddr */, TEST_INETADDR_1 /* reqAddr */, false, TEST_HOSTNAME_1);
+
+        // Different MAC, but same clientId
+        DhcpLease offer = mRepo.getOffer(clientId, TEST_MAC_2,
+                INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+        assertEquals(TEST_INETADDR_1, offer.getNetAddr());
+        assertEquals(TEST_HOSTNAME_1, offer.getHostname());
+    }
+
+    @Test
+    public void testGetOffer_DifferentClientId() throws Exception {
+        final byte[] clientId1 = new byte[] { 1, 2 };
+        final byte[] clientId2 = new byte[] { 3, 4 };
+        mRepo.requestLease(clientId1, TEST_MAC_1, INET4_ANY /* clientAddr */,
+                INET4_ANY /* relayAddr */, TEST_INETADDR_1 /* reqAddr */, false, TEST_HOSTNAME_1);
+
+        // Same MAC, different client ID
+        DhcpLease offer = mRepo.getOffer(clientId2, TEST_MAC_1,
+                INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+        // Obtains a different address
+        assertNotEquals(TEST_INETADDR_1, offer.getNetAddr());
+        assertEquals(HOSTNAME_NONE, offer.getHostname());
+        assertEquals(TEST_MAC_1, offer.getHwAddr());
+    }
+
+    @Test
+    public void testGetOffer_RequestedAddress() throws Exception {
+        DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* relayAddr */,
+                TEST_INETADDR_1 /* reqAddr */, TEST_HOSTNAME_1);
+        assertEquals(TEST_INETADDR_1, offer.getNetAddr());
+        assertEquals(TEST_HOSTNAME_1, offer.getHostname());
+    }
+
+    @Test
+    public void testGetOffer_RequestedAddressInUse() throws Exception {
+        requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1);
+        DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_2, INET4_ANY /* relayAddr */,
+                TEST_INETADDR_1 /* reqAddr */, HOSTNAME_NONE);
+        assertNotEquals(TEST_INETADDR_1, offer.getNetAddr());
+    }
+
+    @Test
+    public void testGetOffer_RequestedAddressReserved() throws Exception {
+        DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* relayAddr */,
+                TEST_RESERVED_ADDR /* reqAddr */, HOSTNAME_NONE);
+        assertNotEquals(TEST_RESERVED_ADDR, offer.getNetAddr());
+    }
+
+    @Test
+    public void testGetOffer_RequestedAddressInvalid() throws Exception {
+        final Inet4Address invalidAddr = parseAddr4("192.168.42.0");
+        DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* relayAddr */,
+                invalidAddr /* reqAddr */, HOSTNAME_NONE);
+        assertNotEquals(invalidAddr, offer.getNetAddr());
+    }
+
+    @Test
+    public void testGetOffer_RequestedAddressOutsideSubnet() throws Exception {
+        final Inet4Address invalidAddr = parseAddr4("192.168.254.2");
+        DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* relayAddr */,
+                invalidAddr /* reqAddr */, HOSTNAME_NONE);
+        assertNotEquals(invalidAddr, offer.getNetAddr());
+    }
+
+    @Test(expected = DhcpLeaseRepository.InvalidSubnetException.class)
+    public void testGetOffer_RelayInInvalidSubnet() throws Exception {
+        mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, parseAddr4("192.168.254.2") /* relayAddr */,
+                INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+    }
+
+    @Test
+    public void testRequestLease_SelectingTwice() throws Exception {
+        final DhcpLease lease1 = requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1,
+                TEST_HOSTNAME_1);
+
+        // Second request from same client for a different address
+        final DhcpLease lease2 = requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_2,
+                TEST_HOSTNAME_2);
+
+        assertEquals(TEST_INETADDR_1, lease1.getNetAddr());
+        assertEquals(TEST_HOSTNAME_1, lease1.getHostname());
+
+        assertEquals(TEST_INETADDR_2, lease2.getNetAddr());
+        assertEquals(TEST_HOSTNAME_2, lease2.getHostname());
+
+        // First address freed when client requested a different one: another client can request it
+        final DhcpLease lease3 = requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1, HOSTNAME_NONE);
+        assertEquals(TEST_INETADDR_1, lease3.getNetAddr());
+    }
+
+    @Test(expected = DhcpLeaseRepository.InvalidAddressException.class)
+    public void testRequestLease_SelectingInvalid() throws Exception {
+        requestLeaseSelecting(TEST_MAC_1, parseAddr4("192.168.254.5"));
+    }
+
+    @Test(expected = DhcpLeaseRepository.InvalidAddressException.class)
+    public void testRequestLease_SelectingInUse() throws Exception {
+        requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1);
+        requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1);
+    }
+
+    @Test(expected = DhcpLeaseRepository.InvalidAddressException.class)
+    public void testRequestLease_SelectingReserved() throws Exception {
+        requestLeaseSelecting(TEST_MAC_1, TEST_RESERVED_ADDR);
+    }
+
+    @Test(expected = DhcpLeaseRepository.InvalidSubnetException.class)
+    public void testRequestLease_SelectingRelayInInvalidSubnet() throws  Exception {
+        mRepo.requestLease(CLIENTID_UNSPEC, TEST_MAC_1, INET4_ANY /* clientAddr */,
+                parseAddr4("192.168.128.1") /* relayAddr */, TEST_INETADDR_1 /* reqAddr */,
+                true /* sidSet */, HOSTNAME_NONE);
+    }
+
+    @Test
+    public void testRequestLease_InitReboot() throws Exception {
+        // Request address once
+        requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1);
+
+        final long newTime = TEST_TIME + 100;
+        when(mClock.elapsedRealtime()).thenReturn(newTime);
+
+        // init-reboot (sidSet == false): verify configuration
+        final DhcpLease lease = requestLeaseInitReboot(TEST_MAC_1, TEST_INETADDR_1);
+        assertEquals(TEST_INETADDR_1, lease.getNetAddr());
+        assertEquals(newTime + TEST_LEASE_TIME_MS, lease.getExpTime());
+    }
+
+    @Test(expected = DhcpLeaseRepository.InvalidAddressException.class)
+    public void testRequestLease_InitRebootWrongAddr() throws Exception {
+        // Request address once
+        requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1);
+        // init-reboot with different requested address
+        requestLeaseInitReboot(TEST_MAC_1, TEST_INETADDR_2);
+    }
+
+    @Test
+    public void testRequestLease_InitRebootUnknownAddr() throws Exception {
+        // init-reboot with unknown requested address
+        final DhcpLease lease = requestLeaseInitReboot(TEST_MAC_1, TEST_INETADDR_2);
+        // RFC2131 says we should not reply to accommodate other servers, but since we are
+        // authoritative we allow creating the lease to avoid issues with lost lease DB (same as
+        // dnsmasq behavior)
+        assertEquals(TEST_INETADDR_2, lease.getNetAddr());
+    }
+
+    @Test(expected = DhcpLeaseRepository.InvalidAddressException.class)
+    public void testRequestLease_InitRebootWrongSubnet() throws Exception {
+        requestLeaseInitReboot(TEST_MAC_1, parseAddr4("192.168.254.2"));
+    }
+
+    @Test
+    public void testRequestLease_Renewing() throws Exception {
+        requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1);
+
+        final long newTime = TEST_TIME + 100;
+        when(mClock.elapsedRealtime()).thenReturn(newTime);
+
+        final DhcpLease lease = requestLeaseRenewing(TEST_MAC_1, TEST_INETADDR_1);
+
+        assertEquals(TEST_INETADDR_1, lease.getNetAddr());
+        assertEquals(newTime + TEST_LEASE_TIME_MS, lease.getExpTime());
+    }
+
+    @Test
+    public void testRequestLease_RenewingUnknownAddr() throws Exception {
+        final long newTime = TEST_TIME + 100;
+        when(mClock.elapsedRealtime()).thenReturn(newTime);
+        final DhcpLease lease = requestLeaseRenewing(TEST_MAC_1, TEST_INETADDR_1);
+        // Allows renewing an unknown address if available
+        assertEquals(TEST_INETADDR_1, lease.getNetAddr());
+        assertEquals(newTime + TEST_LEASE_TIME_MS, lease.getExpTime());
+    }
+
+    @Test(expected = DhcpLeaseRepository.InvalidAddressException.class)
+    public void testRequestLease_RenewingAddrInUse() throws Exception {
+        requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1);
+        requestLeaseRenewing(TEST_MAC_1, TEST_INETADDR_1);
+    }
+
+    @Test(expected = DhcpLeaseRepository.InvalidAddressException.class)
+    public void testRequestLease_RenewingInvalidAddr() throws Exception {
+        requestLeaseRenewing(TEST_MAC_1, parseAddr4("192.168.254.2"));
+    }
+
+    @Test
+    public void testReleaseLease() throws Exception {
+        final DhcpLease lease1 = requestLeaseSelecting(TEST_MAC_1, TEST_INETADDR_1);
+
+        assertHasLease(lease1);
+        assertTrue(mRepo.releaseLease(CLIENTID_UNSPEC, TEST_MAC_1, TEST_INETADDR_1));
+        assertNoLease(lease1);
+
+        final DhcpLease lease2 = requestLeaseSelecting(TEST_MAC_2, TEST_INETADDR_1);
+        assertEquals(TEST_INETADDR_1, lease2.getNetAddr());
+    }
+
+    @Test
+    public void testReleaseLease_UnknownLease() {
+        assertFalse(mRepo.releaseLease(CLIENTID_UNSPEC, TEST_MAC_1, TEST_INETADDR_1));
+    }
+
+    @Test
+    public void testReleaseLease_StableOffer() throws Exception {
+        for (MacAddress mac : new MacAddress[] { TEST_MAC_1, TEST_MAC_2, TEST_MAC_3 }) {
+            final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, mac,
+                    INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+
+            requestLeaseSelecting(mac, lease.getNetAddr());
+            mRepo.releaseLease(CLIENTID_UNSPEC, mac, lease.getNetAddr());
+
+            // Same lease is offered after it was released
+            final DhcpLease newLease = mRepo.getOffer(CLIENTID_UNSPEC, mac,
+                    INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+            assertEquals(lease.getNetAddr(), newLease.getNetAddr());
+        }
+    }
+
+    @Test
+    public void testMarkLeaseDeclined() throws Exception {
+        final DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1,
+                INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+
+        mRepo.markLeaseDeclined(lease.getNetAddr());
+
+        // Same lease is not offered again
+        final DhcpLease newLease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1,
+                INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+        assertNotEquals(lease.getNetAddr(), newLease.getNetAddr());
+    }
+
+    @Test
+    public void testMarkLeaseDeclined_UsedIfOutOfAddresses() throws Exception {
+        // Use a /28 to quickly run out of addresses
+        mRepo.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), TEST_EXCL_SET, TEST_LEASE_TIME_MS);
+
+        mRepo.markLeaseDeclined(TEST_INETADDR_1);
+        mRepo.markLeaseDeclined(TEST_INETADDR_2);
+
+        // /28 should have 16 addresses, 14 w/o the first/last, 11 w/o excluded addresses
+        requestAddresses((byte) 9);
+
+        // Last 2 addresses: addresses marked declined should be used
+        final DhcpLease firstLease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1,
+                INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, TEST_HOSTNAME_1);
+        requestLeaseSelecting(TEST_MAC_1, firstLease.getNetAddr());
+
+        final DhcpLease secondLease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_2,
+                INET4_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, TEST_HOSTNAME_2);
+        requestLeaseSelecting(TEST_MAC_2, secondLease.getNetAddr());
+
+        // Now out of addresses
+        try {
+            mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_3, INET4_ANY /* relayAddr */,
+                    INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+            fail("Repository should be out of addresses and throw");
+        } catch (DhcpLeaseRepository.OutOfAddressesException e) { /* expected */ }
+
+        assertEquals(TEST_INETADDR_1, firstLease.getNetAddr());
+        assertEquals(TEST_HOSTNAME_1, firstLease.getHostname());
+        assertEquals(TEST_INETADDR_2, secondLease.getNetAddr());
+        assertEquals(TEST_HOSTNAME_2, secondLease.getHostname());
+    }
+
+    private DhcpLease requestLease(@NonNull MacAddress macAddr, @NonNull Inet4Address clientAddr,
+            @Nullable Inet4Address reqAddr, @Nullable String hostname, boolean sidSet)
+            throws DhcpLeaseRepository.DhcpLeaseException {
+        return mRepo.requestLease(CLIENTID_UNSPEC, macAddr, clientAddr, INET4_ANY /* relayAddr */,
+                reqAddr, sidSet, hostname);
+    }
+
+    /**
+     * Request a lease simulating a client in the SELECTING state.
+     */
+    private DhcpLease requestLeaseSelecting(@NonNull MacAddress macAddr,
+            @NonNull Inet4Address reqAddr, @Nullable String hostname)
+            throws DhcpLeaseRepository.DhcpLeaseException {
+        return requestLease(macAddr, INET4_ANY /* clientAddr */, reqAddr, hostname,
+                true /* sidSet */);
+    }
+
+    /**
+     * Request a lease simulating a client in the SELECTING state.
+     */
+    private DhcpLease requestLeaseSelecting(@NonNull MacAddress macAddr,
+            @NonNull Inet4Address reqAddr) throws DhcpLeaseRepository.DhcpLeaseException {
+        return requestLeaseSelecting(macAddr, reqAddr, HOSTNAME_NONE);
+    }
+
+    /**
+     * Request a lease simulating a client in the INIT-REBOOT state.
+     */
+    private DhcpLease requestLeaseInitReboot(@NonNull MacAddress macAddr,
+            @NonNull Inet4Address reqAddr) throws DhcpLeaseRepository.DhcpLeaseException {
+        return requestLease(macAddr, INET4_ANY /* clientAddr */, reqAddr, HOSTNAME_NONE,
+                false /* sidSet */);
+    }
+
+    /**
+     * Request a lease simulating a client in the RENEWING state.
+     */
+    private DhcpLease requestLeaseRenewing(@NonNull MacAddress macAddr,
+            @NonNull Inet4Address clientAddr) throws DhcpLeaseRepository.DhcpLeaseException {
+        // Renewing: clientAddr filled in, no reqAddr
+        return requestLease(macAddr, clientAddr, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE,
+                true /* sidSet */);
+    }
+
+    private void assertNoLease(DhcpLease lease) {
+        assertFalse("Leases contain " + lease, mRepo.getCommittedLeases().contains(lease));
+    }
+
+    private void assertHasLease(DhcpLease lease) {
+        assertTrue("Leases do not contain " + lease, mRepo.getCommittedLeases().contains(lease));
+    }
+
+    private void assertNotDeclined(Inet4Address addr) {
+        assertFalse("Address is declined: " + addr, mRepo.getDeclinedAddresses().contains(addr));
+    }
+
+    private void assertDeclined(Inet4Address addr) {
+        assertTrue("Address is not declined: " + addr, mRepo.getDeclinedAddresses().contains(addr));
+    }
+}
diff --git a/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServerTest.java b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServerTest.java
new file mode 100644
index 0000000..d4c1e2e
--- /dev/null
+++ b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServerTest.java
@@ -0,0 +1,327 @@
+/*
+ * 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.InetAddresses.parseNumericAddress;
+import static android.net.dhcp.DhcpPacket.DHCP_CLIENT;
+import static android.net.dhcp.DhcpPacket.DHCP_HOST_NAME;
+import static android.net.dhcp.DhcpPacket.ENCAP_BOOTP;
+import static android.net.dhcp.DhcpPacket.INADDR_ANY;
+import static android.net.dhcp.DhcpPacket.INADDR_BROADCAST;
+import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.INetworkStackStatusCallback;
+import android.net.LinkAddress;
+import android.net.MacAddress;
+import android.net.dhcp.DhcpLeaseRepository.InvalidAddressException;
+import android.net.dhcp.DhcpLeaseRepository.OutOfAddressesException;
+import android.net.dhcp.DhcpServer.Clock;
+import android.net.dhcp.DhcpServer.Dependencies;
+import android.net.util.SharedLog;
+import android.os.HandlerThread;
+import android.support.test.filters.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.testing.TestableLooper.RunWithLooper;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.Inet4Address;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(AndroidTestingRunner.class)
+@SmallTest
+@RunWithLooper
+public class DhcpServerTest {
+    private static final String TEST_IFACE = "testiface";
+
+    private static final Inet4Address TEST_SERVER_ADDR = parseAddr("192.168.0.2");
+    private static final LinkAddress TEST_SERVER_LINKADDR = new LinkAddress(TEST_SERVER_ADDR, 20);
+    private static final Set<Inet4Address> TEST_DEFAULT_ROUTERS = new HashSet<>(
+            Arrays.asList(parseAddr("192.168.0.123"), parseAddr("192.168.0.124")));
+    private static final Set<Inet4Address> TEST_DNS_SERVERS = new HashSet<>(
+            Arrays.asList(parseAddr("192.168.0.126"), parseAddr("192.168.0.127")));
+    private static final Set<Inet4Address> TEST_EXCLUDED_ADDRS = new HashSet<>(
+            Arrays.asList(parseAddr("192.168.0.200"), parseAddr("192.168.0.201")));
+    private static final long TEST_LEASE_TIME_SECS = 3600L;
+    private static final int TEST_MTU = 1500;
+    private static final String TEST_HOSTNAME = "testhostname";
+
+    private static final int TEST_TRANSACTION_ID = 123;
+    private static final byte[] TEST_CLIENT_MAC_BYTES = new byte [] { 1, 2, 3, 4, 5, 6 };
+    private static final MacAddress TEST_CLIENT_MAC = MacAddress.fromBytes(TEST_CLIENT_MAC_BYTES);
+    private static final Inet4Address TEST_CLIENT_ADDR = parseAddr("192.168.0.42");
+
+    private static final long TEST_CLOCK_TIME = 1234L;
+    private static final int TEST_LEASE_EXPTIME_SECS = 3600;
+    private static final DhcpLease TEST_LEASE = new DhcpLease(null, TEST_CLIENT_MAC,
+            TEST_CLIENT_ADDR, TEST_LEASE_EXPTIME_SECS * 1000L + TEST_CLOCK_TIME,
+            null /* hostname */);
+    private static final DhcpLease TEST_LEASE_WITH_HOSTNAME = new DhcpLease(null, TEST_CLIENT_MAC,
+            TEST_CLIENT_ADDR, TEST_LEASE_EXPTIME_SECS * 1000L + TEST_CLOCK_TIME, TEST_HOSTNAME);
+
+    @NonNull @Mock
+    private Dependencies mDeps;
+    @NonNull @Mock
+    private DhcpLeaseRepository mRepository;
+    @NonNull @Mock
+    private Clock mClock;
+    @NonNull @Mock
+    private DhcpPacketListener mPacketListener;
+
+    @NonNull @Captor
+    private ArgumentCaptor<ByteBuffer> mSentPacketCaptor;
+    @NonNull @Captor
+    private ArgumentCaptor<Inet4Address> mResponseDstAddrCaptor;
+
+    @NonNull
+    private HandlerThread mHandlerThread;
+    @NonNull
+    private TestableLooper mLooper;
+    @NonNull
+    private DhcpServer mServer;
+
+    @Nullable
+    private String mPrevShareClassloaderProp;
+
+    private final INetworkStackStatusCallback mAssertSuccessCallback =
+            new INetworkStackStatusCallback.Stub() {
+        @Override
+        public void onStatusAvailable(int statusCode) {
+            assertEquals(STATUS_SUCCESS, statusCode);
+        }
+    };
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        when(mDeps.makeLeaseRepository(any(), any(), any())).thenReturn(mRepository);
+        when(mDeps.makeClock()).thenReturn(mClock);
+        when(mDeps.makePacketListener()).thenReturn(mPacketListener);
+        doNothing().when(mDeps)
+                .sendPacket(any(), mSentPacketCaptor.capture(), mResponseDstAddrCaptor.capture());
+        when(mClock.elapsedRealtime()).thenReturn(TEST_CLOCK_TIME);
+
+        final DhcpServingParams servingParams = new DhcpServingParams.Builder()
+                .setDefaultRouters(TEST_DEFAULT_ROUTERS)
+                .setDhcpLeaseTimeSecs(TEST_LEASE_TIME_SECS)
+                .setDnsServers(TEST_DNS_SERVERS)
+                .setServerAddr(TEST_SERVER_LINKADDR)
+                .setLinkMtu(TEST_MTU)
+                .setExcludedAddrs(TEST_EXCLUDED_ADDRS)
+                .build();
+
+        mLooper = TestableLooper.get(this);
+        mHandlerThread = spy(new HandlerThread("TestDhcpServer"));
+        when(mHandlerThread.getLooper()).thenReturn(mLooper.getLooper());
+        mServer = new DhcpServer(mHandlerThread, TEST_IFACE, servingParams,
+                new SharedLog(DhcpServerTest.class.getSimpleName()), mDeps);
+
+        mServer.start(mAssertSuccessCallback);
+        mLooper.processAllMessages();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mServer.stop(mAssertSuccessCallback);
+        mLooper.processMessages(1);
+        verify(mPacketListener, times(1)).stop();
+        verify(mHandlerThread, times(1)).quitSafely();
+    }
+
+    @Test
+    public void testStart() throws Exception {
+        verify(mPacketListener, times(1)).start();
+    }
+
+    @Test
+    public void testDiscover() throws Exception {
+        // TODO: refactor packet construction to eliminate unnecessary/confusing/duplicate fields
+        when(mRepository.getOffer(isNull() /* clientId */, eq(TEST_CLIENT_MAC),
+                eq(INADDR_ANY) /* relayAddr */, isNull() /* reqAddr */, isNull() /* hostname */))
+                .thenReturn(TEST_LEASE);
+
+        final DhcpDiscoverPacket discover = new DhcpDiscoverPacket(TEST_TRANSACTION_ID,
+                (short) 0 /* secs */, INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES,
+                false /* broadcast */, INADDR_ANY /* srcIp */);
+        mServer.processPacket(discover, DHCP_CLIENT);
+
+        assertResponseSentTo(TEST_CLIENT_ADDR);
+        final DhcpOfferPacket packet = assertOffer(getPacket());
+        assertMatchesTestLease(packet);
+    }
+
+    @Test
+    public void testDiscover_OutOfAddresses() throws Exception {
+        when(mRepository.getOffer(isNull() /* clientId */, eq(TEST_CLIENT_MAC),
+                eq(INADDR_ANY) /* relayAddr */, isNull() /* reqAddr */, isNull() /* hostname */))
+                .thenThrow(new OutOfAddressesException("Test exception"));
+
+        final DhcpDiscoverPacket discover = new DhcpDiscoverPacket(TEST_TRANSACTION_ID,
+                (short) 0 /* secs */, INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES,
+                false /* broadcast */, INADDR_ANY /* srcIp */);
+        mServer.processPacket(discover, DHCP_CLIENT);
+
+        assertResponseSentTo(INADDR_BROADCAST);
+        final DhcpNakPacket packet = assertNak(getPacket());
+        assertMatchesClient(packet);
+    }
+
+    private DhcpRequestPacket makeRequestSelectingPacket() {
+        final DhcpRequestPacket request = new DhcpRequestPacket(TEST_TRANSACTION_ID,
+                (short) 0 /* secs */, INADDR_ANY /* clientIp */, INADDR_ANY /* relayIp */,
+                TEST_CLIENT_MAC_BYTES, false /* broadcast */);
+        request.mServerIdentifier = TEST_SERVER_ADDR;
+        request.mRequestedIp = TEST_CLIENT_ADDR;
+        return request;
+    }
+
+    @Test
+    public void testRequest_Selecting_Ack() throws Exception {
+        when(mRepository.requestLease(isNull() /* clientId */, eq(TEST_CLIENT_MAC),
+                eq(INADDR_ANY) /* clientAddr */, eq(INADDR_ANY) /* relayAddr */,
+                eq(TEST_CLIENT_ADDR) /* reqAddr */, eq(true) /* sidSet */, eq(TEST_HOSTNAME)))
+                .thenReturn(TEST_LEASE_WITH_HOSTNAME);
+
+        final DhcpRequestPacket request = makeRequestSelectingPacket();
+        request.mHostName = TEST_HOSTNAME;
+        request.mRequestedParams = new byte[] { DHCP_HOST_NAME };
+        mServer.processPacket(request, DHCP_CLIENT);
+
+        assertResponseSentTo(TEST_CLIENT_ADDR);
+        final DhcpAckPacket packet = assertAck(getPacket());
+        assertMatchesTestLease(packet, TEST_HOSTNAME);
+    }
+
+    @Test
+    public void testRequest_Selecting_Nak() throws Exception {
+        when(mRepository.requestLease(isNull(), eq(TEST_CLIENT_MAC),
+                eq(INADDR_ANY) /* clientAddr */, eq(INADDR_ANY) /* relayAddr */,
+                eq(TEST_CLIENT_ADDR) /* reqAddr */, eq(true) /* sidSet */, isNull() /* hostname */))
+                .thenThrow(new InvalidAddressException("Test error"));
+
+        final DhcpRequestPacket request = makeRequestSelectingPacket();
+        mServer.processPacket(request, DHCP_CLIENT);
+
+        assertResponseSentTo(INADDR_BROADCAST);
+        final DhcpNakPacket packet = assertNak(getPacket());
+        assertMatchesClient(packet);
+    }
+
+    @Test
+    public void testRequest_Selecting_WrongClientPort() throws Exception {
+        final DhcpRequestPacket request = makeRequestSelectingPacket();
+        mServer.processPacket(request, 50000);
+
+        verify(mRepository, never())
+                .requestLease(any(), any(), any(), any(), any(), anyBoolean(), any());
+        verify(mDeps, never()).sendPacket(any(), any(), any());
+    }
+
+    @Test
+    public void testRelease() throws Exception {
+        final DhcpReleasePacket release = new DhcpReleasePacket(TEST_TRANSACTION_ID,
+                TEST_SERVER_ADDR, TEST_CLIENT_ADDR,
+                INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES);
+        mServer.processPacket(release, DHCP_CLIENT);
+
+        verify(mRepository, times(1))
+                .releaseLease(isNull(), eq(TEST_CLIENT_MAC), eq(TEST_CLIENT_ADDR));
+    }
+
+    /* TODO: add more tests once packet construction is refactored, including:
+     *  - usage of giaddr
+     *  - usage of broadcast bit
+     *  - other request states (init-reboot/renewing/rebinding)
+     */
+
+    private void assertMatchesTestLease(@NonNull DhcpPacket packet, @Nullable String hostname) {
+        assertMatchesClient(packet);
+        assertFalse(packet.hasExplicitClientId());
+        assertEquals(TEST_SERVER_ADDR, packet.mServerIdentifier);
+        assertEquals(TEST_CLIENT_ADDR, packet.mYourIp);
+        assertNotNull(packet.mLeaseTime);
+        assertEquals(TEST_LEASE_EXPTIME_SECS, (int) packet.mLeaseTime);
+        assertEquals(hostname, packet.mHostName);
+    }
+
+    private void assertMatchesTestLease(@NonNull DhcpPacket packet) {
+        assertMatchesTestLease(packet, null);
+    }
+
+    private void assertMatchesClient(@NonNull DhcpPacket packet) {
+        assertEquals(TEST_TRANSACTION_ID, packet.mTransId);
+        assertEquals(TEST_CLIENT_MAC, MacAddress.fromBytes(packet.mClientMac));
+    }
+
+    private void assertResponseSentTo(@NonNull Inet4Address addr) {
+        assertEquals(addr, mResponseDstAddrCaptor.getValue());
+    }
+
+    private static DhcpNakPacket assertNak(@Nullable DhcpPacket packet) {
+        assertTrue(packet instanceof DhcpNakPacket);
+        return (DhcpNakPacket) packet;
+    }
+
+    private static DhcpAckPacket assertAck(@Nullable DhcpPacket packet) {
+        assertTrue(packet instanceof DhcpAckPacket);
+        return (DhcpAckPacket) packet;
+    }
+
+    private static DhcpOfferPacket assertOffer(@Nullable DhcpPacket packet) {
+        assertTrue(packet instanceof DhcpOfferPacket);
+        return (DhcpOfferPacket) packet;
+    }
+
+    private DhcpPacket getPacket() throws Exception {
+        verify(mDeps, times(1)).sendPacket(any(), any(), any());
+        return DhcpPacket.decodeFullPacket(mSentPacketCaptor.getValue(), ENCAP_BOOTP);
+    }
+
+    private static Inet4Address parseAddr(@Nullable String inet4Addr) {
+        return (Inet4Address) parseNumericAddress(inet4Addr);
+    }
+}
diff --git a/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServingParamsTest.java b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServingParamsTest.java
new file mode 100644
index 0000000..3ca0564
--- /dev/null
+++ b/packages/NetworkStack/tests/src/android/net/dhcp/DhcpServingParamsTest.java
@@ -0,0 +1,220 @@
+/*
+ * 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.InetAddresses.parseNumericAddress;
+import static android.net.NetworkUtils.inet4AddressToIntHTH;
+import static android.net.dhcp.DhcpServingParams.MTU_UNSET;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.LinkAddress;
+import android.net.NetworkUtils;
+import android.net.dhcp.DhcpServingParams.InvalidParameterException;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Modifier;
+import java.net.Inet4Address;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DhcpServingParamsTest {
+    @NonNull
+    private DhcpServingParams.Builder mBuilder;
+
+    private static final Set<Inet4Address> TEST_DEFAULT_ROUTERS = new HashSet<>(
+            Arrays.asList(parseAddr("192.168.0.123"), parseAddr("192.168.0.124")));
+    private static final long TEST_LEASE_TIME_SECS = 3600L;
+    private static final Set<Inet4Address> TEST_DNS_SERVERS = new HashSet<>(
+            Arrays.asList(parseAddr("192.168.0.126"), parseAddr("192.168.0.127")));
+    private static final Inet4Address TEST_SERVER_ADDR = parseAddr("192.168.0.2");
+    private static final LinkAddress TEST_LINKADDR = new LinkAddress(TEST_SERVER_ADDR, 20);
+    private static final int TEST_MTU = 1500;
+    private static final Set<Inet4Address> TEST_EXCLUDED_ADDRS = new HashSet<>(
+            Arrays.asList(parseAddr("192.168.0.200"), parseAddr("192.168.0.201")));
+    private static final boolean TEST_METERED = true;
+
+    @Before
+    public void setUp() {
+        mBuilder = new DhcpServingParams.Builder()
+                .setDefaultRouters(TEST_DEFAULT_ROUTERS)
+                .setDhcpLeaseTimeSecs(TEST_LEASE_TIME_SECS)
+                .setDnsServers(TEST_DNS_SERVERS)
+                .setServerAddr(TEST_LINKADDR)
+                .setLinkMtu(TEST_MTU)
+                .setExcludedAddrs(TEST_EXCLUDED_ADDRS)
+                .setMetered(TEST_METERED);
+    }
+
+    @Test
+    public void testBuild_Immutable() throws InvalidParameterException {
+        final Set<Inet4Address> routers = new HashSet<>(TEST_DEFAULT_ROUTERS);
+        final Set<Inet4Address> dnsServers = new HashSet<>(TEST_DNS_SERVERS);
+        final Set<Inet4Address> excludedAddrs = new HashSet<>(TEST_EXCLUDED_ADDRS);
+
+        final DhcpServingParams params = mBuilder
+                .setDefaultRouters(routers)
+                .setDnsServers(dnsServers)
+                .setExcludedAddrs(excludedAddrs)
+                .build();
+
+        // Modifications to source objects should not affect builder or final parameters
+        final Inet4Address addedAddr = parseAddr("192.168.0.223");
+        routers.add(addedAddr);
+        dnsServers.add(addedAddr);
+        excludedAddrs.add(addedAddr);
+
+        assertEquals(TEST_DEFAULT_ROUTERS, params.defaultRouters);
+        assertEquals(TEST_LEASE_TIME_SECS, params.dhcpLeaseTimeSecs);
+        assertEquals(TEST_DNS_SERVERS, params.dnsServers);
+        assertEquals(TEST_LINKADDR, params.serverAddr);
+        assertEquals(TEST_MTU, params.linkMtu);
+        assertEquals(TEST_METERED, params.metered);
+
+        assertContains(params.excludedAddrs, TEST_EXCLUDED_ADDRS);
+        assertContains(params.excludedAddrs, TEST_DEFAULT_ROUTERS);
+        assertContains(params.excludedAddrs, TEST_DNS_SERVERS);
+        assertContains(params.excludedAddrs, TEST_SERVER_ADDR);
+
+        assertFalse("excludedAddrs should not contain " + addedAddr,
+                params.excludedAddrs.contains(addedAddr));
+    }
+
+    @Test(expected = InvalidParameterException.class)
+    public void testBuild_NegativeLeaseTime() throws InvalidParameterException {
+        mBuilder.setDhcpLeaseTimeSecs(-1).build();
+    }
+
+    @Test(expected = InvalidParameterException.class)
+    public void testBuild_LeaseTimeTooLarge() throws InvalidParameterException {
+        // Set lease time larger than max value for uint32
+        mBuilder.setDhcpLeaseTimeSecs(1L << 32).build();
+    }
+
+    @Test
+    public void testBuild_InfiniteLeaseTime() throws InvalidParameterException {
+        final long infiniteLeaseTime = 0xffffffffL;
+        final DhcpServingParams params = mBuilder
+                .setDhcpLeaseTimeSecs(infiniteLeaseTime).build();
+        assertEquals(infiniteLeaseTime, params.dhcpLeaseTimeSecs);
+        assertTrue(params.dhcpLeaseTimeSecs > 0L);
+    }
+
+    @Test
+    public void testBuild_UnsetMtu() throws InvalidParameterException {
+        final DhcpServingParams params = mBuilder.setLinkMtu(MTU_UNSET).build();
+        assertEquals(MTU_UNSET, params.linkMtu);
+    }
+
+    @Test(expected = InvalidParameterException.class)
+    public void testBuild_MtuTooSmall() throws InvalidParameterException {
+        mBuilder.setLinkMtu(20).build();
+    }
+
+    @Test(expected = InvalidParameterException.class)
+    public void testBuild_MtuTooLarge() throws InvalidParameterException {
+        mBuilder.setLinkMtu(65_536).build();
+    }
+
+    @Test(expected = InvalidParameterException.class)
+    public void testBuild_IPv6Addr() throws InvalidParameterException {
+        mBuilder.setServerAddr(new LinkAddress(parseNumericAddress("fe80::1111"), 120)).build();
+    }
+
+    @Test(expected = InvalidParameterException.class)
+    public void testBuild_PrefixTooLarge() throws InvalidParameterException {
+        mBuilder.setServerAddr(new LinkAddress(TEST_SERVER_ADDR, 15)).build();
+    }
+
+    @Test(expected = InvalidParameterException.class)
+    public void testBuild_PrefixTooSmall() throws InvalidParameterException {
+        mBuilder.setDefaultRouters(parseAddr("192.168.0.254"))
+                .setServerAddr(new LinkAddress(TEST_SERVER_ADDR, 31))
+                .build();
+    }
+
+    @Test(expected = InvalidParameterException.class)
+    public void testBuild_RouterNotInPrefix() throws InvalidParameterException {
+        mBuilder.setDefaultRouters(parseAddr("192.168.254.254")).build();
+    }
+
+    @Test
+    public void testFromParcelableObject() throws InvalidParameterException {
+        final DhcpServingParams params = mBuilder.build();
+        final DhcpServingParamsParcel parcel = new DhcpServingParamsParcel();
+        parcel.defaultRouters = toIntArray(TEST_DEFAULT_ROUTERS);
+        parcel.dhcpLeaseTimeSecs = TEST_LEASE_TIME_SECS;
+        parcel.dnsServers = toIntArray(TEST_DNS_SERVERS);
+        parcel.serverAddr = inet4AddressToIntHTH(TEST_SERVER_ADDR);
+        parcel.serverAddrPrefixLength = TEST_LINKADDR.getPrefixLength();
+        parcel.linkMtu = TEST_MTU;
+        parcel.excludedAddrs = toIntArray(TEST_EXCLUDED_ADDRS);
+        parcel.metered = TEST_METERED;
+        final DhcpServingParams parceled = DhcpServingParams.fromParcelableObject(parcel);
+
+        assertEquals(params.defaultRouters, parceled.defaultRouters);
+        assertEquals(params.dhcpLeaseTimeSecs, parceled.dhcpLeaseTimeSecs);
+        assertEquals(params.dnsServers, parceled.dnsServers);
+        assertEquals(params.serverAddr, parceled.serverAddr);
+        assertEquals(params.linkMtu, parceled.linkMtu);
+        assertEquals(params.excludedAddrs, parceled.excludedAddrs);
+        assertEquals(params.metered, parceled.metered);
+
+        // Ensure that we do not miss any field if added in the future
+        final long numFields = Arrays.stream(DhcpServingParams.class.getDeclaredFields())
+                .filter(f -> !Modifier.isStatic(f.getModifiers()))
+                .count();
+        assertEquals(7, numFields);
+    }
+
+    @Test(expected = InvalidParameterException.class)
+    public void testFromParcelableObject_NullArgument() throws InvalidParameterException {
+        DhcpServingParams.fromParcelableObject(null);
+    }
+
+    private static int[] toIntArray(Collection<Inet4Address> addrs) {
+        return addrs.stream().mapToInt(NetworkUtils::inet4AddressToIntHTH).toArray();
+    }
+
+    private static <T> void assertContains(@NonNull Set<T> set, @NonNull Set<T> subset) {
+        for (final T elem : subset) {
+            assertContains(set, elem);
+        }
+    }
+
+    private static <T> void assertContains(@NonNull Set<T> set, @Nullable T elem) {
+        assertTrue("Set does not contain " + elem, set.contains(elem));
+    }
+
+    @NonNull
+    private static Inet4Address parseAddr(@NonNull String inet4Addr) {
+        return (Inet4Address) parseNumericAddress(inet4Addr);
+    }
+}
diff --git a/packages/NetworkStack/tests/src/com/android/server/util/SharedLogTest.java b/packages/NetworkStack/tests/src/com/android/server/util/SharedLogTest.java
new file mode 100644
index 0000000..07ad3123
--- /dev/null
+++ b/packages/NetworkStack/tests/src/com/android/server/util/SharedLogTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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 com.android.server.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.net.util.SharedLog;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SharedLogTest {
+    private static final String TIMESTAMP_PATTERN = "\\d{2}:\\d{2}:\\d{2}";
+    private static final String TIMESTAMP = "HH:MM:SS";
+
+    @Test
+    public void testBasicOperation() {
+        final SharedLog logTop = new SharedLog("top");
+        logTop.mark("first post!");
+
+        final SharedLog logLevel2a = logTop.forSubComponent("twoA");
+        final SharedLog logLevel2b = logTop.forSubComponent("twoB");
+        logLevel2b.e("2b or not 2b");
+        logLevel2b.e("No exception", null);
+        logLevel2b.e("Wait, here's one", new Exception("Test"));
+        logLevel2a.w("second post?");
+
+        final SharedLog logLevel3 = logLevel2a.forSubComponent("three");
+        logTop.log("still logging");
+        logLevel3.log("3 >> 2");
+        logLevel2a.mark("ok: last post");
+
+        final String[] expected = {
+            " - MARK first post!",
+            " - [twoB] ERROR 2b or not 2b",
+            " - [twoB] ERROR No exception",
+            // No stacktrace in shared log, only in logcat
+            " - [twoB] ERROR Wait, here's one: Test",
+            " - [twoA] WARN second post?",
+            " - still logging",
+            " - [twoA.three] 3 >> 2",
+            " - [twoA] MARK ok: last post",
+        };
+        // Verify the logs are all there and in the correct order.
+        verifyLogLines(expected, logTop);
+
+        // In fact, because they all share the same underlying LocalLog,
+        // every subcomponent SharedLog's dump() is identical.
+        verifyLogLines(expected, logLevel2a);
+        verifyLogLines(expected, logLevel2b);
+        verifyLogLines(expected, logLevel3);
+    }
+
+    private static void verifyLogLines(String[] expected, SharedLog log) {
+        final ByteArrayOutputStream ostream = new ByteArrayOutputStream();
+        final PrintWriter pw = new PrintWriter(ostream, true);
+        log.dump(null, pw, null);
+
+        final String dumpOutput = ostream.toString();
+        assertTrue(dumpOutput != null);
+        assertTrue(!"".equals(dumpOutput));
+
+        final String[] lines = dumpOutput.split("\n");
+        assertEquals(expected.length, lines.length);
+
+        for (int i = 0; i < expected.length; i++) {
+            String got = lines[i];
+            String want = expected[i];
+            assertTrue(String.format("'%s' did not contain '%s'", got, want), got.endsWith(want));
+            assertTrue(String.format("'%s' did not contain a %s timestamp", got, TIMESTAMP),
+                    got.replaceFirst(TIMESTAMP_PATTERN, TIMESTAMP).contains(TIMESTAMP));
+        }
+    }
+}