Snap for 9992948 from d31736e3a0a53cd2995167086cc3d0f7d62bfc76 to mainline-media-swcodec-release

Change-Id: Ic9ef0827ec01db46cf41c5c12d2c048831a2b72a
diff --git a/src/android/net/apf/DnsUtils.java b/src/android/net/apf/DnsUtils.java
index e30ebc7..6b0bdc4 100644
--- a/src/android/net/apf/DnsUtils.java
+++ b/src/android/net/apf/DnsUtils.java
@@ -20,7 +20,6 @@
 import static android.net.apf.ApfGenerator.Register.R1;
 
 import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN;
-import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN;
 import static com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN;
 
 import androidx.annotation.NonNull;
@@ -264,20 +263,26 @@
         gen.addJump(jumpTable.getStartLabel());
     }
 
-    /**
-     * Returns the name of a jump label used while parsing the specified DNS label.
-     * TODO: use another scheme to name the labels. Using the label name does not work if the name
-     * to be matched contains duplicate labels.
-     */
-    private static String getPostMatchJumpTargetForLabel(String label) {
-        return label + "_parsed";
+    /** @return jump label that points to the start of a DNS label's parsing code. */
+    private static String getStartMatchLabel(int labelIndex) {
+        return "dns_parse_" + labelIndex;
+    }
+
+    /** @return jump label used while parsing the specified DNS label. */
+    private static String getPostMatchJumpTargetForLabel(int labelIndex) {
+        return "dns_parsed_" + labelIndex;
+    }
+
+    /** @return jump label used when the match for the specified DNS label fails. */
+    private static String getNoMatchLabel(int labelIndex) {
+        return "dns_nomatch_" + labelIndex;
     }
 
     private static void addMatchLabel(@NonNull ApfGenerator gen, @NonNull JumpTable jumpTable,
-            @NonNull String label, @NonNull String nextLabel) throws Exception {
-        final String parsedLabel = getPostMatchJumpTargetForLabel(label);
-        final String noMatchLabel = label + "_nomatch";
-        gen.defineLabel(label);
+            int labelIndex, @NonNull String label, @NonNull String nextLabel) throws Exception {
+        final String parsedLabel = getPostMatchJumpTargetForLabel(labelIndex);
+        final String noMatchLabel = getNoMatchLabel(labelIndex);
+        gen.defineLabel(getStartMatchLabel(labelIndex));
 
         // Store return address.
         gen.addLoadImmediate(R0, jumpTable.getIndex(parsedLabel));
@@ -319,45 +324,46 @@
      * packet, e.g., as used by MDNS. However, it currently only supports one DNS name.
      *
      * Limitations:
-     * - Filter size is just under 300 bytes for a typical question.
-     * - Because the bytecode extensively uses backwards jumps, it can hit the APF interpreter
+     * <ul>
+     * <li>Filter size is just under 300 bytes for a typical question.
+     * <li>Because the bytecode extensively uses backwards jumps, it can hit the APF interpreter
      *   instruction limit. This limit causes the APF interpreter to accept the packet once it has
      *   executed a number of instructions equal to the program length in bytes.
      *   A program that consists *only* of this filter will be able to execute just under 300
      *   instructions, and will be able to correctly drop packets with two questions but not three
      *   questions. In a real APF setup, there will be other code (e.g., RA filtering) which counts
      *   against the limit, so the filter should be able to parse packets with more questions.
-     * - Matches are case-sensitive. This is due to the use of JNEBS to match DNS labels and is
+     * <li>Matches are case-sensitive. This is due to the use of JNEBS to match DNS labels and is
      *   likely impossible to overcome without interpreter changes.
+     * </ul>
      *
      * TODO:
-     * - Add unit tests for the parse_dns_label and find_next_dns_question functions.
-     * - Add an efficient way to parse the first question in the packet. This can be done much more
-     *   efficiently because the first name cannot be compressed.
-     * - Support accepting more than one name.
-     * - For devices where power saving is a priority (e.g., flat panel TVs), add support for
+     * <ul>
+     * <li>Add unit tests for the parse_dns_label and find_next_dns_question functions.
+     * <li>Support accepting more than one name.
+     * <li>For devices where power saving is a priority (e.g., flat panel TVs), add support for
      *   dropping packets with more than X queries, to ensure the filter will drop the packet rather
      *   than hit the instruction limit.
+     * </ul>
      */
-    public static void generateFilter(ApfGenerator gen, boolean ipv6, String[] labels)
-            throws Exception {
+    public static void generateFilter(ApfGenerator gen, String[] labels) throws Exception {
         final int etherPlusUdpLen = ETHER_HEADER_LEN + UDP_HEADER_LEN;
 
         final String labelJumpTable = "jump_table";
 
         // Initialize parsing
         /**
+         * - R1: length of IP header.
          * - m[SLOT_DNS_HEADER_OFFSET]: offset of DNS header
          * - m[SLOT_CURRENT_PARSE_OFFSET]: current parsing offset (start of question section)
          * - m[SLOT_AFTER_POINTER_OFFSET]: offset after first pointer in name, must be 0 when
          *                                 starting a new name
          * - m[SLOT_NEGATIVE_QDCOUNT_REMAINING]: negative qdcount
          */
-        if (ipv6) {
-            gen.addLoadImmediate(R0, IPV6_HEADER_LEN);
-        } else {
-            gen.addLoadFromMemory(R0, ApfGenerator.IPV4_HEADER_SIZE_MEMORY_SLOT);
-        }
+        // Move IP header length to R0 and use it to find the DNS header offset.
+        // TODO: this uses R1 for consistency with ApfFilter#generateMdnsFilterLocked. Evaluate
+        // using R0 instead.
+        gen.addMove(R0);
         gen.addAdd(etherPlusUdpLen);
         gen.addStoreToMemory(R0, SLOT_DNS_HEADER_OFFSET);
 
@@ -386,7 +392,7 @@
         // calls below) because otherwise all the jumps are backwards, and backwards jumps are more
         // expensive (5 bytes of bytecode)
         for (int i = 0; i < labels.length; i++) {
-            table.addLabel(getPostMatchJumpTargetForLabel(labels[i]));
+            table.addLabel(getPostMatchJumpTargetForLabel(i));
         }
         table.addLabel(LABEL_START_MATCH);
         table.generate(gen);
@@ -396,8 +402,8 @@
         for (int i = 0; i < labels.length; i++) {
             final String nextLabel = (i == labels.length - 1)
                     ? ApfGenerator.PASS_LABEL
-                    : labels[i + 1];
-            addMatchLabel(gen, table, labels[i], nextLabel);
+                    : getStartMatchLabel(i + 1);
+            addMatchLabel(gen, table, i, labels[i], nextLabel);
         }
         gen.addJump(ApfGenerator.DROP_LABEL);
     }
diff --git a/src/android/net/dhcp6/Dhcp6Client.java b/src/android/net/dhcp6/Dhcp6Client.java
new file mode 100644
index 0000000..3fa7cb0
--- /dev/null
+++ b/src/android/net/dhcp6/Dhcp6Client.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2023 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.dhcp6;
+
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOCK_NONBLOCK;
+
+import static com.android.net.module.util.NetworkStackConstants.ALL_DHCP_RELAY_AGENTS_AND_SERVERS;
+import static com.android.net.module.util.NetworkStackConstants.DHCP6_CLIENT_PORT;
+import static com.android.net.module.util.NetworkStackConstants.DHCP6_SERVER_PORT;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ANY;
+
+import android.content.Context;
+import android.net.ip.IpClient;
+import android.net.util.SocketUtils;
+import android.os.Handler;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.util.StateMachine;
+import com.android.net.module.util.InterfaceParams;
+import com.android.net.module.util.PacketReader;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+
+/**
+ * A DHCPv6 client.
+ *
+ * So far only support IA_PD (prefix delegation), not for IA_NA/IA_TA yet.
+ *
+ * @hide
+ */
+public class Dhcp6Client extends StateMachine {
+    private static final String TAG = Dhcp6Client.class.getSimpleName();
+    private static final boolean DBG = true;
+
+    // Internal messages.
+    // Dhcp6Client shares the same handler with IpClient, define the base command range for
+    // both public and private messages used in Dhcp6Client, to avoid commands overlap.
+    private static final int PRIVATE_BASE         = IpClient.DHCP6CLIENT_CMD_BASE + 100;
+    private static final int CMD_RECEIVED_PACKET  = PRIVATE_BASE + 1;
+
+    @NonNull private final Context mContext;
+
+    // State variables.
+    @NonNull private final StateMachine mController;
+    @NonNull private final String mIfaceName;
+
+    private Dhcp6Client(@NonNull final Context context, @NonNull final StateMachine controller,
+            @NonNull final InterfaceParams iface) {
+        super(TAG, controller.getHandler());
+
+        mContext = context;
+        mController = controller;
+        mIfaceName = iface.name;
+
+        // TODO: add state machine initialization.
+    }
+
+    private class Dhcp6PacketHandler extends PacketReader {
+        private FileDescriptor mUdpSock;
+
+        Dhcp6PacketHandler(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        protected void handlePacket(byte[] recvbuf, int length) {
+            try {
+                final Dhcp6Packet packet = Dhcp6Packet.decodePacket(recvbuf, length);
+                if (DBG) Log.d(TAG, "Received packet: " + packet);
+                sendMessage(CMD_RECEIVED_PACKET, packet);
+            } catch (Dhcp6Packet.ParseException e) {
+                Log.e(TAG, "Can't parse DHCPv6 packet: " + e.getMessage());
+            }
+        }
+
+        @Override
+        protected FileDescriptor createFd() {
+            try {
+                mUdpSock = Os.socket(AF_INET6, SOCK_DGRAM | SOCK_NONBLOCK, IPPROTO_UDP);
+                SocketUtils.bindSocketToInterface(mUdpSock, mIfaceName);
+                Os.bind(mUdpSock, IPV6_ADDR_ANY, DHCP6_CLIENT_PORT);
+            } catch (SocketException | ErrnoException e) {
+                Log.e(TAG, "Error creating udp socket", e);
+                closeFd(mUdpSock);
+                mUdpSock = null;
+                return null;
+            }
+            return mUdpSock;
+        }
+
+        @Override
+        protected int readPacket(FileDescriptor fd, byte[] packetBuffer) throws Exception {
+            try {
+                return Os.read(fd, packetBuffer, 0, packetBuffer.length);
+            } catch (IOException | ErrnoException e) {
+                Log.e(TAG, "Fail to read packet");
+                throw e;
+            }
+        }
+
+        public int transmitPacket(final ByteBuffer buf) throws ErrnoException, SocketException {
+            int ret = Os.sendto(mUdpSock, buf.array(), 0 /* byteOffset */,
+                    buf.limit() /* byteCount */, 0 /* flags */, ALL_DHCP_RELAY_AGENTS_AND_SERVERS,
+                    DHCP6_SERVER_PORT);
+            return ret;
+        }
+    }
+}
diff --git a/src/android/net/dhcp6/Dhcp6Packet.java b/src/android/net/dhcp6/Dhcp6Packet.java
index 98c214b..6d77d4e 100644
--- a/src/android/net/dhcp6/Dhcp6Packet.java
+++ b/src/android/net/dhcp6/Dhcp6Packet.java
@@ -180,7 +180,7 @@
         }
     }
 
-    private static void skipOption(final ByteBuffer packet, int optionLen)
+    private static void skipOption(@NonNull final ByteBuffer packet, int optionLen)
             throws BufferUnderflowException {
         for (int i = 0; i < optionLen; i++) {
             packet.get();
@@ -297,6 +297,8 @@
                         statusMsg = readAsciiString(packet, expectedLen - 2, false /* isNullOk */);
                         break;
                     default:
+                        expectedLen = optionLen;
+                        // BufferUnderflowException will be thrown if option is truncated.
                         skipOption(packet, optionLen);
                         break;
                 }
@@ -325,6 +327,12 @@
             case DHCP6_MESSAGE_TYPE_REPLY:
                 newPacket = new Dhcp6ReplyPacket(transId, clientDuid, serverDuid, iapd);
                 break;
+            case DHCP6_MESSAGE_TYPE_RENEW:
+                newPacket = new Dhcp6RenewPacket(transId, secs, clientDuid, serverDuid, iapd);
+                break;
+            case DHCP6_MESSAGE_TYPE_REBIND:
+                newPacket = new Dhcp6RebindPacket(transId, secs, clientDuid, iapd);
+                break;
             default:
                 throw new ParseException("Unimplemented DHCP6 message type %d" + messageType);
         }
@@ -345,6 +353,15 @@
     }
 
     /**
+     * Parse a packet from an array of bytes, stopping at the given length.
+     */
+    public static Dhcp6Packet decodePacket(@NonNull final byte[] packet, int length)
+            throws ParseException {
+        final ByteBuffer buffer = ByteBuffer.wrap(packet, 0, length).order(ByteOrder.BIG_ENDIAN);
+        return decodePacket(buffer);
+    }
+
+    /**
      * Adds an optional parameter containing an array of bytes.
      */
     protected static void addTlv(ByteBuffer buf, short type, @NonNull byte[] payload) {
@@ -403,4 +420,23 @@
                 new Dhcp6RequestPacket(transId, secs, clientDuid, serverDuid, iapd);
         return pkt.buildPacket();
     }
+
+    /**
+     * Builds a DHCPv6 RENEW packet from the required specified parameters.
+     */
+    public static ByteBuffer buildRenewPacket(int transId, short secs, @NonNull final byte[] iapd,
+            @NonNull final byte[] clientDuid, @NonNull final byte[] serverDuid) {
+        final Dhcp6RenewPacket pkt =
+                new Dhcp6RenewPacket(transId, secs, clientDuid, serverDuid, iapd);
+        return pkt.buildPacket();
+    }
+
+    /**
+     * Builds a DHCPv6 REBIND packet from the required specified parameters.
+     */
+    public static ByteBuffer buildRebindPacket(int transId, short secs, @NonNull final byte[] iapd,
+            @NonNull final byte[] clientDuid) {
+        final Dhcp6RebindPacket pkt = new Dhcp6RebindPacket(transId, secs, clientDuid, iapd);
+        return pkt.buildPacket();
+    }
 }
