Merge "Support specific client address configuration" am: 5fca2175db am: c6e1433cf0

Change-Id: I46b9b70fba4386b8af788c86d04825fbcc470f03
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/DhcpServingParamsParcel.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/DhcpServingParamsParcel.aidl
index a802e41..eb780a2 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/DhcpServingParamsParcel.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/dhcp/DhcpServingParamsParcel.aidl
@@ -25,4 +25,5 @@
   long dhcpLeaseTimeSecs;
   int linkMtu;
   boolean metered;
+  int clientAddr;
 }
diff --git a/common/networkstackclient/src/android/net/dhcp/DhcpServingParamsParcel.aidl b/common/networkstackclient/src/android/net/dhcp/DhcpServingParamsParcel.aidl
index 7b8b9ee..5e19374 100644
--- a/common/networkstackclient/src/android/net/dhcp/DhcpServingParamsParcel.aidl
+++ b/common/networkstackclient/src/android/net/dhcp/DhcpServingParamsParcel.aidl
@@ -26,5 +26,6 @@
     long dhcpLeaseTimeSecs;
     int linkMtu;
     boolean metered;
+    int clientAddr;
 }
 
diff --git a/src/android/net/dhcp/DhcpLeaseRepository.java b/src/android/net/dhcp/DhcpLeaseRepository.java
index 1dc2f7f..a1f6612 100644
--- a/src/android/net/dhcp/DhcpLeaseRepository.java
+++ b/src/android/net/dhcp/DhcpLeaseRepository.java
@@ -80,6 +80,8 @@
     private int mSubnetMask;
     private int mNumAddresses;
     private long mLeaseTimeMs;
+    @Nullable
+    private Inet4Address mClientAddr;
 
     /**
      * Next timestamp when committed or declined leases should be checked for expired ones. This
@@ -128,21 +130,24 @@
     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);
+            long leaseTimeMs, @Nullable Inet4Address clientAddr, @NonNull SharedLog log,
+            @NonNull Clock clock) {
         mLog = log;
         mClock = clock;
+        mClientAddr = clientAddr;
+        updateParams(prefix, reservedAddrs, leaseTimeMs, clientAddr);
     }
 
     public void updateParams(@NonNull IpPrefix prefix, @NonNull Set<Inet4Address> reservedAddrs,
-            long leaseTimeMs) {
+            long leaseTimeMs, @Nullable Inet4Address clientAddr) {
         mPrefix = prefix;
         mReservedAddrs = Collections.unmodifiableSet(new HashSet<>(reservedAddrs));
         mPrefixLength = prefix.getPrefixLength();
         mSubnetMask = prefixLengthToV4NetmaskIntHTH(mPrefixLength);
         mSubnetAddr = inet4AddressToIntHTH((Inet4Address) prefix.getAddress()) & mSubnetMask;
-        mNumAddresses = 1 << (IPV4_ADDR_BITS - prefix.getPrefixLength());
+        mNumAddresses = clientAddr != null ? 1 : 1 << (IPV4_ADDR_BITS - prefix.getPrefixLength());
         mLeaseTimeMs = leaseTimeMs;
+        mClientAddr = clientAddr;
 
         cleanMap(mDeclinedAddrs);
         if (cleanMap(mCommittedLeases)) {
@@ -514,6 +519,9 @@
      * address (with the ordering in {@link #getAddrIndex(int)}) is returned.
      */
     private int getValidAddress(int addr) {
+        // Only mClientAddr is valid if static client address is enforced.
+        if (mClientAddr != null) return inet4AddressToIntHTH(mClientAddr);
+
         final int lastByteMask = 0xff;
         int addrIndex = getAddrIndex(addr); // 0-based index of the address in the subnet
 
diff --git a/src/android/net/dhcp/DhcpServer.java b/src/android/net/dhcp/DhcpServer.java
index 9c5b3c6..bf22fcb 100644
--- a/src/android/net/dhcp/DhcpServer.java
+++ b/src/android/net/dhcp/DhcpServer.java
@@ -205,8 +205,8 @@
                 @NonNull SharedLog log, @NonNull Clock clock) {
             return new DhcpLeaseRepository(
                     DhcpServingParams.makeIpPrefix(servingParams.serverAddr),
-                    servingParams.excludedAddrs,
-                    servingParams.dhcpLeaseTimeSecs * 1000, log.forSubComponent(REPO_TAG), clock);
+                    servingParams.excludedAddrs, servingParams.dhcpLeaseTimeSecs * 1000,
+                    servingParams.clientAddr, log.forSubComponent(REPO_TAG), clock);
         }
 
         @Override
@@ -351,7 +351,8 @@
                     mLeaseRepo.updateParams(
                             DhcpServingParams.makeIpPrefix(mServingParams.serverAddr),
                             params.excludedAddrs,
-                            params.dhcpLeaseTimeSecs);
+                            params.dhcpLeaseTimeSecs,
+                            params.clientAddr);
 
                     cb = pair.second;
                     break;
