Only renew/rebind valid prefixes at ReacquireState.

For prefixes that DHCP server cannot extend, client should stop trying
to send renew/rebind message to extend the lifetime for those prefixes,
for example, prefix with preferred/valid lifetime of 0 or prefix with
t1/t2 equals to valid lifetime, which indicates that server won't extend
the lifetime for these prefixes.

Bug: 260934173
Test: atest NetworkStackIntegraionTests
Change-Id: I8cefd3d6e8cd6dd75ef4785bbf280a8beb5083ad
diff --git a/src/android/net/dhcp6/Dhcp6Client.java b/src/android/net/dhcp6/Dhcp6Client.java
index eba5767..291a97a 100644
--- a/src/android/net/dhcp6/Dhcp6Client.java
+++ b/src/android/net/dhcp6/Dhcp6Client.java
@@ -67,6 +67,7 @@
 import java.net.SocketException;
 import java.nio.ByteBuffer;
 import java.util.Collections;
+import java.util.List;
 import java.util.Random;
 import java.util.function.IntSupplier;
 
@@ -384,8 +385,9 @@
     }
 
     private void scheduleLeaseTimers() {
-        // TODO: validate t1, t2, valid and preferred lifetimes before the timers are scheduled to
-        // prevent packet storms due to low timeouts.
+        // TODO: validate t1, t2, valid and preferred lifetimes before the timers are scheduled
+        // to prevent packet storms due to low timeouts. Preferred/valid lifetime of 0 should be
+        // excluded before scheduling the lease timer.
         int renewTimeout = mReply.t1;
         int rebindTimeout = mReply.t2;
         final long preferredTimeout = mReply.getMinimalPreferredLifetime();
@@ -785,7 +787,9 @@
 
         @Override
         protected boolean sendPacket(int transId, long elapsedTimeMs) {
-            return sendRenewPacket(transId, elapsedTimeMs, mReply.build());
+            final List<IaPrefixOption> toBeRenewed = mReply.getRenewableIaPrefixes();
+            if (toBeRenewed.isEmpty()) return false;
+            return sendRenewPacket(transId, elapsedTimeMs, mReply.build(toBeRenewed));
         }
     }
 
@@ -801,7 +805,9 @@
 
         @Override
         protected boolean sendPacket(int transId, long elapsedTimeMs) {
-            return sendRebindPacket(transId, elapsedTimeMs, mReply.build());
+            final List<IaPrefixOption> toBeRebound = mReply.getRenewableIaPrefixes();
+            if (toBeRebound.isEmpty()) return false;
+            return sendRebindPacket(transId, elapsedTimeMs, mReply.build(toBeRebound));
         }
     }
 
diff --git a/src/android/net/dhcp6/Dhcp6Packet.java b/src/android/net/dhcp6/Dhcp6Packet.java
index 7a977e5..e1dad15 100644
--- a/src/android/net/dhcp6/Dhcp6Packet.java
+++ b/src/android/net/dhcp6/Dhcp6Packet.java
@@ -278,12 +278,19 @@
          * Build an IA_PD option from given specific parameters, including IA_PREFIX options.
          */
         public ByteBuffer build() {
+            return build(ipos);
+        }
+
+        /**
+         * Build an IA_PD option from given specific parameters, including IA_PREFIX options.
+         */
+        public ByteBuffer build(@NonNull final List<IaPrefixOption> input) {
             final ByteBuffer iapd = ByteBuffer.allocate(IaPdOption.LENGTH
-                    + Struct.getSize(IaPrefixOption.class) * ipos.size());
+                    + Struct.getSize(IaPrefixOption.class) * input.size());
             iapd.putInt(iaid);
             iapd.putInt(t1);
             iapd.putInt(t2);
-            for (IaPrefixOption ipo : ipos) {
+            for (IaPrefixOption ipo : input) {
                 ipo.writeToByteBuffer(iapd);
             }
             iapd.flip();
@@ -332,6 +339,22 @@
                     (IaPrefixOption lhs, IaPrefixOption rhs) -> Long.compare(lhs.valid, rhs.valid));
             return ipo.valid;
         }