diff --git a/src/android/net/dhcp6/Dhcp6RebindPacket.java b/src/android/net/dhcp6/Dhcp6RebindPacket.java
new file mode 100644
index 0000000..10c10b9
--- /dev/null
+++ b/src/android/net/dhcp6/Dhcp6RebindPacket.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2023 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.dhcp6;
+
+import static com.android.net.module.util.NetworkStackConstants.DHCP_MAX_LENGTH;
+
+import androidx.annotation.NonNull;
+
+import java.nio.ByteBuffer;
+
+/**
+ * DHCPv6 REBIND packet class, a client sends a Rebind message to any available server to extend
+ * the lifetimes on the leases assigned to the client and to update other configuration parameters.
+ * This message is sent after a client receives no response to a Renew message.
+ *
+ * https://tools.ietf.org/html/rfc8415#page-24
+ */
+public class Dhcp6RebindPacket extends Dhcp6Packet {
+    /**
+     * Generates a rebind packet with the specified parameters.
+     */
+    Dhcp6RebindPacket(int transId, short secs, @NonNull final byte[] clientDuid,
+            @NonNull final byte[] iapd) {
+        super(transId, secs, clientDuid, null /* serverDuid */, iapd);
+    }
+
+    /**
+     * Build a DHCPv6 Rebind message with the specific parameters.
+     */
+    public ByteBuffer buildPacket() {
+        final ByteBuffer packet = ByteBuffer.allocate(DHCP_MAX_LENGTH);
+        final int msgTypeAndTransId = (DHCP6_MESSAGE_TYPE_REBIND << 24) | (mTransId & 0x0FFF);
+        packet.putInt(msgTypeAndTransId);
+
+        addTlv(packet, DHCP6_CLIENT_IDENTIFIER, getClientDuid());
+        addTlv(packet, DHCP6_ELAPSED_TIME, mSecs);
+        addTlv(packet, DHCP6_IA_PD, mIaPd);
+
+        packet.flip();
+        return packet;
+    }
+}
diff --git a/src/android/net/dhcp6/Dhcp6RenewPacket.java b/src/android/net/dhcp6/Dhcp6RenewPacket.java
new file mode 100644
index 0000000..8715b59
--- /dev/null
+++ b/src/android/net/dhcp6/Dhcp6RenewPacket.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2023 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.dhcp6;
+
+import static com.android.net.module.util.NetworkStackConstants.DHCP_MAX_LENGTH;
+
+import androidx.annotation.NonNull;
+
+import java.nio.ByteBuffer;
+
+/**
+ * DHCPv6 RENEW packet class, a client sends an Renew message to the server that originally
+ * provided the client's leases and configuration parameters to extend the lifetimes on the
+ * leases assigned to the client and to update other configuration parameters.
+ *
+ * https://tools.ietf.org/html/rfc8415#page-24
+ */
+public class Dhcp6RenewPacket extends Dhcp6Packet {
+    /**
+     * Generates a renew packet with the specified parameters.
+     */
+    Dhcp6RenewPacket(int transId, short secs, @NonNull final byte[] clientDuid,
+            @NonNull final byte[] serverDuid, final byte[] iapd) {
+        super(transId, secs, clientDuid, serverDuid, iapd);
+    }
+
+    /**
+     * Build a DHCPv6 Renew message with the specific parameters.
+     */
+    public ByteBuffer buildPacket() {
+        final ByteBuffer packet = ByteBuffer.allocate(DHCP_MAX_LENGTH);
+        final int msgTypeAndTransId = (DHCP6_MESSAGE_TYPE_RENEW << 24) | (mTransId & 0x0FFF);
+        packet.putInt(msgTypeAndTransId);
+
+        addTlv(packet, DHCP6_SERVER_IDENTIFIER, getServerDuid());
+        addTlv(packet, DHCP6_CLIENT_IDENTIFIER, getClientDuid());
+        addTlv(packet, DHCP6_ELAPSED_TIME, mSecs);
+        addTlv(packet, DHCP6_IA_PD, mIaPd);
+
+        packet.flip();
+        return packet;
+    }
+}
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java
index 9abaf92..5737d87 100644
--- a/src/android/net/ip/IpClient.java
+++ b/src/android/net/ip/IpClient.java
@@ -500,6 +500,9 @@
     // IpClient shares a handler with DhcpClient: commands must not overlap
     public static final int DHCPCLIENT_CMD_BASE = 1000;
 