diff --git a/src/android/net/dhcp/DhcpServingParams.java b/src/android/net/dhcp/DhcpServingParams.java
index eafe44e..63f847d 100644
--- a/src/android/net/dhcp/DhcpServingParams.java
+++ b/src/android/net/dhcp/DhcpServingParams.java
@@ -85,6 +85,12 @@
     public final boolean metered;
 
     /**
+     * Client inet address. This will be the only address offered by DhcpServer if set.
+     */
+    @Nullable
+    public final Inet4Address clientAddr;
+
+    /**
      * Checked exception thrown when some parameters used to build {@link DhcpServingParams} are
      * missing or invalid.
      */
@@ -97,7 +103,7 @@
     private DhcpServingParams(@NonNull LinkAddress serverAddr,
             @NonNull Set<Inet4Address> defaultRouters,
             @NonNull Set<Inet4Address> dnsServers, @NonNull Set<Inet4Address> excludedAddrs,
-            long dhcpLeaseTimeSecs, int linkMtu, boolean metered) {
+            long dhcpLeaseTimeSecs, int linkMtu, boolean metered, Inet4Address clientAddr) {
         this.serverAddr = serverAddr;
         this.defaultRouters = defaultRouters;
         this.dnsServers = dnsServers;
@@ -105,6 +111,7 @@
         this.dhcpLeaseTimeSecs = dhcpLeaseTimeSecs;
         this.linkMtu = linkMtu;
         this.metered = metered;
+        this.clientAddr = clientAddr;
     }
 
     /**
@@ -119,6 +126,11 @@
         final LinkAddress serverAddr = new LinkAddress(
                 intToInet4AddressHTH(parcel.serverAddr),
                 parcel.serverAddrPrefixLength);
+        Inet4Address clientAddr = null;
+        if (parcel.clientAddr != 0) {
+            clientAddr = intToInet4AddressHTH(parcel.clientAddr);
+        }
+
         return new Builder()
                 .setServerAddr(serverAddr)
                 .setDefaultRouters(toInet4AddressSet(parcel.defaultRouters))
@@ -127,6 +139,7 @@
                 .setDhcpLeaseTimeSecs(parcel.dhcpLeaseTimeSecs)
                 .setLinkMtu(parcel.linkMtu)
                 .setMetered(parcel.metered)
+                .setClientAddr(clientAddr)
                 .build();
     }
 
@@ -181,6 +194,7 @@
         private long mDhcpLeaseTimeSecs;
         private int mLinkMtu = MTU_UNSET;
         private boolean mMetered;
+        private Inet4Address mClientAddr;
 
         /**
          * Set the server address and served prefix for the DHCP server.
@@ -305,6 +319,16 @@
         }
 
         /**
+         * Set the client address.
+         *
+         * <p>If not set, the default value is null.
+         */
+        public Builder setClientAddr(@Nullable Inet4Address clientAddr) {
+            this.mClientAddr = clientAddr;
+            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
@@ -358,7 +382,7 @@
                     Collections.unmodifiableSet(new HashSet<>(mDefaultRouters)),
                     Collections.unmodifiableSet(new HashSet<>(mDnsServers)),
                     Collections.unmodifiableSet(excl),
-                    mDhcpLeaseTimeSecs, mLinkMtu, mMetered);
+                    mDhcpLeaseTimeSecs, mLinkMtu, mMetered, mClientAddr);
         }
     }
 
diff --git a/tests/unit/src/android/net/dhcp/DhcpLeaseRepositoryTest.java b/tests/unit/src/android/net/dhcp/DhcpLeaseRepositoryTest.java
index 82f9b50..818a48a 100644
--- a/tests/unit/src/android/net/dhcp/DhcpLeaseRepositoryTest.java
+++ b/tests/unit/src/android/net/dhcp/DhcpLeaseRepositoryTest.java
@@ -34,6 +34,7 @@
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -71,6 +72,7 @@
 public class DhcpLeaseRepositoryTest {
     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_CLIENT_ADDR = parseAddr4("192.168.42.2");
     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 });
@@ -108,12 +110,17 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
+        initDhcpLeaseRepositoryWithOption(null);
+    }
+
+    private void initDhcpLeaseRepositoryWithOption(final Inet4Address clientAddr) {
+        reset(mCallbacks, mClock);
         mLog = new SharedLog("DhcpLeaseRepositoryTest");
         when(mClock.elapsedRealtime()).thenReturn(TEST_TIME);
         // Use a non-null Binder for linkToDeath
         when(mCallbacks.asBinder()).thenReturn(mCallbacksBinder);
         mRepo = new DhcpLeaseRepository(
-                TEST_IP_PREFIX, TEST_EXCL_SET, TEST_LEASE_TIME_MS, mLog, mClock);
+                TEST_IP_PREFIX, TEST_EXCL_SET, TEST_LEASE_TIME_MS, clientAddr, mLog, mClock);
         mRepo.addLeaseCallbacks(mCallbacks);
         verify(mCallbacks, atLeastOnce()).asBinder();
     }
@@ -145,7 +152,8 @@
     @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);
+        mRepo.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), TEST_EXCL_SET, TEST_LEASE_TIME_MS,
+                null /* clientAddr */);
 
         // /28 should have 16 addresses, 14 w/o the first/last, 11 w/o excluded addresses
         requestAddresses((byte) 11);