+
+        /**
+         * Return IA prefix option list to be renewed/rebound.
+         *
+         * Per RFC8415#section-18.2.4, client must not include any prefixes that it didn't obtain
+         * from server or that are no longer valid (that have a valid lifetime of 0). Section-18.3.4
+         * also mentions that server can inform client that it will not extend the prefix by setting
+         * T1 and T2 to values equal to the valid lifetime, so in this case client has no point in
+         * renewing as well.
+         */
+        public List<IaPrefixOption> getRenewableIaPrefixes() {
+            final List<IaPrefixOption> toBeRenewed = getValidIaPrefixes();
+            toBeRenewed.removeIf(ipo -> ipo.preferred == 0 && ipo.valid == 0);
+            toBeRenewed.removeIf(ipo -> t1 == ipo.valid && t2 == ipo.valid);
+            return toBeRenewed;
+        }
     }
 
     /**
diff --git a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
index 090b83d..40da4b7 100644
--- a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
+++ b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
@@ -5220,6 +5220,92 @@
         verify(mCb, never()).onLinkPropertiesChange(any());
     }
 
+    @SignatureRequiredTest(reason = "Need to mock the DHCP6 renew/rebind alarms")
+    @Test
+    public void testDhcp6Pd_renewInvalidPrefixes_zeroPreferredAndValidLifetime() throws Exception {
+        prepareDhcp6PdRenewTest();
+
+        final InOrder inOrder = inOrder(mAlarm);
+        final Handler handler = mDependencies.mDhcp6Client.getHandler();
+        final OnAlarmListener renewAlarm = expectAlarmSet(inOrder, "RENEW", 3600, handler);
+
+        handler.post(() -> renewAlarm.onAlarm());
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        Dhcp6Packet packet = getNextDhcp6Packet();
+        assertTrue(packet instanceof Dhcp6RenewPacket);
+
+        // Reply with the requested prefix with preferred/valid lifetime of 0.
+        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
+        final IpPrefix prefix1 = new IpPrefix("2001:db8:2::/64");
+        final IaPrefixOption ipo = buildIaPrefixOption(prefix, 0 /* preferred */,
+                0 /* valid */);
+        final IaPrefixOption ipo1 = buildIaPrefixOption(prefix1, 5000 /* preferred */,
+                6000 /* valid */);
+        final PrefixDelegation pd = new PrefixDelegation(packet.getIaId(), 3600 /* t1 */,
+                4500 /* t2 */, Arrays.asList(ipo, ipo1));
+        final ByteBuffer iapd = pd.build();
+        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
+                (Inet6Address) mClientIpAddress, false /* rapidCommit */));
+        verify(mCb, never()).onProvisioningFailure(any());
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(argThat(
+                x -> x.isIpv6Provisioned()
+                        && hasIpv6AddressPrefixedWith(x, prefix)
+                        && hasIpv6AddressPrefixedWith(x, prefix1)
+                        && hasRouteTo(x, "2001:db8:1::/64", RTN_UNREACHABLE)
+                        && hasRouteTo(x, "2001:db8:2::/64", RTN_UNREACHABLE)
+                        // IPv6 link-local, four global delegated IPv6 addresses
+                        && x.getLinkAddresses().size() == 5
+        ));
+
+        handler.post(() -> renewAlarm.onAlarm());
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        packet = getNextDhcp6Packet();
+        assertTrue(packet instanceof Dhcp6RenewPacket);
+        final List<IaPrefixOption> renewIpos = packet.getPrefixDelegation().ipos;
+        assertEquals(1, renewIpos.size()); // don't renew prefix 2001:db8:1::/64
+        assertEquals(prefix1, renewIpos.get(0).getIpPrefix());
+    }
+
+    @SignatureRequiredTest(reason = "Need to mock the DHCP6 renew/rebind alarms")
+    @Test
+    public void testDhcp6Pd_renewInvalidPrefixes_theSameT1T2ValidLifetime() throws Exception {
+        prepareDhcp6PdRenewTest();
+
+        final InOrder inOrder = inOrder(mAlarm);
+        final Handler handler = mDependencies.mDhcp6Client.getHandler();
+        final OnAlarmListener renewAlarm = expectAlarmSet(inOrder, "RENEW", 3600, handler);
+
+        handler.post(() -> renewAlarm.onAlarm());
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        Dhcp6Packet packet = getNextDhcp6Packet();
+        assertTrue(packet instanceof Dhcp6RenewPacket);
+
+        clearInvocations(mCb);
+
+        // Reply with the requested prefix with preferred/valid lifetime of 0.
+        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
+        final IaPrefixOption ipo = buildIaPrefixOption(prefix, 3600 /* preferred */,
+                3600 /* valid */);
+        final PrefixDelegation pd = new PrefixDelegation(packet.getIaId(), 3600 /* t1 */,
+                3600 /* t2 */, Collections.singletonList(ipo));
+        final ByteBuffer iapd = pd.build();
+        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
+                (Inet6Address) mClientIpAddress, false /* rapidCommit */));
+        // The prefix doesn't change only the lifetime is updated, therefore, LinkProperties update
+        // isn't expected.
+        verify(mCb, never()).onProvisioningFailure(any());
+        verify(mCb, never()).onLinkPropertiesChange(any());
+
+        handler.post(() -> renewAlarm.onAlarm());
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        packet = getNextDhcp6Packet(TEST_TIMEOUT_MS);
+        assertNull(packet);
+    }
+
     @Test
     @SignatureRequiredTest(reason = "InterfaceParams.getByName requires CAP_NET_ADMIN")
     public void testSendRtmDelAddressMethod() throws Exception {