+    // IpClient shares a handler with Dhcp6Client: commands must not overlap
+    public static final int DHCP6CLIENT_CMD_BASE = 2000;
+
     // Settings and default values.
     private static final int MAX_LOG_RECORDS = 500;
     private static final int MAX_PACKET_RECORDS = 100;
diff --git a/src/android/net/ip/IpClientLinkObserver.java b/src/android/net/ip/IpClientLinkObserver.java
index 0771bff..71632af 100644
--- a/src/android/net/ip/IpClientLinkObserver.java
+++ b/src/android/net/ip/IpClientLinkObserver.java
@@ -65,6 +65,7 @@
 
 import java.net.Inet6Address;
 import java.net.InetAddress;
+import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -539,7 +540,17 @@
             if (!mNetlinkEventParsingEnabled) return;
             final String[] addresses = new String[opt.servers.length];
             for (int i = 0; i < opt.servers.length; i++) {
-                addresses[i] = opt.servers[i].getHostAddress();
+                Inet6Address addr = (Inet6Address) opt.servers[i];
+                if (addr.isLinkLocalAddress()) {
+                    try {
+                        addr = Inet6Address.getByAddress(null /* hostname */, addr.getAddress(),
+                                mIfindex);
+                    } catch (UnknownHostException impossible) {
+                        Log.wtf("Cannot construct scoped Inet6Address with Inet6Address.getAddress("
+                                + addr.getHostAddress() + "): ", impossible);
+                    }
+                }
+                addresses[i] = addr.getHostAddress();
             }
             updateInterfaceDnsServerInfo(opt.header.lifetime, addresses);
         }