@@ -191,7 +199,8 @@
         // 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);
+        mRepo.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), newReserved, TEST_LEASE_TIME_MS,
+                null /* clientAddr */);
         // Callback is called for the second time with just this lease
         verifyLeasesChangedCallback(2 /* times */, reqAddrIn28Lease);
         verifyNoMoreInteractions(mCallbacks);
@@ -223,7 +232,7 @@
     @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);
+        mRepo.updateParams(newPrefix, TEST_EXCL_SET, TEST_LEASE_TIME_MS, null /* clientAddr */);
 
         DhcpLease lease = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1,
                 IPV4_ADDR_ANY /* relayAddr */, INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
@@ -313,6 +322,31 @@
         assertNotEquals(invalidAddr, offer.getNetAddr());
     }
 
+    @Test
+    public void testGetOffer_StaticClientAddress() throws Exception {
+        initDhcpLeaseRepositoryWithOption(TEST_CLIENT_ADDR);
+        final DhcpLease offer = mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1,
+                IPV4_ADDR_ANY /* relayAddr */, TEST_INETADDR_1 /* reqAddr */, TEST_HOSTNAME_1);
+        assertEquals(TEST_CLIENT_ADDR, offer.getNetAddr());
+        assertEquals(TEST_HOSTNAME_1, offer.getHostname());
+    }
+
+    @Test
+    public void testGetOffer_StaticClientAddressInUse() throws Exception {
+        initDhcpLeaseRepositoryWithOption(TEST_CLIENT_ADDR);
+        final byte[] clientId = new byte[] { 1 };
+        final DhcpLease lease = mRepo.requestLease(clientId, TEST_MAC_1,
+                IPV4_ADDR_ANY /* clientAddr */, IPV4_ADDR_ANY /* relayAddr */,
+                TEST_CLIENT_ADDR /* reqAddr */, false, TEST_HOSTNAME_1);
+
+        // Static client address only support single client use case.
+        try {
+            mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, IPV4_ADDR_ANY /* relayAddr */,
+                    INETADDR_UNSPEC /* reqAddr */, HOSTNAME_NONE);
+            fail("Repository should be out of addresses and throw");
+        } catch (DhcpLeaseRepository.OutOfAddressesException e) { /* expected */ }
+    }
+
     @Test(expected = DhcpLeaseRepository.InvalidSubnetException.class)
     public void testGetOffer_RelayInInvalidSubnet() throws Exception {
         mRepo.getOffer(CLIENTID_UNSPEC, TEST_MAC_1, parseAddr4("192.168.254.2") /* relayAddr */,
@@ -507,7 +541,8 @@
     @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.updateParams(new IpPrefix(TEST_SERVER_ADDR, 28), TEST_EXCL_SET, TEST_LEASE_TIME_MS,
+                null /* clientAddr */);
 
         mRepo.markLeaseDeclined(TEST_INETADDR_1);
         mRepo.markLeaseDeclined(TEST_INETADDR_2);
diff --git a/tests/unit/src/android/net/dhcp/DhcpServingParamsTest.java b/tests/unit/src/android/net/dhcp/DhcpServingParamsTest.java
index 57a87a4..9948fe3 100644
--- a/tests/unit/src/android/net/dhcp/DhcpServingParamsTest.java
+++ b/tests/unit/src/android/net/dhcp/DhcpServingParamsTest.java
@@ -33,11 +33,12 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.testutils.MiscAssertsKt;
+
 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;
@@ -56,6 +57,7 @@
     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 Inet4Address TEST_CLIENT_ADDR = parseAddr("192.168.0.42");
     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<>(
@@ -71,7 +73,8 @@
                 .setServerAddr(TEST_LINKADDR)
                 .setLinkMtu(TEST_MTU)
                 .setExcludedAddrs(TEST_EXCLUDED_ADDRS)
-                .setMetered(TEST_METERED);
+                .setMetered(TEST_METERED)
+                .setClientAddr(TEST_CLIENT_ADDR);
     }
 
     @Test
@@ -178,6 +181,7 @@
         parcel.linkMtu = TEST_MTU;
         parcel.excludedAddrs = toIntArray(TEST_EXCLUDED_ADDRS);
         parcel.metered = TEST_METERED;
+        parcel.clientAddr = inet4AddressToIntHTH(TEST_CLIENT_ADDR);
         final DhcpServingParams parceled = DhcpServingParams.fromParcelableObject(parcel);
 
         assertEquals(params.defaultRouters, parceled.defaultRouters);
@@ -187,12 +191,9 @@
         assertEquals(params.linkMtu, parceled.linkMtu);
         assertEquals(params.excludedAddrs, parceled.excludedAddrs);
         assertEquals(params.metered, parceled.metered);
+        assertEquals(params.clientAddr, parceled.clientAddr);
 
-        // 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);
+        MiscAssertsKt.assertFieldCountEquals(9, DhcpServingParamsParcel.class);
     }
 
     @Test(expected = InvalidParameterException.class)