diff --git a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
index 9790ec9..2fb5ac8 100644
--- a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
+++ b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
@@ -729,7 +729,14 @@
         final TestNetworkInterface iface = runAsShell(MANAGE_TEST_NETWORKS, () -> {
             final TestNetworkManager tnm =
                     inst.getContext().getSystemService(TestNetworkManager.class);
-            return tnm.createTapInterface();
+            try {
+                return tnm.createTapInterface(true /* carrierUp */, true /* bringUp */,
+                        true /* disableIpv6ProvisioningDelay */);
+            } catch (NoSuchMethodError e) {
+                // createTapInterface(boolean, boolean, boolean) has been introduced since T,
+                // use the legancy API if the method is not found on previous platforms.
+                return tnm.createTapInterface();
+            }
         });
         mIfaceName = iface.getInterfaceName();
         mClientMac = getIfaceMacAddr(mIfaceName).toByteArray();
@@ -2039,6 +2046,33 @@
         reset(mCb);
     }
 
+    @Test
+    public void testRaRdnss_Ipv6LinkLocalDns() throws Exception {
+        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIpReachabilityMonitor()
+                .withoutIPv4()
+                .build();
+        startIpClientProvisioning(config);
+
+        final ByteBuffer pio = buildPioOption(600, 300, "2001:db8:1::/64");
+        // put an IPv6 link-local DNS server
+        final ByteBuffer rdnss = buildRdnssOption(600, ROUTER_LINK_LOCAL.getHostAddress());
+        // put SLLA option to avoid address resolution for "fe80::1"
+        final ByteBuffer slla = buildSllaOption();
+        final ByteBuffer ra = buildRaPacket(pio, rdnss, slla);
+
+        waitForRouterSolicitation();
+        mPacketReader.sendResponse(ra);
+
+        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
+        final LinkProperties lp = captor.getValue();
+        assertNotNull(lp);
+        assertEquals(1, lp.getDnsServers().size());
+        assertEquals(ROUTER_LINK_LOCAL, (Inet6Address) lp.getDnsServers().get(0));
+        assertTrue(lp.isIpv6Provisioned());
+    }
+
     private void expectNat64PrefixUpdate(InOrder inOrder, IpPrefix expected) throws Exception {
         inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(
                 argThat(lp -> Objects.equals(expected, lp.getNat64Prefix())));
@@ -2882,10 +2916,12 @@
         final InOrder inOrder = inOrder(mCb);
         final CompletableFuture<LinkProperties> lpFuture = new CompletableFuture<>();
 
-        // Start IPv4 provisioning first and then IPv6 provisioning, which is more realistic.
-        // And wait until entire provisioning completes.
+        // Start IPv4 provisioning first and wait IPv4 provisioning to succeed, and then start
+        // IPv6 provisioning, which is more realistic and avoid the flaky case of both IPv4 and
+        // IPv6 provisioning complete at the same time.
         handleDhcpPackets(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
                 true /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU, null /* serverSentUrl */);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(any());
 
         waitForRouterSolicitation();
         mPacketReader.sendResponse(ra);
diff --git a/tests/unit/src/android/net/apf/ApfTest.java b/tests/unit/src/android/net/apf/ApfTest.java
index 46c36a0..0b3bba7 100644
--- a/tests/unit/src/android/net/apf/ApfTest.java
+++ b/tests/unit/src/android/net/apf/ApfTest.java
@@ -189,7 +189,12 @@
     private void assertVerdict(int expected, byte[] program, byte[] packet, int filterAge) {
         final String msg = "Unexpected APF verdict. To debug:\n"
                 + "  apf_run --program " + HexDump.toHexString(program)
-                + " --packet " +  HexDump.toHexString(packet) + " --trace | less\n  ";
+                + " --packet " + HexDump.toHexString(packet) + " --trace | less\n  ";
+        assertReturnCodesEqual(msg, expected, apfSimulate(program, packet, null, filterAge));
+    }
+
+    private void assertVerdict(String msg, int expected, byte[] program, byte[] packet,
+            int filterAge) {
         assertReturnCodesEqual(msg, expected, apfSimulate(program, packet, null, filterAge));
     }
 
@@ -1395,6 +1400,25 @@
         return builder.finalizePacket().array();
     }
 
+    /** Adds to the program a no-op instruction that is one byte long. */
+    private void addOneByteNoop(ApfGenerator gen) {
+        gen.addOr(0);
+    }
+
+    @Test
+    public void testAddOneByteNoopAddsOneByte() throws Exception {
+        ApfGenerator gen = new ApfGenerator(MIN_APF_VERSION);
+        addOneByteNoop(gen);
+        assertEquals(1, gen.generate().length);
+
+        final int count = 42;
+        gen = new ApfGenerator(MIN_APF_VERSION);
+        for (int i = 0; i < count; i++) {
+            addOneByteNoop(gen);
+        }
+        assertEquals(count, gen.generate().length);
+    }
+
     @Test
     public void testQnameEncoding() {
         String[] qname = new String[]{"abcd", "ef", "日本"};
@@ -1457,15 +1481,21 @@
         apfFilter.shutdown();
     }
 
+    private ApfGenerator generateDnsFilter(boolean ipv6, String... labels) throws Exception {
+        ApfGenerator gen = new ApfGenerator(MIN_APF_VERSION);
+        gen.addLoadImmediate(R1, ipv6 ? IPV6_HEADER_LEN : IPV4_HEADER_LEN);
+        DnsUtils.generateFilter(gen, labels);
+        return gen;
+    }
+
     private void doTestDnsParsing(boolean expectPass, boolean ipv6, String filterName,
             byte[] pkt) throws Exception {
-        ApfGenerator gen = new ApfGenerator(MIN_APF_VERSION);
         final String[] labels = filterName.split(/*regex=*/ "[.]");
-        DnsUtils.generateFilter(gen, ipv6, labels);
+        ApfGenerator gen = generateDnsFilter(ipv6, labels);
 
         // Hack to prevent the APF instruction limit triggering.
         for (int i = 0; i < 500; i++) {
-            gen.addOr(0);
+            addOneByteNoop(gen);
         }
 
         byte[] program = gen.generate();
@@ -1488,6 +1518,7 @@
         final boolean ipv4 = false, ipv6 = true;
 
         // Packets with one question.
+        // Names don't start with _ because DnsPacket thinks such names are invalid.
         doTestDnsParsing(true, ipv6, "googlecast.tcp.local", "googlecast.tcp.local");
         doTestDnsParsing(true, ipv4, "googlecast.tcp.local", "googlecast.tcp.local");
         doTestDnsParsing(false, ipv6, "googlecast.tcp.lozal", "googlecast.tcp.local");
@@ -1510,6 +1541,9 @@
         doTestDnsParsing(true, ipv6, "googlecast.tcp.local",
                 "www.google.co.jp", "googlecast.tcp.local", "developer.android.com");
 
+        // Name with duplicate labels.
+        doTestDnsParsing(true, ipv6, "local.tcp.local", "local.tcp.local");
+
         final byte[] pkt = makeMdnsCompressedV6Packet();
         doTestDnsParsing(true, ipv6, "googlecast.tcp.local", pkt);
         doTestDnsParsing(true, ipv6, "matter.tcp.local", pkt);
@@ -1517,6 +1551,106 @@
         doTestDnsParsing(false, ipv6, "otherservice.tcp.local", pkt);
     }
 
+    private void doTestDnsParsingProgramLength(int expectedLength,
+            String filterName) throws Exception {
+        final String[] labels = filterName.split(/*regex=*/ "[.]");
+
+        ApfGenerator gen = generateDnsFilter(/*ipv6=*/ true, labels);
+        assertEquals("Program for " + filterName + " had unexpected length:",
+                expectedLength, gen.generate().length);
+    }
+
+    /**
+     * Rough metric of code size. Checks how large the generated filter is in various scenarios.
+     * Helps ensure any changes to the code do not substantially increase APF code size.
+     */
+    @Test
+    public void testDnsParsingProgramLength() throws Exception {
+        doTestDnsParsingProgramLength(237, "MyDevice.local");
+        doTestDnsParsingProgramLength(285, "_googlecast.tcp.local");
+        doTestDnsParsingProgramLength(291, "_googlecast12345.tcp.local");
+        doTestDnsParsingProgramLength(244, "_googlecastZtcp.local");
+        doTestDnsParsingProgramLength(249, "_googlecastZtcp12345.local");
+    }
+
+    private void doTestDnsParsingNecessaryOverhead(int expectedNecessaryOverhead,
+            String filterName, byte[] pkt, String description) throws Exception {
+        final String[] labels = filterName.split(/*regex=*/ "[.]");
+
+        // Check that the generated code, when the program contains the specified number of extra
+        // bytes, is capable of dropping the packet.
+        ApfGenerator gen = generateDnsFilter(/*ipv6=*/ true, labels);
+        for (int i = 0; i < expectedNecessaryOverhead; i++) {
+            addOneByteNoop(gen);
+        }
+        final byte[] programWithJustEnoughOverhead = gen.generate();
+        assertVerdict(
+                "Overhead too low: filter for " + filterName + " with " + expectedNecessaryOverhead
+                        + " extra instructions unexpectedly passed " + description,
+                DROP, programWithJustEnoughOverhead, pkt, 0);
+
+        if (expectedNecessaryOverhead == 0) return;
+
+        // Check that the generated code, without the specified number of extra program bytes,
+        // cannot correctly drop the packet because it hits the interpreter instruction limit.
+        gen = generateDnsFilter(/*ipv6=*/ true, labels);
+        for (int i = 0; i < expectedNecessaryOverhead - 1; i++) {
+            addOneByteNoop(gen);
+        }
+        final byte[] programWithNotEnoughOverhead = gen.generate();
+
+        assertVerdict(
+                "Overhead too high: filter for " + filterName + " with " + expectedNecessaryOverhead
+                        + " extra instructions unexpectedly dropped " + description,
+                PASS, programWithNotEnoughOverhead, pkt, 0);
+    }
+
+    private void doTestDnsParsingNecessaryOverhead(int expectedNecessaryOverhead,
+            String filterName, String... packetNames) throws Exception {
+        doTestDnsParsingNecessaryOverhead(expectedNecessaryOverhead, filterName,
+                makeMdnsV6Packet(packetNames),
+                "IPv6 MDNS packet containing: " + Arrays.toString(packetNames));
+    }
+
+    /**
+     * Rough metric of filter efficiency. Because the filter uses backwards jumps, on complex
+     * packets it will not finish running before the interpreter hits the maximum number of allowed
+     * instructions (== number of bytes in the program) and unconditionally accepts the packet.
+     * This test checks much extra code the program must contain in order for the generated filter
+     * to successfully drop the packet. It helps ensure any changes to the code do not reduce the
+     * complexity of packets that the APF code can drop.
+     */
+    @Test
+    public void testDnsParsingNecessaryOverhead() throws Exception {
+        // Simple packets can be parsed with zero extra code.
+        doTestDnsParsingNecessaryOverhead(0, "googlecast.tcp.local",
+                "matter.tcp.local", "developer.android.com");
+
+        doTestDnsParsingNecessaryOverhead(0, "googlecast.tcp.local",
+                "developer.android.com", "matter.tcp.local");
+
+        doTestDnsParsingNecessaryOverhead(0, "googlecast.tcp.local",
+                "developer.android.com", "matter.tcp.local", "www.google.co.jp");
+
+        doTestDnsParsingNecessaryOverhead(0, "googlecast.tcp.local",
+                "developer.android.com", "matter.tcp.local", "www.google.co.jp",
+                "example.org");
+
+        // More complicated packets cause more instructions to be run and can only be dropped if
+        // the program contains lots of extra code.
+        doTestDnsParsingNecessaryOverhead(57, "googlecast.tcp.local",
+                "developer.android.com", "matter.tcp.local", "www.google.co.jp",
+                "example.org", "otherexample.net");
+
+        doTestDnsParsingNecessaryOverhead(115, "googlecast.tcp.local",
+                "developer.android.com", "matter.tcp.local", "www.google.co.jp",
+                "example.org", "otherexample.net", "docs.new");
+
+        doTestDnsParsingNecessaryOverhead(0, "foo.tcp.local",
+                makeMdnsCompressedV6Packet(), "compressed packet");
+
+    }
+
     @Test
     public void testApfFilterMulticast() throws Exception {
         final byte[] unicastIpv4Addr   = {(byte)192,0,2,63};
diff --git a/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt b/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt
index 264f785..2d093f7 100644
--- a/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt
+++ b/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt
@@ -53,7 +53,7 @@
                 "01000F51" +
                 // client identifier option(option_len=12)
                 "0001000C0003001B024CCBFFFE5F6EA9" +
-                // elapsed time option(wrong option_len: 4)
+                // elapsed time option(wrong option_len=4)
                 "000800040000" +
                 // IA_PD option(option_len=41, including IA prefix option)
                 "00190029DE3570F50000000000000000" +
@@ -102,4 +102,47 @@
                 Dhcp6Packet.decodePacket(ByteBuffer.wrap(bytes))
         }
     }
+
+    @Test
+    fun testDecodeDhcp6AdvertisePacket() {
+        val advertiseHex =
+                // Advertise, Transaction ID
+                "0200078A" +
+                // server identifier option(option_len=10)
+                "0002000A0003000186C9B26AED4D" +
+                // client identifier option(option_len=12)
+                "0001000C0003001B024CCBFFFE5F6EA9" +
+                // IA_PD option(option_len=70, including IA prefix option)
+                "001900460CDDCA0C000000CF0000014C" +
+                // IA prefix option(option_len=25, prefix="2401:fa00:49c:412::/64")
+                "001A00190000019F0000064F402401FA00049C04810000000000000000" +
+                // IA prefix option(option_len=25, prefix="fdfd:9ed6:7950:2::/64")
+                "001A00190000019F0000A8C040FDFD9ED6795000010000000000000000"
+        val bytes = HexDump.hexStringToByteArray(advertiseHex)
+        val packet = Dhcp6Packet.decodePacket(ByteBuffer.wrap(bytes))
+        assertTrue(packet is Dhcp6AdvertisePacket)
+    }
+
+    @Test
+    fun testDecodeDhcp6SolicitPacket_unsupportedOption() {
+        val advertiseHex =
+                // Advertise, Transaction ID
+                "0200078A" +
+                // server identifier option(option_len=10)
+                "0002000A0003000186C9B26AED4D" +
+                // client identifier option(option_len=12)
+                "0001000C0003001B024CCBFFFE5F6EA9" +
+                // SOL_MAX_RT (don't support this option yet)
+                "005200040000003C" +
+                // IA_PD option(option_len=70, including IA prefix option)
+                "001900460CDDCA0C000000CF0000014C" +
+                // IA prefix option(option_len=25, prefix="2401:fa00:49c:412::/64")
+                "001A00190000019F0000064F402401FA00049C04810000000000000000" +
+                // IA prefix option(option_len=25, prefix="fdfd:9ed6:7950:2::/64")
+                "001A00190000019F0000A8C040FDFD9ED6795000010000000000000000"
+        val bytes = HexDump.hexStringToByteArray(advertiseHex)
+        // The unsupported option will be skipped normally and won't throw ParseException.
+        val packet = Dhcp6Packet.decodePacket(ByteBuffer.wrap(bytes))
+        assertTrue(packet is Dhcp6AdvertisePacket)
+    }
 }