Snap for 11296156 from 8a5811fa39b7fe3b4193ba7619e499418eed7185 to mainline-tzdata5-release

Change-Id: Iaece0b368116d208ed5f04f43b18ed3c068f889c
diff --git a/Android.bp b/Android.bp
index 4461c26..a7f57ae 100644
--- a/Android.bp
+++ b/Android.bp
@@ -58,6 +58,7 @@
     name: "NetworkStackNextEnableDefaults",
     enabled: true,
 }
+
 // This is a placeholder comment to avoid merge conflicts
 // as the above target may have different "enabled" values
 // depending on the branch
@@ -72,7 +73,7 @@
         "framework-connectivity-t",
         "framework-statsd",
         "framework-wifi",
-    ]
+    ],
 }
 
 // Common defaults for NetworkStack integration tests, root tests and coverage tests
@@ -85,7 +86,7 @@
 
 java_defaults {
     name: "NetworkStackReleaseApiLevel",
-    defaults:["NetworkStackReleaseTargetSdk"],
+    defaults: ["NetworkStackReleaseTargetSdk"],
     sdk_version: module_34_version,
     libs: [
         "framework-configinfrastructure",
@@ -93,7 +94,7 @@
         "framework-connectivity-t",
         "framework-statsd",
         "framework-wifi",
-    ]
+    ],
 }
 
 // Libraries for the API shims
@@ -103,12 +104,12 @@
         "androidx.annotation_annotation",
         "networkstack-aidl-latest",
     ],
-    static_libs : [
-        "modules-utils-build_system"
+    static_libs: [
+        "modules-utils-build_system",
     ],
     apex_available: [
         "com.android.tethering",
-        "//apex_available:platform",  // For InProcessNetworkStack
+        "//apex_available:platform", // For InProcessNetworkStack
     ],
     min_sdk_version: "30",
 }
@@ -124,6 +125,9 @@
     srcs: ["apishim/common/**/*.java"],
     sdk_version: "system_current",
     visibility: ["//visibility:private"],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Each level of the shims (29, 30, ...) is its own java_library compiled against the corresponding
@@ -137,6 +141,9 @@
     ],
     sdk_version: "system_29",
     visibility: ["//visibility:private"],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 java_library {
@@ -153,6 +160,7 @@
     visibility: ["//visibility:private"],
     lint: {
         strict_updatability_linting: true,
+        baseline_filename: "lint-baseline.xml",
     },
 }
 
@@ -176,6 +184,9 @@
     ],
     sdk_version: "module_31",
     visibility: ["//visibility:private"],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 java_library {
@@ -196,6 +207,9 @@
     ],
     sdk_version: "module_33",
     visibility: ["//visibility:private"],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 java_library {
@@ -217,6 +231,9 @@
     ],
     sdk_version: module_34_version,
     visibility: ["//visibility:private"],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Shims for APIs being added to the current development version of Android. These APIs are not
@@ -228,7 +245,10 @@
 // are part of the stable shims and scanned when generating jarjar rules.
 java_library {
     name: "NetworkStackApi35Shims",
-    defaults: ["NetworkStackShimsDefaults", "ConnectivityNextEnableDefaults"],
+    defaults: [
+        "NetworkStackShimsDefaults",
+        "ConnectivityNextEnableDefaults",
+    ],
     srcs: [
         "apishim/35/**/*.java",
     ],
@@ -243,10 +263,13 @@
         "framework-connectivity",
         "framework-connectivity-t.stubs.module_lib",
         "framework-tethering",
-        "android.net.ipsec.ike.stubs.module_lib"
+        "android.net.ipsec.ike.stubs.module_lib",
     ],
     sdk_version: "module_current",
     visibility: ["//visibility:private"],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // API current uses the API current shims directly.
@@ -274,6 +297,9 @@
         "//packages/modules/Connectivity/service-t",
         "//packages/modules/Connectivity/tests:__subpackages__",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // API stable uses jarjar to rename the latest stable apishim package from
@@ -281,7 +307,10 @@
 // the networkstack code.
 java_library {
     name: "NetworkStackApiStableShims",
-    defaults: ["NetworkStackShimsDefaults", "NetworkStackReleaseApiLevel"],
+    defaults: [
+        "NetworkStackShimsDefaults",
+        "NetworkStackReleaseApiLevel",
+    ],
     static_libs: [
         "NetworkStackShimsCommon",
         "NetworkStackApi29Shims",
@@ -297,6 +326,9 @@
         "//packages/modules/Connectivity/service-t",
         "//packages/modules/Connectivity/tests:__subpackages__",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Common defaults for android libraries containing network stack code, used to compile variants of
@@ -336,7 +368,7 @@
     ],
     srcs: [
         "src/**/*.java",
-        ":statslog-networkstack-java-gen-current"
+        ":statslog-networkstack-java-gen-current",
     ],
     static_libs: [
         "NetworkStackApiCurrentShims",
@@ -348,12 +380,18 @@
         "//packages/modules/NetworkStack/tests/unit",
         "//packages/modules/NetworkStack/tests/integration",
     ],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 android_library {
     name: "NetworkStackApiStableLib",
-    defaults: ["NetworkStackReleaseApiLevel", "NetworkStackAndroidLibraryDefaults"],
+    defaults: [
+        "NetworkStackReleaseApiLevel",
+        "NetworkStackAndroidLibraryDefaults",
+    ],
     srcs: [
         "src/**/*.java",
         ":statslog-networkstack-java-gen-stable",
@@ -370,7 +408,10 @@
         "//packages/modules/NetworkStack/tests/unit",
         "//packages/modules/NetworkStack/tests/integration",
     ],
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 java_library {
@@ -396,6 +437,9 @@
         "//packages/modules/Connectivity/Tethering/tests/integration",
         "//packages/modules/Connectivity/tests/cts/net",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 java_genrule {
@@ -455,12 +499,18 @@
     certificate: "platform",
     manifest: "AndroidManifest_InProcess.xml",
     // InProcessNetworkStack is a replacement for NetworkStack
-    overrides: ["NetworkStack", "NetworkStackNext"],
+    overrides: [
+        "NetworkStack",
+        "NetworkStackNext",
+    ],
     // The InProcessNetworkStack goes together with the PlatformCaptivePortalLogin, which replaces
     // the default CaptivePortalLogin.
     required: [
         "PlatformCaptivePortalLogin",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Pre-merge the AndroidManifest for NetworkStackNext, so that its manifest can be merged on top
@@ -472,7 +522,10 @@
         "ConnectivityNextEnableDefaults",
     ],
     static_libs: ["NetworkStackApiCurrentLib"],
-    manifest: "AndroidManifest.xml"
+    manifest: "AndroidManifest.xml",
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // NetworkStack build targeting the current API release, for testing on in-development SDK
@@ -490,13 +543,19 @@
         "privapp_whitelist_com.android.networkstack",
     ],
     updatable: true,
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Updatable network stack for finalized API
 android_app {
     name: "NetworkStack",
-    defaults: ["NetworkStackAppDefaults", "NetworkStackReleaseApiLevel"],
+    defaults: [
+        "NetworkStackAppDefaults",
+        "NetworkStackReleaseApiLevel",
+    ],
     static_libs: ["NetworkStackApiStableLib"],
     certificate: "networkstack",
     manifest: "AndroidManifest.xml",
@@ -504,13 +563,16 @@
         "privapp_whitelist_com.android.networkstack",
     ],
     updatable: true,
-    lint: { strict_updatability_linting: true },
+    lint: {
+        strict_updatability_linting: true,
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 cc_library_shared {
     name: "libnetworkstackutilsjni",
     srcs: [
-        "jni/network_stack_utils_jni.cpp"
+        "jni/network_stack_utils_jni.cpp",
     ],
     header_libs: [
         "bpf_headers",
@@ -548,8 +610,8 @@
     name: "statslog-networkstack-java-gen-current",
     tools: ["stats-log-api-gen"],
     cmd: "$(location stats-log-api-gen) --java $(out) --module network_stack" +
-         " --javaPackage com.android.networkstack.metrics --javaClass NetworkStackStatsLog" +
-         " --minApiLevel 30",
+        " --javaPackage com.android.networkstack.metrics --javaClass NetworkStackStatsLog" +
+        " --minApiLevel 30",
     out: ["com/android/networkstack/metrics/NetworkStackStatsLog.java"],
 }
 
@@ -557,12 +619,11 @@
     name: "statslog-networkstack-java-gen-stable",
     tools: ["stats-log-api-gen"],
     cmd: "$(location stats-log-api-gen) --java $(out) --module network_stack" +
-         " --javaPackage com.android.networkstack.metrics --javaClass NetworkStackStatsLog" +
-         " --minApiLevel 30 --compileApiLevel 30",
+        " --javaPackage com.android.networkstack.metrics --javaClass NetworkStackStatsLog" +
+        " --minApiLevel 30 --compileApiLevel 30",
     out: ["com/android/networkstack/metrics/NetworkStackStatsLog.java"],
 }
 
-
 version_code_networkstack_next = "300000000"
 version_code_networkstack_test = "999999999"
 
@@ -570,21 +631,27 @@
     name: "NetworkStackTestAndroidManifest",
     srcs: ["AndroidManifest.xml"],
     out: ["TestAndroidManifest.xml"],
-    cmd: "sed -E 's/versionCode=\"[0-9]+\"/versionCode=\""
-        + version_code_networkstack_test
-        + "\"/' $(in) > $(out)",
+    cmd: "sed -E 's/versionCode=\"[0-9]+\"/versionCode=\"" +
+        version_code_networkstack_test +
+        "\"/' $(in) > $(out)",
     visibility: ["//visibility:private"],
 }
 
 android_app {
     name: "TestNetworkStack",
-    defaults: ["NetworkStackAppDefaults", "NetworkStackReleaseApiLevel"],
+    defaults: [
+        "NetworkStackAppDefaults",
+        "NetworkStackReleaseApiLevel",
+    ],
     static_libs: ["NetworkStackApiStableLib"],
     certificate: "networkstack",
     manifest: ":NetworkStackTestAndroidManifest",
     required: [
         "privapp_whitelist_com.android.networkstack",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // When adding or modifying protos, the jarjar rules and possibly proguard rules need
@@ -601,4 +668,7 @@
         "networkstackprotos",
     ],
     defaults: ["NetworkStackReleaseApiLevel"],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
diff --git a/common/networkstackclient/Android.bp b/common/networkstackclient/Android.bp
index 07fd52e..060f0da 100644
--- a/common/networkstackclient/Android.bp
+++ b/common/networkstackclient/Android.bp
@@ -221,6 +221,9 @@
         "com.android.tethering",
         "com.android.wifi",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 java_library {
@@ -265,4 +268,7 @@
         "com.android.tethering",
         "com.android.wifi",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
diff --git a/src/android/net/apf/ApfFilter.java b/src/android/net/apf/ApfFilter.java
index 2ef9c08..ee2990b 100644
--- a/src/android/net/apf/ApfFilter.java
+++ b/src/android/net/apf/ApfFilter.java
@@ -1148,7 +1148,7 @@
                 // Generate code to match the packet bytes.
                 if (section.type == PacketSection.Type.MATCH) {
                     gen.addLoadImmediate(Register.R0, section.start);
-                    gen.addJumpIfBytesNotEqual(Register.R0,
+                    gen.addJumpIfBytesAtR0NotEqual(
                             Arrays.copyOfRange(mPacket.array(), section.start,
                                     section.start + section.length),
                             nextFilterLabel);
@@ -1283,7 +1283,7 @@
             final String nextFilterLabel = "natt_keepalive_filter" + getUniqueNumberLocked();
 
             gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN + IPV4_SRC_ADDR_OFFSET);
-            gen.addJumpIfBytesNotEqual(Register.R0, mSrcDstAddr, nextFilterLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mSrcDstAddr, nextFilterLabel);
 
             // A NAT-T keepalive packet contains 1 byte payload with the value 0xff
             // Check payload length is 1
@@ -1298,11 +1298,11 @@
             // Check that the ports match
             gen.addLoadFromMemory(Register.R0, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
             gen.addAdd(ETH_HEADER_LEN);
-            gen.addJumpIfBytesNotEqual(Register.R0, mPortFingerprint, nextFilterLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mPortFingerprint, nextFilterLabel);
 
             // Payload offset = R0 + UDP header length
             gen.addAdd(UDP_HEADER_LEN);
-            gen.addJumpIfBytesNotEqual(Register.R0, mPayload, nextFilterLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mPayload, nextFilterLabel);
 
             maybeSetupCounter(gen, Counter.DROPPED_IPV4_NATT_KEEPALIVE);
             gen.addJump(mCountAndDropLabel);
@@ -1398,7 +1398,7 @@
             final String nextFilterLabel = "keepalive_ack" + getUniqueNumberLocked();
 
             gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN + IPV4_SRC_ADDR_OFFSET);
-            gen.addJumpIfBytesNotEqual(Register.R0, mSrcDstAddr, nextFilterLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mSrcDstAddr, nextFilterLabel);
 
             // Skip to the next filter if it's not zero-sized :
             // TCP_HEADER_SIZE + IPV4_HEADER_SIZE - ipv4_total_length == 0
@@ -1420,7 +1420,7 @@
             gen.addLoadFromMemory(Register.R1, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
             gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN);
             gen.addAddR1();
-            gen.addJumpIfBytesNotEqual(Register.R0, mPortSeqAckFingerprint, nextFilterLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mPortSeqAckFingerprint, nextFilterLabel);
 
             maybeSetupCounter(gen, Counter.DROPPED_IPV4_KEEPALIVE_ACK);
             gen.addJump(mCountAndDropLabel);
@@ -1522,7 +1522,7 @@
         // Drop if not ARP IPv4.
         gen.addLoadImmediate(Register.R0, ARP_HEADER_OFFSET);
         maybeSetupCounter(gen, Counter.DROPPED_ARP_NON_IPV4);
-        gen.addJumpIfBytesNotEqual(Register.R0, ARP_IPV4_HEADER, mCountAndDropLabel);
+        gen.addJumpIfBytesAtR0NotEqual(ARP_IPV4_HEADER, mCountAndDropLabel);
 
         // Drop if unknown ARP opcode.
         gen.addLoad16(Register.R0, ARP_OPCODE_OFFSET);
@@ -1538,7 +1538,7 @@
         // Pass if non-broadcast reply.
         gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
         maybeSetupCounter(gen, Counter.PASSED_ARP_UNICAST_REPLY);
-        gen.addJumpIfBytesNotEqual(Register.R0, ETHER_BROADCAST, mCountAndPassLabel);
+        gen.addJumpIfBytesAtR0NotEqual(ETHER_BROADCAST, mCountAndPassLabel);
 
         // Either a request, or a broadcast reply.
         gen.defineLabel(checkTargetIPv4);
@@ -1552,7 +1552,7 @@
             // and broadcast replies with a different target IPv4 address.
             gen.addLoadImmediate(Register.R0, ARP_TARGET_IP_ADDRESS_OFFSET);
             maybeSetupCounter(gen, Counter.DROPPED_ARP_OTHER_HOST);
-            gen.addJumpIfBytesNotEqual(Register.R0, mIPv4Address, mCountAndDropLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mIPv4Address, mCountAndDropLabel);
         }
 
         maybeSetupCounter(gen, Counter.PASSED_ARP);
@@ -1600,7 +1600,7 @@
             gen.addLoadImmediate(Register.R0, DHCP_CLIENT_MAC_OFFSET);
             // NOTE: Relies on R1 containing IPv4 header offset.
             gen.addAddR1();
-            gen.addJumpIfBytesNotEqual(Register.R0, mHardwareAddress, skipDhcpv4Filter);
+            gen.addJumpIfBytesAtR0NotEqual(mHardwareAddress, skipDhcpv4Filter);
             maybeSetupCounter(gen, Counter.PASSED_DHCP);
             gen.addJump(mCountAndPassLabel);
 
@@ -1639,7 +1639,7 @@
             // TODO: can we invert this condition to fall through to the common pass case below?
             maybeSetupCounter(gen, Counter.PASSED_IPV4_UNICAST);
             gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
-            gen.addJumpIfBytesNotEqual(Register.R0, ETHER_BROADCAST, mCountAndPassLabel);
+            gen.addJumpIfBytesAtR0NotEqual(ETHER_BROADCAST, mCountAndPassLabel);
             maybeSetupCounter(gen, Counter.DROPPED_IPV4_L2_BROADCAST);
             gen.addJump(mCountAndDropLabel);
         }
@@ -1763,8 +1763,7 @@
         // TODO: Drop only if they don't contain the address of on-link neighbours.
         final byte[] unsolicitedNaDropPrefix = Arrays.copyOf(IPV6_ALL_NODES_ADDRESS, 15);
         gen.addLoadImmediate(Register.R0, IPV6_DEST_ADDR_OFFSET);
-        gen.addJumpIfBytesNotEqual(Register.R0, unsolicitedNaDropPrefix,
-                skipUnsolicitedMulticastNALabel);
+        gen.addJumpIfBytesAtR0NotEqual(unsolicitedNaDropPrefix, skipUnsolicitedMulticastNALabel);
 
         maybeSetupCounter(gen, Counter.DROPPED_IPV6_MULTICAST_NA);
         gen.addJump(mCountAndDropLabel);
@@ -1821,8 +1820,7 @@
 
         // Check it's L2 mDNS multicast address.
         gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
-        gen.addJumpIfBytesNotEqual(Register.R0, ETH_MULTICAST_MDNS_V4_MAC_ADDRESS,
-                skipMdnsv4Filter);
+        gen.addJumpIfBytesAtR0NotEqual(ETH_MULTICAST_MDNS_V4_MAC_ADDRESS, skipMdnsv4Filter);
 
         // Checks it's IPv4.
         gen.addLoad16(Register.R0, ETH_ETHERTYPE_OFFSET);
@@ -1845,8 +1843,7 @@
 
         // Checks it's L2 mDNS multicast address.
         // Relies on R0 containing the ethernet destination mac address offset.
-        gen.addJumpIfBytesNotEqual(Register.R0, ETH_MULTICAST_MDNS_V6_MAC_ADDRESS,
-                skipMdnsFilter);
+        gen.addJumpIfBytesAtR0NotEqual(ETH_MULTICAST_MDNS_V6_MAC_ADDRESS, skipMdnsFilter);
 
         // Checks it's IPv6.
         gen.addLoad16(Register.R0, ETH_ETHERTYPE_OFFSET);
@@ -1877,7 +1874,7 @@
         for (int i = 0; i < mMdnsAllowList.size(); ++i) {
             final String mDnsNextAllowedQnameCheck = "mdns_next_allowed_qname_check" + i;
             final byte[] encodedQname = encodeQname(mMdnsAllowList.get(i));
-            gen.addJumpIfBytesNotEqual(Register.R0, encodedQname, mDnsNextAllowedQnameCheck);
+            gen.addJumpIfBytesAtR0NotEqual(encodedQname, mDnsNextAllowedQnameCheck);
             // QNAME matched
             gen.addJump(mDnsAcceptPacket);
             // QNAME not matched
@@ -2023,7 +2020,7 @@
         // Drop non-IP non-ARP broadcasts, pass the rest
         gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
         maybeSetupCounter(gen, Counter.PASSED_NON_IP_UNICAST);
-        gen.addJumpIfBytesNotEqual(Register.R0, ETHER_BROADCAST, mCountAndPassLabel);
+        gen.addJumpIfBytesAtR0NotEqual(ETHER_BROADCAST, mCountAndPassLabel);
         maybeSetupCounter(gen, Counter.DROPPED_ETH_BROADCAST);
         gen.addJump(mCountAndDropLabel);
 
diff --git a/src/android/net/apf/ApfGenerator.java b/src/android/net/apf/ApfGenerator.java
index 0c4007b..6346a02 100644
--- a/src/android/net/apf/ApfGenerator.java
+++ b/src/android/net/apf/ApfGenerator.java
@@ -16,7 +16,13 @@
 
 package android.net.apf;
 
+import static android.net.apf.ApfGenerator.Register.R0;
+import static android.net.apf.ApfGenerator.Register.R1;
+
+import androidx.annotation.NonNull;
+
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.net.module.util.HexDump;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -26,7 +32,7 @@
  * APF assembler/generator.  A tool for generating an APF program.
  *
  * Call add*() functions to add instructions to the program, then call
- * {@link generate} to get the APF bytecode for the program.
+ * {@link ApfGenerator#generate} to get the APF bytecode for the program.
  *
  * @hide
  */
@@ -63,7 +69,12 @@
         OR(11),    // Or, e.g. "or R0,5"
         SH(12),    // Left shift, e.g, "sh R0, 5" or "sh R0, -5" (shifts right)
         LI(13),    // Load immediate, e.g. "li R0,5" (immediate encoded as signed value)
-        JMP(14),   // Jump, e.g. "jmp label"
+        // Jump, e.g. "jmp label"
+        // In APFv6, we use JMP(R=1) to encode the DATA instruction. DATA is executed as a jump.
+        // It tells how many bytes of the program regions are used to store the data and followed
+        // by the actual data bytes.
+        // "e.g. data 5, abcde"
+        JMP(14),
         JEQ(15),   // Compare equal and branch, e.g. "jeq R0,5,label"
         JNE(16),   // Compare not equal and branch, e.g. "jne R0,5,label"
         JGT(17),   // Compare greater than and branch, e.g. "jgt R0,5,label"
@@ -73,12 +84,17 @@
         EXT(21),   // Followed by immediate indicating ExtendedOpcodes.
         LDDW(22),  // Load 4 bytes from data memory address (register + immediate): "lddw R0, [5]R1"
         STDW(23),  // Store 4 bytes to data memory address (register + immediate): "stdw R0, [5]R1"
-        WRITE(24),  // Write 1, 2 or 4 bytes imm to the output buffer, e.g. "WRITE 5"
-        // Copy the data from input packet or APF data region to output buffer. Register bit is
-        // used to specify the source of data copy: R=0 means copy from packet, R=1 means copy
-        // from APF data region. The source offset is encoded in the first imm and the copy length
-        // is encoded in the second imm. "e.g. MEMCOPY(R=0), 5, 5"
-        MEMCOPY(25);
+        // Write 1, 2 or 4 bytes immediate to the output buffer and auto-increment the pointer to
+        // write. e.g. "write 5"
+        WRITE(24),
+        // Copy bytes from input packet/APF program/data region to output buffer and
+        // auto-increment the output buffer pointer.
+        // Register bit is used to specify the source of data copy.
+        // R=0 means copy from packet.
+        // R=1 means copy from APF program/data region.
+        // The copy length is stored in (u8)imm2.
+        // e.g. "pktcopy 5, 5" "datacopy 5, 5"
+        PKTDATACOPY(25);
 
         final int value;
 
@@ -95,22 +111,50 @@
         NEG(33),  // Negate, e.g. "neg R0"
         SWAP(34), // Swap, e.g. "swap R0,R1"
         MOVE(35),  // Move, e.g. "move R0,R1"
-        ALLOC(36), // Allocate buffer, "e.g. ALLOC R0"
+        // Allocate writable output buffer.
+        // R=0, use register R0 to store the length. R=1, encode the length in the u16 int imm2.
+        // "e.g. allocate R0"
+        // "e.g. allocate 123"
+        ALLOCATE(36),
         //  Transmit and deallocate the buffer (transmission can be delayed until the program
         //  terminates). R=0 means discard the buffer, R=1 means transmit the buffer.
         // "e.g. trans"
         // "e.g. discard"
         TRANSMIT(37),
         DISCARD(37),
-        EWRITE1(38), // Write 1 byte from register to the output buffer, e.g. "EWRITE1 R0"
-        EWRITE2(39), // Write 2 bytes from register to the output buffer, e.g. "EWRITE2 R0"
-        EWRITE4(40), // Write 4 bytes from register to the output buffer, e.g. "EWRITE4 R0"
-        // Copy the data from input packet to output buffer. The source offset is encoded as [Rx
-        // + second imm]. The copy length is encoded in the third imm. "e.g. EPKTCOPY [R0 + 5], 5"
+        // Write 1, 2 or 4 byte value from register to the output buffer and auto-increment the
+        // output buffer pointer.
+        // e.g. "ewrite1 r0"
+        EWRITE1(38),
+        EWRITE2(39),
+        EWRITE4(40),
+        // Copy bytes from input packet/APF program/data region to output buffer and
+        // auto-increment the output buffer pointer.
+        // The copy src offset is stored in R0.
+        // when R=0, the copy length is stored in (u8)imm2.
+        // when R=1, the copy length is stored in R1.
+        // e.g. "pktcopy r0, 5", "pktcopy r0, r1", "datacopy r0, 5", "datacopy r0, r1"
         EPKTCOPY(41),
-        // Copy the data from APF data region to output buffer. The source offset is encoded as [Rx
-        // + second imm]. The copy length is encoded in the third imm. "e.g. EDATACOPY [R0 + 5], 5"
-        EDATACOPY(42);
+        EDATACOPY(42),
+        // Jumps if the UDP payload content (starting at R0) does not contain ont
+        // of the specified QNAME, applying case insensitivity.
+        // R0: Offset to UDP payload content
+        // R=0/1 meanining 'does not match' vs 'matches'
+        // imm1: Opcode
+        // imm2: Label offset
+        // imm3(u8): Question type (PTR/SRV/TXT/A/AAAA)
+        // imm4(bytes): TLV-encoded QNAME list (null-terminated)
+        // e.g.: "jdnsqmatch R0,label,0x0c,\002aa\005local\0\0"
+        JDNSQMATCH(43), // Jumps if the UDP payload content (starting at R0) does not contain one
+        // of the specified NAME in answers/authority/additional records, applying
+        // case insensitivity.
+        // R=0/1 meanining 'does not match' vs 'matches'
+        // R0: Offset to UDP payload content
+        // imm1: Opcode
+        // imm2: Label offset
+        // imm3(bytes): TLV-encoded QNAME list (null-terminated)
+        // e.g.: "jdnsamatch R0,label,0x0c,\002aa\005local\0\0"
+        JDNSAMATCH(44);
 
         final int value;
 
@@ -129,34 +173,136 @@
         }
     }
 
-    private static class Immediate {
-        public final boolean mSigned;
-        public final byte mImmSize;
+    private enum IntImmediateType {
+        INDETERMINATE_SIZE_SIGNED,
+        INDETERMINATE_SIZE_UNSIGNED,
+        SIGNED_8,
+        UNSIGNED_8,
+        SIGNED_BE16,
+        UNSIGNED_BE16,
+        SIGNED_BE32,
+        UNSIGNED_BE32;
+    }
+
+    private static class IntImmediate {
+        public final IntImmediateType mImmediateType;
         public final int mValue;
 
-        Immediate(int value, boolean signed) {
-            this(value, signed, calculateImmSize(value, signed));
+        IntImmediate(int value, IntImmediateType type) {
+            mImmediateType = type;
+            mValue = value;
         }
 
-        Immediate(int value, boolean signed, byte size) {
-            mValue = value;
-            mSigned = signed;
-            mImmSize = size;
+        private int calculateIndeterminateSize() {
+            switch (mImmediateType) {
+                case INDETERMINATE_SIZE_SIGNED:
+                    return calculateImmSize(mValue, true /* signed */);
+                case INDETERMINATE_SIZE_UNSIGNED:
+                    return calculateImmSize(mValue, false /* signed */);
+                default:
+                    // For IMM with determinate size, return 0 to allow Math.max() calculation in
+                    // caller function.
+                    return 0;
+            }
+        }
+
+        private int getEncodingSize(int immFieldSize) {
+            switch (mImmediateType) {
+                case SIGNED_8:
+                case UNSIGNED_8:
+                    return 1;
+                case SIGNED_BE16:
+                case UNSIGNED_BE16:
+                    return 2;
+                case SIGNED_BE32:
+                case UNSIGNED_BE32:
+                    return 4;
+                case INDETERMINATE_SIZE_SIGNED:
+                case INDETERMINATE_SIZE_UNSIGNED: {
+                    int minSizeRequired = calculateIndeterminateSize();
+                    if (minSizeRequired > immFieldSize) {
+                        throw new IllegalStateException(
+                                String.format("immFieldSize: %d is too small to encode value %d",
+                                        immFieldSize, mValue));
+                    }
+                    return immFieldSize;
+                }
+            }
+            throw new IllegalStateException("UnhandledInvalid IntImmediateType: " + mImmediateType);
+        }
+
+        private int writeValue(byte[] bytecode, Integer writingOffset, int immFieldSize) {
+            return Instruction.writeValue(mValue, bytecode, writingOffset,
+                    getEncodingSize(immFieldSize));
+        }
+
+        public static IntImmediate newSigned(int imm) {
+            return new IntImmediate(imm, IntImmediateType.INDETERMINATE_SIZE_SIGNED);
+        }
+
+        public static IntImmediate newUnsigned(long imm) {
+            // upperBound is 2^32 - 1
+            checkRange("Unsigned IMM", imm, 0 /* lowerBound */,
+                    4294967295L /* upperBound */);
+            return new IntImmediate((int) imm, IntImmediateType.INDETERMINATE_SIZE_UNSIGNED);
+        }
+
+        public static IntImmediate newTwosComplementUnsigned(long imm) {
+            checkRange("Unsigned TwosComplement IMM", imm, Integer.MIN_VALUE,
+                    4294967295L /* upperBound */);
+            return new IntImmediate((int) imm, IntImmediateType.INDETERMINATE_SIZE_UNSIGNED);
+        }
+
+        public static IntImmediate newTwosComplementSigned(long imm) {
+            checkRange("Signed TwosComplement IMM", imm, Integer.MIN_VALUE,
+                    4294967295L /* upperBound */);
+            return new IntImmediate((int) imm, IntImmediateType.INDETERMINATE_SIZE_SIGNED);
+        }
+
+        public static IntImmediate newS8(byte imm) {
+            checkRange("S8 IMM", imm, Byte.MIN_VALUE, Byte.MAX_VALUE);
+            return new IntImmediate(imm, IntImmediateType.SIGNED_8);
+        }
+
+        public static IntImmediate newU8(int imm) {
+            checkRange("U8 IMM", imm, 0, 255);
+            return new IntImmediate(imm, IntImmediateType.UNSIGNED_8);
+        }
+
+        public static IntImmediate newS16(short imm) {
+            return new IntImmediate(imm, IntImmediateType.SIGNED_BE16);
+        }
+
+        public static IntImmediate newU16(int imm) {
+            checkRange("U16 IMM", imm, 0, 65535);
+            return new IntImmediate(imm, IntImmediateType.UNSIGNED_BE16);
+        }
+
+        public static IntImmediate newS32(int imm) {
+            return new IntImmediate(imm, IntImmediateType.SIGNED_BE32);
+        }
+
+        public static IntImmediate newU32(long imm) {
+            // upperBound is 2^32 - 1
+            checkRange("U32 IMM", imm, 0 /* lowerBound */,
+                    4294967295L /* upperBound */);
+            return new IntImmediate((int) imm, IntImmediateType.UNSIGNED_BE32);
         }
 
         @Override
         public String toString() {
-            return "Immediate{" + "mSigned=" + mSigned + ", mImmSize=" + mImmSize + ", mValue="
-                    + mValue + '}';
+            return "IntImmediate{" + "mImmediateType=" + mImmediateType + ", mValue=" + mValue
+                    + '}';
         }
     }
 
     private class Instruction {
         private final byte mOpcode;   // A "Opcode" value.
         private final byte mRegister; // A "Register" value.
-        public final List<Immediate> mImms = new ArrayList<>();
+        public final List<IntImmediate> mIntImms = new ArrayList<>();
         // When mOpcode is a jump:
-        private byte mTargetLabelSize;
+        private int mTargetLabelSize;
+        private int mLenFieldOverride = -1;
         private String mTargetLabel;
         // When mOpcode == Opcodes.LABEL:
         private String mLabel;
@@ -169,27 +315,81 @@
             mRegister = (byte) register.value;
         }
 
+        Instruction(ExtendedOpcodes extendedOpcodes, Register register) {
+            this(Opcodes.EXT, register);
+            addUnsigned(extendedOpcodes.value);
+        }
+
+        Instruction(ExtendedOpcodes extendedOpcodes, int slot, Register register)
+                throws IllegalInstructionException {
+            this(Opcodes.EXT, register);
+            if (slot < 0 || slot >= MEMORY_SLOTS) {
+                throw new IllegalInstructionException("illegal memory slot number: " + slot);
+            }
+            addUnsigned(extendedOpcodes.value + slot);
+        }
+
         Instruction(Opcodes opcode) {
-            this(opcode, Register.R0);
+            this(opcode, R0);
         }
 
-        void addUnsignedImm(int imm) {
-            addImm(new Immediate(imm, false));
+        Instruction(ExtendedOpcodes extendedOpcodes) {
+            this(extendedOpcodes, R0);
         }
 
-        void addUnsignedImm(int imm, byte size) {
-            addImm(new Immediate(imm, false, size));
+        Instruction addSigned(int imm) {
+            mIntImms.add(IntImmediate.newSigned(imm));
+            return this;
         }
 
-        void addSignedImm(int imm) {
-            addImm(new Immediate(imm, true));
+        Instruction addUnsigned(int imm) {
+            mIntImms.add(IntImmediate.newUnsigned(imm));
+            return this;
         }
 
-        void addImm(Immediate imm) {
-            mImms.add(imm);
+
+        Instruction addTwosCompSigned(int imm) {
+            mIntImms.add(IntImmediate.newTwosComplementSigned(imm));
+            return this;
         }
 
-        void setLabel(String label) throws IllegalInstructionException {
+
+        Instruction addTwosCompUnsigned(int imm) {
+            mIntImms.add(IntImmediate.newTwosComplementUnsigned(imm));
+            return this;
+        }
+
+        Instruction addS8(byte imm) {
+            mIntImms.add(IntImmediate.newS8(imm));
+            return this;
+        }
+
+        Instruction addU8(int imm) {
+            mIntImms.add(IntImmediate.newU8(imm));
+            return this;
+        }
+
+        Instruction addS16(short imm) {
+            mIntImms.add(IntImmediate.newS16(imm));
+            return this;
+        }
+
+        Instruction addU16(int imm) {
+            mIntImms.add(IntImmediate.newU16(imm));
+            return this;
+        }
+
+        Instruction addS32(int imm) {
+            mIntImms.add(IntImmediate.newS32(imm));
+            return this;
+        }
+
+        Instruction addU32(long imm) {
+            mIntImms.add(IntImmediate.newU32(imm));
+            return this;
+        }
+
+        Instruction setLabel(String label) throws IllegalInstructionException {
             if (mLabels.containsKey(label)) {
                 throw new IllegalInstructionException("duplicate label " + label);
             }
@@ -198,18 +398,23 @@
             }
             mLabel = label;
             mLabels.put(label, this);
+            return this;
         }
 
-        void setTargetLabel(String label) {
+        Instruction setTargetLabel(String label) {
             mTargetLabel = label;
             mTargetLabelSize = 4; // May shrink later on in generate().
+            return this;
         }
 
-        void setBytesImm(byte[] bytes) {
-            if (mOpcode != Opcodes.JNEBS.value) {
-                throw new IllegalStateException("adding compare bytes to non-JNEBS instruction");
-            }
+        Instruction overrideLenField(int size) {
+            mLenFieldOverride = size;
+            return this;
+        }
+
+        Instruction setBytesImm(byte[] bytes) {
             mBytesImm = bytes;
+            return this;
         }
 
         /**
@@ -220,11 +425,12 @@
                 return 0;
             }
             int size = 1;
-            byte maxImmSize = getMaxImmSize();
-            // For the copy opcode, the last imm is the length field is always 1 byte
-            size += mImms.size() * maxImmSize;
+            int indeterminateSize = calculateRequiredIndeterminateSize();
+            for (IntImmediate imm : mIntImms) {
+                size += imm.getEncodingSize(indeterminateSize);
+            }
             if (mTargetLabel != null) {
-                size += maxImmSize;
+                size += indeterminateSize;
             }
             if (mBytesImm != null) {
                 size += mBytesImm.length;
@@ -241,20 +447,34 @@
             if (mTargetLabel == null) {
                 return false;
             }
-            int oldSize = size();
             int oldTargetLabelSize = mTargetLabelSize;
             mTargetLabelSize = calculateImmSize(calculateTargetLabelOffset(), false);
             if (mTargetLabelSize > oldTargetLabelSize) {
                 throw new IllegalStateException("instruction grew");
             }
-            return size() < oldSize;
+            return mTargetLabelSize < oldTargetLabelSize;
         }
 
         /**
          * Assemble value for instruction size field.
          */
-        private byte generateImmSizeField() {
-            byte immSize = getMaxImmSize();
+        private int generateImmSizeField() {
+            // If we already know the size the length field, just use it
+            switch (mLenFieldOverride) {
+                case -1:
+                    break;
+                case 1:
+                    return 1;
+                case 2:
+                    return 2;
+                case 4:
+                    return 3;
+                default:
+                    throw new IllegalStateException(
+                            "mLenFieldOverride has invalid value: " + mLenFieldOverride);
+            }
+            // Otherwise, calculate
+            int immSize = calculateRequiredIndeterminateSize();
             // Encode size field to fit in 2 bits: 0->0, 1->1, 2->2, 3->4.
             return immSize == 4 ? 3 : immSize;
         }
@@ -263,7 +483,7 @@
          * Assemble first byte of generated instruction.
          */
         private byte generateInstructionByte() {
-            byte sizeField = generateImmSizeField();
+            int sizeField = generateImmSizeField();
             return (byte)((mOpcode << 3) | (sizeField << 1) | mRegister);
         }
 
@@ -276,7 +496,7 @@
          * be sign extended and the truncation should simply throw away their signed
          * upper bits.
          */
-        private int writeValue(int value, byte[] bytecode, int writingOffset, byte immSize) {
+        private static int writeValue(int value, byte[] bytecode, int writingOffset, int immSize) {
             for (int i = immSize - 1; i >= 0; i--) {
                 bytecode[writingOffset++] = (byte)((value >> (i * 8)) & 255);
             }
@@ -284,7 +504,7 @@
         }
 
         /**
-         * Generate bytecode for this instruction at offset {@link offset}.
+         * Generate bytecode for this instruction at offset {@link Instruction#offset}.
          */
         void generate(byte[] bytecode) throws IllegalInstructionException {
             if (mOpcode == Opcodes.LABEL.value) {
@@ -292,13 +512,20 @@
             }
             int writingOffset = offset;
             bytecode[writingOffset++] = generateInstructionByte();
-            byte maxImmSize = getMaxImmSize();
+            int indeterminateSize = calculateRequiredIndeterminateSize();
+            int startOffset = 0;
+            if (mOpcode == Opcodes.EXT.value) {
+                // For extend opcode, always write the actual opcode first.
+                writingOffset = mIntImms.get(startOffset++).writeValue(bytecode, writingOffset,
+                        indeterminateSize);
+            }
             if (mTargetLabel != null) {
                 writingOffset = writeValue(calculateTargetLabelOffset(), bytecode, writingOffset,
-                        maxImmSize);
+                        indeterminateSize);
             }
-            for (Immediate imm : mImms) {
-                writingOffset = writeValue(imm.mValue, bytecode, writingOffset, maxImmSize);
+            for (int i = startOffset; i < mIntImms.size(); ++i) {
+                writingOffset = mIntImms.get(i).writeValue(bytecode, writingOffset,
+                        indeterminateSize);
             }
             if (mBytesImm != null) {
                 System.arraycopy(mBytesImm, 0, bytecode, writingOffset, mBytesImm.length);
@@ -311,17 +538,15 @@
         }
 
         /**
-         * Calculate the size of either the immediate fields or the target label field, if either is
-         * present. Most instructions have either immediates or a target label field, but for the
-         * instructions that have both, the size of the target label field must be the same as the
-         * size of the immediate fields, because there is only one length field in the instruction
-         * byte, hence why this function simply takes the maximum of those sizes, so neither is
-         * truncated.
+         * Calculates the maximum indeterminate size of all IMMs in this instruction.
+         * <p>
+         * This method finds the largest size needed to encode any indeterminate-sized IMMs in
+         * the instruction. This size will be stored in the immLen field.
          */
-        private byte getMaxImmSize() {
-            byte maxSize = mTargetLabelSize;
-            for (int i = 0; i < mImms.size(); ++i) {
-                maxSize = (byte) Math.max(maxSize, mImms.get(i).mImmSize);
+        private int calculateRequiredIndeterminateSize() {
+            int maxSize = mTargetLabelSize;
+            for (IntImmediate imm : mIntImms) {
+                maxSize = Math.max(maxSize, imm.calculateIndeterminateSize());
             }
             return maxSize;
         }
@@ -401,6 +626,7 @@
     // This version number syncs up with APF_VERSION in hardware/google/apf/apf_interpreter.h
     public static final int MIN_APF_VERSION = 2;
     public static final int MIN_APF_VERSION_IN_DEV = 5;
+    public static final int APF_VERSION_4 = 4;
 
 
     private final ArrayList<Instruction> mInstructions = new ArrayList<Instruction>();
@@ -434,11 +660,12 @@
         }
     }
 
-    private void addInstruction(Instruction instruction) {
+    private ApfGenerator append(Instruction instruction) {
         if (mGenerated) {
             throw new IllegalStateException("Program already generated");
         }
         mInstructions.add(instruction);
+        return this;
     }
 
     /**
@@ -457,53 +684,38 @@
      * In this case "next_filter" may not have any generated code associated with it.
      */
     public ApfGenerator defineLabel(String name) throws IllegalInstructionException {
-        Instruction instruction = new Instruction(Opcodes.LABEL);
-        instruction.setLabel(name);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(Opcodes.LABEL).setLabel(name));
     }
 
     /**
      * Add an unconditional jump instruction to the end of the program.
      */
     public ApfGenerator addJump(String target) {
-        Instruction instruction = new Instruction(Opcodes.JMP);
-        instruction.setTargetLabel(target);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(Opcodes.JMP).setTargetLabel(target));
     }
 
     /**
      * Add an instruction to the end of the program to load the byte at offset {@code offset}
      * bytes from the beginning of the packet into {@code register}.
      */
-    public ApfGenerator addLoad8(Register register, int offset) {
-        Instruction instruction = new Instruction(Opcodes.LDB, register);
-        instruction.addUnsignedImm(offset);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addLoad8(Register r, int ofs) {
+        return append(new Instruction(Opcodes.LDB, r).addUnsigned(ofs));
     }
 
     /**
      * Add an instruction to the end of the program to load 16-bits at offset {@code offset}
      * bytes from the beginning of the packet into {@code register}.
      */
-    public ApfGenerator addLoad16(Register register, int offset) {
-        Instruction instruction = new Instruction(Opcodes.LDH, register);
-        instruction.addUnsignedImm(offset);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addLoad16(Register r, int ofs) {
+        return append(new Instruction(Opcodes.LDH, r).addUnsigned(ofs));
     }
 
     /**
      * Add an instruction to the end of the program to load 32-bits at offset {@code offset}
      * bytes from the beginning of the packet into {@code register}.
      */
-    public ApfGenerator addLoad32(Register register, int offset) {
-        Instruction instruction = new Instruction(Opcodes.LDW, register);
-        instruction.addUnsignedImm(offset);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addLoad32(Register r, int ofs) {
+        return append(new Instruction(Opcodes.LDW, r).addUnsigned(ofs));
     }
 
     /**
@@ -511,11 +723,8 @@
      * {@code register}. The offset of the loaded byte from the beginning of the packet is
      * the sum of {@code offset} and the value in register R1.
      */
-    public ApfGenerator addLoad8Indexed(Register register, int offset) {
-        Instruction instruction = new Instruction(Opcodes.LDBX, register);
-        instruction.addUnsignedImm(offset);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addLoad8Indexed(Register r, int ofs) {
+        return append(new Instruction(Opcodes.LDBX, r).addUnsigned(ofs));
     }
 
     /**
@@ -523,11 +732,8 @@
      * {@code register}. The offset of the loaded 16-bits from the beginning of the packet is
      * the sum of {@code offset} and the value in register R1.
      */
-    public ApfGenerator addLoad16Indexed(Register register, int offset) {
-        Instruction instruction = new Instruction(Opcodes.LDHX, register);
-        instruction.addUnsignedImm(offset);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addLoad16Indexed(Register r, int ofs) {
+        return append(new Instruction(Opcodes.LDHX, r).addUnsigned(ofs));
     }
 
     /**
@@ -535,109 +741,81 @@
      * {@code register}. The offset of the loaded 32-bits from the beginning of the packet is
      * the sum of {@code offset} and the value in register R1.
      */
-    public ApfGenerator addLoad32Indexed(Register register, int offset) {
-        Instruction instruction = new Instruction(Opcodes.LDWX, register);
-        instruction.addUnsignedImm(offset);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addLoad32Indexed(Register r, int ofs) {
+        return append(new Instruction(Opcodes.LDWX, r).addUnsigned(ofs));
     }
 
     /**
      * Add an instruction to the end of the program to add {@code value} to register R0.
      */
-    public ApfGenerator addAdd(int value) {
-        Instruction instruction = new Instruction(Opcodes.ADD);
-        instruction.addUnsignedImm(value);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addAdd(int val) {
+        return append(new Instruction(Opcodes.ADD).addTwosCompUnsigned(val));
     }
 
     /**
      * Add an instruction to the end of the program to multiply register R0 by {@code value}.
      */
-    public ApfGenerator addMul(int value) {
-        Instruction instruction = new Instruction(Opcodes.MUL);
-        instruction.addUnsignedImm(value);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addMul(int val) {
+        return append(new Instruction(Opcodes.MUL).addUnsigned(val));
     }
 
     /**
      * Add an instruction to the end of the program to divide register R0 by {@code value}.
      */
-    public ApfGenerator addDiv(int value) {
-        Instruction instruction = new Instruction(Opcodes.DIV);
-        instruction.addUnsignedImm(value);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addDiv(int val) {
+        return append(new Instruction(Opcodes.DIV).addUnsigned(val));
     }
 
     /**
      * Add an instruction to the end of the program to logically and register R0 with {@code value}.
      */
-    public ApfGenerator addAnd(int value) {
-        Instruction instruction = new Instruction(Opcodes.AND);
-        instruction.addUnsignedImm(value);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addAnd(int val) {
+        return append(new Instruction(Opcodes.AND).addTwosCompUnsigned(val));
     }
 
     /**
      * Add an instruction to the end of the program to logically or register R0 with {@code value}.
      */
-    public ApfGenerator addOr(int value) {
-        Instruction instruction = new Instruction(Opcodes.OR);
-        instruction.addUnsignedImm(value);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addOr(int val) {
+        return append(new Instruction(Opcodes.OR).addTwosCompUnsigned(val));
     }
 
     /**
      * Add an instruction to the end of the program to shift left register R0 by {@code value} bits.
      */
-    public ApfGenerator addLeftShift(int value) {
-        Instruction instruction = new Instruction(Opcodes.SH);
-        instruction.addSignedImm(value);
-        addInstruction(instruction);
-        return this;
+    // TODO: consider whether should change the argument type to byte
+    public ApfGenerator addLeftShift(int val) {
+        return append(new Instruction(Opcodes.SH).addSigned(val));
     }
 
     /**
      * Add an instruction to the end of the program to shift right register R0 by {@code value}
      * bits.
      */
-    public ApfGenerator addRightShift(int value) {
-        Instruction instruction = new Instruction(Opcodes.SH);
-        instruction.addSignedImm(-value);
-        addInstruction(instruction);
-        return this;
+    // TODO: consider whether should change the argument type to byte
+    public ApfGenerator addRightShift(int val) {
+        return append(new Instruction(Opcodes.SH).addSigned(-val));
     }
 
     /**
      * Add an instruction to the end of the program to add register R1 to register R0.
      */
     public ApfGenerator addAddR1() {
-        Instruction instruction = new Instruction(Opcodes.ADD, Register.R1);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(Opcodes.ADD, R1));
     }
 
     /**
      * Add an instruction to the end of the program to multiply register R0 by register R1.
      */
     public ApfGenerator addMulR1() {
-        Instruction instruction = new Instruction(Opcodes.MUL, Register.R1);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(Opcodes.MUL, R1));
     }
 
     /**
      * Add an instruction to the end of the program to divide register R0 by register R1.
      */
     public ApfGenerator addDivR1() {
-        Instruction instruction = new Instruction(Opcodes.DIV, Register.R1);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(Opcodes.DIV, R1));
     }
 
     /**
@@ -645,9 +823,7 @@
      * and store the result back into register R0.
      */
     public ApfGenerator addAndR1() {
-        Instruction instruction = new Instruction(Opcodes.AND, Register.R1);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(Opcodes.AND, R1));
     }
 
     /**
@@ -655,9 +831,7 @@
      * and store the result back into register R0.
      */
     public ApfGenerator addOrR1() {
-        Instruction instruction = new Instruction(Opcodes.OR, Register.R1);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(Opcodes.OR, R1));
     }
 
     /**
@@ -665,111 +839,77 @@
      * register R1.
      */
     public ApfGenerator addLeftShiftR1() {
-        Instruction instruction = new Instruction(Opcodes.SH, Register.R1);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(Opcodes.SH, R1));
     }
 
     /**
      * Add an instruction to the end of the program to move {@code value} into {@code register}.
      */
     public ApfGenerator addLoadImmediate(Register register, int value) {
-        Instruction instruction = new Instruction(Opcodes.LI, register);
-        instruction.addSignedImm(value);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(Opcodes.LI, register).addSigned(value));
     }
 
     /**
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value equals {@code value}.
      */
-    public ApfGenerator addJumpIfR0Equals(int value, String target) {
-        Instruction instruction = new Instruction(Opcodes.JEQ);
-        instruction.addUnsignedImm(value);
-        instruction.setTargetLabel(target);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addJumpIfR0Equals(int val, String tgt) {
+        return append(new Instruction(Opcodes.JEQ).addTwosCompUnsigned(val).setTargetLabel(tgt));
     }
 
     /**
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value does not equal {@code value}.
      */
-    public ApfGenerator addJumpIfR0NotEquals(int value, String target) {
-        Instruction instruction = new Instruction(Opcodes.JNE);
-        instruction.addUnsignedImm(value);
-        instruction.setTargetLabel(target);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addJumpIfR0NotEquals(int val, String tgt) {
+        return append(new Instruction(Opcodes.JNE).addTwosCompUnsigned(val).setTargetLabel(tgt));
     }
 
     /**
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value is greater than {@code value}.
      */
-    public ApfGenerator addJumpIfR0GreaterThan(int value, String target) {
-        Instruction instruction = new Instruction(Opcodes.JGT);
-        instruction.addUnsignedImm(value);
-        instruction.setTargetLabel(target);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addJumpIfR0GreaterThan(int val, String tgt) {
+        return append(new Instruction(Opcodes.JGT).addUnsigned(val).setTargetLabel(tgt));
     }
 
     /**
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value is less than {@code value}.
      */
-    public ApfGenerator addJumpIfR0LessThan(int value, String target) {
-        Instruction instruction = new Instruction(Opcodes.JLT);
-        instruction.addUnsignedImm(value);
-        instruction.setTargetLabel(target);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addJumpIfR0LessThan(int val, String tgt) {
+        return append(new Instruction(Opcodes.JLT).addUnsigned(val).setTargetLabel(tgt));
     }
 
     /**
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value has any bits set that are also set in {@code value}.
      */
-    public ApfGenerator addJumpIfR0AnyBitsSet(int value, String target) {
-        Instruction instruction = new Instruction(Opcodes.JSET);
-        instruction.addUnsignedImm(value);
-        instruction.setTargetLabel(target);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addJumpIfR0AnyBitsSet(int val, String tgt) {
+        return append(new Instruction(Opcodes.JSET).addTwosCompUnsigned(val).setTargetLabel(tgt));
     }
     /**
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value equals register R1's value.
      */
-    public ApfGenerator addJumpIfR0EqualsR1(String target) {
-        Instruction instruction = new Instruction(Opcodes.JEQ, Register.R1);
-        instruction.setTargetLabel(target);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addJumpIfR0EqualsR1(String tgt) {
+        return append(new Instruction(Opcodes.JEQ, R1).setTargetLabel(tgt));
     }
 
     /**
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value does not equal register R1's value.
      */
-    public ApfGenerator addJumpIfR0NotEqualsR1(String target) {
-        Instruction instruction = new Instruction(Opcodes.JNE, Register.R1);
-        instruction.setTargetLabel(target);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addJumpIfR0NotEqualsR1(String tgt) {
+        return append(new Instruction(Opcodes.JNE, R1).setTargetLabel(tgt));
     }
 
     /**
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value is greater than register R1's value.
      */
-    public ApfGenerator addJumpIfR0GreaterThanR1(String target) {
-        Instruction instruction = new Instruction(Opcodes.JGT, Register.R1);
-        instruction.setTargetLabel(target);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addJumpIfR0GreaterThanR1(String tgt) {
+        return append(new Instruction(Opcodes.JGT, R1).setTargetLabel(tgt));
     }
 
     /**
@@ -777,132 +917,104 @@
      * value is less than register R1's value.
      */
     public ApfGenerator addJumpIfR0LessThanR1(String target) {
-        Instruction instruction = new Instruction(Opcodes.JLT, Register.R1);
-        instruction.setTargetLabel(target);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(Opcodes.JLT, R1).setTargetLabel(target));
     }
 
     /**
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value has any bits set that are also set in R1's value.
      */
-    public ApfGenerator addJumpIfR0AnyBitsSetR1(String target) {
-        Instruction instruction = new Instruction(Opcodes.JSET, Register.R1);
-        instruction.setTargetLabel(target);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addJumpIfR0AnyBitsSetR1(String tgt) {
+        return append(new Instruction(Opcodes.JSET, R1).setTargetLabel(tgt));
     }
 
     /**
-     * Add an instruction to the end of the program to jump to {@code target} if the bytes of the
-     * packet at an offset specified by {@code register} don't match {@code bytes}, {@code register}
-     * must be R0.
+     * Add an instruction to the end of the program to jump to {@code tgt} if the bytes of the
+     * packet at an offset specified by {@code register} don't match {@code bytes}
+     * R=0 means check for not equal
      */
-    public ApfGenerator addJumpIfBytesNotEqual(Register register, byte[] bytes, String target)
+    public ApfGenerator addJumpIfBytesAtR0NotEqual(byte[] bytes, String tgt) {
+        return append(new Instruction(Opcodes.JNEBS).addUnsigned(
+                bytes.length).setTargetLabel(tgt).setBytesImm(bytes));
+    }
+
+    /**
+     * Add an instruction to the end of the program to jump to {@code tgt} if the bytes of the
+     * packet at an offset specified by {@code register} match {@code bytes}
+     * R=1 means check for equal.
+     */
+    public ApfGenerator addJumpIfBytesAtR0Equal(byte[] bytes, String tgt)
             throws IllegalInstructionException {
-        if (register == Register.R1) {
-            throw new IllegalInstructionException("JNEBS fails with R1");
-        }
-        Instruction instruction = new Instruction(Opcodes.JNEBS, register);
-        instruction.addUnsignedImm(bytes.length);
-        instruction.setTargetLabel(target);
-        instruction.setBytesImm(bytes);
-        addInstruction(instruction);
-        return this;
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(Opcodes.JNEBS, R1).addUnsigned(
+                bytes.length).setTargetLabel(tgt).setBytesImm(bytes));
     }
 
     /**
      * Add an instruction to the end of the program to load memory slot {@code slot} into
      * {@code register}.
      */
-    public ApfGenerator addLoadFromMemory(Register register, int slot)
+    public ApfGenerator addLoadFromMemory(Register r, int slot)
             throws IllegalInstructionException {
-        if (slot < 0 || slot > (MEMORY_SLOTS - 1)) {
-            throw new IllegalInstructionException("illegal memory slot number: " + slot);
-        }
-        Instruction instruction = new Instruction(Opcodes.EXT, register);
-        instruction.addUnsignedImm(ExtendedOpcodes.LDM.value + slot);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(ExtendedOpcodes.LDM, slot, r));
     }
 
     /**
      * Add an instruction to the end of the program to store {@code register} into memory slot
      * {@code slot}.
      */
-    public ApfGenerator addStoreToMemory(Register register, int slot)
+    public ApfGenerator addStoreToMemory(Register r, int slot)
             throws IllegalInstructionException {
-        if (slot < 0 || slot > (MEMORY_SLOTS - 1)) {
-            throw new IllegalInstructionException("illegal memory slot number: " + slot);
-        }
-        Instruction instruction = new Instruction(Opcodes.EXT, register);
-        instruction.addUnsignedImm(ExtendedOpcodes.STM.value + slot);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(ExtendedOpcodes.STM, slot, r));
     }
 
     /**
      * Add an instruction to the end of the program to logically not {@code register}.
      */
-    public ApfGenerator addNot(Register register) {
-        Instruction instruction = new Instruction(Opcodes.EXT, register);
-        instruction.addUnsignedImm(ExtendedOpcodes.NOT.value);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addNot(Register r) {
+        return append(new Instruction(ExtendedOpcodes.NOT, r));
     }
 
     /**
      * Add an instruction to the end of the program to negate {@code register}.
      */
-    public ApfGenerator addNeg(Register register) {
-        Instruction instruction = new Instruction(Opcodes.EXT, register);
-        instruction.addUnsignedImm(ExtendedOpcodes.NEG.value);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addNeg(Register r) {
+        return append(new Instruction(ExtendedOpcodes.NEG, r));
     }
 
     /**
      * Add an instruction to swap the values in register R0 and register R1.
      */
     public ApfGenerator addSwap() {
-        Instruction instruction = new Instruction(Opcodes.EXT);
-        instruction.addUnsignedImm(ExtendedOpcodes.SWAP.value);
-        addInstruction(instruction);
-        return this;
+        return append(new Instruction(ExtendedOpcodes.SWAP));
     }
 
     /**
      * Add an instruction to the end of the program to move the value into
      * {@code register} from the other register.
      */
-    public ApfGenerator addMove(Register register) {
-        Instruction instruction = new Instruction(Opcodes.EXT, register);
-        instruction.addUnsignedImm(ExtendedOpcodes.MOVE.value);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addMove(Register r) {
+        return append(new Instruction(ExtendedOpcodes.MOVE, r));
     }
 
     /**
      * Add an instruction to the end of the program to let the program immediately return PASS.
      */
-    public ApfGenerator addPass() throws IllegalInstructionException {
-        Instruction instruction = new Instruction(Opcodes.PASS, Register.R0);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addPass() {
+        // PASS requires using R0 because it shares opcode with DROP
+        return append(new Instruction(Opcodes.PASS));
     }
 
     /**
      * Add an instruction to the end of the program to increment the counter value and
      * immediately return PASS.
      */
-    public ApfGenerator addCountAndPass(int counterNumber) throws IllegalInstructionException {
+    public ApfGenerator addCountAndPass(int cnt) throws IllegalInstructionException {
         requireApfVersion(MIN_APF_VERSION_IN_DEV);
-        checkCounterNumber(counterNumber);
-        Instruction instruction = new Instruction(Opcodes.PASS, Register.R0);
-        instruction.addUnsignedImm(counterNumber);
-        addInstruction(instruction);
-        return this;
+        checkRange("CounterNumber", cnt /* value */, 1 /* lowerBound */,
+                1000 /* upperBound */);
+        // PASS requires using R0 because it shares opcode with DROP
+        return append(new Instruction(Opcodes.PASS).addUnsigned(cnt));
     }
 
     /**
@@ -910,35 +1022,52 @@
      */
     public ApfGenerator addDrop() throws IllegalInstructionException {
         requireApfVersion(MIN_APF_VERSION_IN_DEV);
-        Instruction instruction = new Instruction(Opcodes.DROP, Register.R1);
-        addInstruction(instruction);
-        return this;
+        // DROP requires using R1 because it shares opcode with PASS
+        return append(new Instruction(Opcodes.DROP, R1));
     }
 
     /**
      * Add an instruction to the end of the program to increment the counter value and
      * immediately return DROP.
      */
-    public ApfGenerator addCountAndDrop(int counterNumber) throws IllegalInstructionException {
+    public ApfGenerator addCountAndDrop(int cnt) throws IllegalInstructionException {
         requireApfVersion(MIN_APF_VERSION_IN_DEV);
-        checkCounterNumber(counterNumber);
-        Instruction instruction = new Instruction(Opcodes.DROP, Register.R1);
-        instruction.addUnsignedImm(counterNumber);
-        addInstruction(instruction);
-        return this;
+        checkRange("CounterNumber", cnt /* value */, 1 /* lowerBound */,
+                1000 /* upperBound */);
+        // DROP requires using R1 because it shares opcode with PASS
+        return append(new Instruction(Opcodes.DROP, R1).addUnsigned(cnt));
+    }
+
+    /**
+     * Add an instruction to the end of the program to call the apf_allocate_buffer() function.
+     * Buffer length to be allocated is stored in register 0.
+     */
+    public ApfGenerator addAllocateR0() throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(ExtendedOpcodes.ALLOCATE));
     }
 
     /**
      * Add an instruction to the end of the program to call the apf_allocate_buffer() function.
      *
-     * @param register the register value contains the buffer size.
+     * @param size the buffer length to be allocated.
      */
-    public ApfGenerator addAlloc(Register register) throws IllegalInstructionException {
-        requireApfVersion(5);
-        Instruction instruction = new Instruction(Opcodes.EXT, register);
-        instruction.addUnsignedImm(ExtendedOpcodes.ALLOC.value);
-        addInstruction(instruction);
-        return this;
+    public ApfGenerator addAllocate(int size) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        // R1 means the extra be16 immediate is present
+        return append(new Instruction(ExtendedOpcodes.ALLOCATE, R1).addU16(size));
+    }
+
+    /**
+     * Add an instruction to the beginning of the program to reserve the data region.
+     * @param data the actual data byte
+     */
+    public ApfGenerator addData(byte[] data) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        if (!mInstructions.isEmpty()) {
+            throw new IllegalInstructionException("data instruction has to come first");
+        }
+        return append(new Instruction(Opcodes.JMP, R1).addUnsigned(data.length).setBytesImm(data));
     }
 
     /**
@@ -946,10 +1075,8 @@
      */
     public ApfGenerator addTransmit() throws IllegalInstructionException {
         requireApfVersion(MIN_APF_VERSION_IN_DEV);
-        Instruction instruction = new Instruction(Opcodes.EXT, Register.R0);
-        instruction.addUnsignedImm(ExtendedOpcodes.TRANSMIT.value);
-        addInstruction(instruction);
-        return this;
+        // TRANSMIT requires using R0 because it shares opcode with DISCARD
+        return append(new Instruction(ExtendedOpcodes.TRANSMIT));
     }
 
     /**
@@ -957,213 +1084,270 @@
      */
     public ApfGenerator addDiscard() throws IllegalInstructionException {
         requireApfVersion(MIN_APF_VERSION_IN_DEV);
-        Instruction instruction = new Instruction(Opcodes.EXT, Register.R1);
-        instruction.addUnsignedImm(ExtendedOpcodes.DISCARD.value);
-        addInstruction(instruction);
-        return this;
+        // DISCARD requires using R1 because it shares opcode with TRANSMIT
+        return append(new Instruction(ExtendedOpcodes.DISCARD, R1));
     }
 
-    // TODO: add back when support WRITE opcode
-//    /**
-//     * Add an instruction to the end of the program to write 1, 2 or 4 bytes value to output
-//     buffer.
-//     *
-//     * @param value the value to write
-//     * @param size the size of the value
-//     * @return the ApfGenerator object
-//     * @throws IllegalInstructionException throws when size is not 1, 2 or 4
-//     */
-//    public ApfGenerator addWrite(int value, byte size) throws IllegalInstructionException {
-//        requireApfVersion(5);
-//        if (!(size == 1 || size == 2 || size == 4)) {
-//            throw new IllegalInstructionException("length field must be 1, 2 or 4");
-//        }
-//        if (size < calculateImmSize(value, false)) {
-//            throw new IllegalInstructionException(
-//                    String.format("the value %d is unfit into size: %d", value, size));
-//        }
-//        Instruction instruction = new Instruction(Opcodes.WRITE);
-//        instruction.addUnsignedImm(value, size);
-//        addInstruction(instruction);
-//        return this;
-//    }
+    /**
+     * Add an instruction to the end of the program to write 1 byte value to output buffer.
+     */
+    public ApfGenerator addWriteU8(int val) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(Opcodes.WRITE).overrideLenField(1).addU8(val));
+    }
 
-    // TODO: add back when support EWRITE opcode
-//    /**
-//     * Add an instruction to the end of the program to write 1, 2 or 4 bytes value from register
-//     * to output buffer.
-//     *
-//     * @param register the register contains the value to be written
-//     * @param size the size of the value
-//     * @return the ApfGenerator object
-//     * @throws IllegalInstructionException throws when size is not 1, 2 or 4
-//     */
-//    public ApfGenerator addWrite(Register register, byte size)
-//            throws IllegalInstructionException {
-//        requireApfVersion(5);
-//        if (!(size == 1 || size == 2 || size == 4)) {
-//            throw new IllegalInstructionException(
-//                    "length field must be 1, 2 or 4");
-//        }
-//        Instruction instruction = new Instruction(Opcodes.EXT, register);
-//        if (size == 1) {
-//            instruction.addUnsignedImm(ExtendedOpcodes.EWRITE1.value);
-//        } else if (size == 2) {
-//            instruction.addUnsignedImm(ExtendedOpcodes.EWRITE2.value);
-//        } else {
-//            instruction.addUnsignedImm(ExtendedOpcodes.EWRITE4.value);
-//        }
-//        addInstruction(instruction);
-//        return this;
-//    }
+    /**
+     * Add an instruction to the end of the program to write 2 bytes value to output buffer.
+     */
+    public ApfGenerator addWriteU16(int val) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(Opcodes.WRITE).overrideLenField(2).addU16(val));
+    }
 
-    // TODO: add back when support PKTCOPY/DATACOPY opcode
-//    /**
-//     * Add an instruction to the end of the program to copy data from APF data region to output
-//     * buffer.
-//     *
-//     * @param srcOffset the offset inside the APF data region for where to start copy
-//     * @param length the length of bytes needed to be copied, only <= 255 bytes can be copied at
-//     *               one time.
-//     * @return the ApfGenerator object
-//     * @throws IllegalInstructionException throws when imm size is incorrectly set.
-//     */
-//    public ApfGenerator addDataCopy(int srcOffset, int length)
-//            throws IllegalInstructionException {
-//        return addMemCopy(srcOffset, length, Register.R1);
-//    }
-//
-//    /**
-//     * Add an instruction to the end of the program to copy data from input packet to output
-//     buffer.
-//     *
-//     * @param srcOffset the offset inside the input packet for where to start copy
-//     * @param length the length of bytes needed to be copied, only <= 255 bytes can be copied at
-//     *               one time.
-//     * @return the ApfGenerator object
-//     * @throws IllegalInstructionException throws when imm size is incorrectly set.
-//     */
-//    public ApfGenerator addPacketCopy(int srcOffset, int length)
-//            throws IllegalInstructionException {
-//        return addMemCopy(srcOffset, length, Register.R0);
-//    }
-//
-//    private ApfGenerator addMemCopy(int srcOffset, int length, Register register)
-//            throws IllegalInstructionException {
-//        requireApfVersion(5);
-//        checkCopyLength(length);
-//        checkCopyOffset(srcOffset);
-//        Instruction instruction = new Instruction(Opcodes.MEMCOPY, register);
-//        // if the offset == 0, it should still be encoded with 1 byte size.
-//        if (srcOffset == 0) {
-//            instruction.addUnsignedImm(srcOffset, (byte) 1 /* size */);
-//        } else {
-//            instruction.addUnsignedImm(srcOffset);
-//        }
-//        instruction.addUnsignedImm(length, (byte) 1 /* size */);
-//        addInstruction(instruction);
-//        return this;
-//    }
-//    TODO: add back when support EPKTCOPY/EDATACOPY opcode
-//    /**
-//     * Add an instruction to the end of the program to copy data from APF data region to output
-//     * buffer.
-//     *
-//     * @param register the register that stored the base offset value.
-//     * @param relativeOffset the offset inside the APF data region for where to start copy
-//     * @param length the length of bytes needed to be copied, only <= 255 bytes can be copied at
-//     *               one time.
-//     * @return the ApfGenerator object
-//     * @throws IllegalInstructionException throws when imm size is incorrectly set.
-//     */
-//    public ApfGenerator addDataCopy(Register register, int relativeOffset, int length)
-//            throws IllegalInstructionException {
-//        return addMemcopy(register, relativeOffset, length, ExtendedOpcodes.EDATACOPY.value);
-//    }
-//
-//    /**
-//     * Add an instruction to the end of the program to copy data from input packet to output
-//     buffer.
-//     *
-//     * @param register the register that stored the base offset value.
-//     * @param relativeOffset the offset inside the input packet for where to start copy
-//     * @param length the length of bytes needed to be copied, only <= 255 bytes can be copied at
-//     *               one time.
-//     * @return the ApfGenerator object
-//     * @throws IllegalInstructionException throws when imm size is incorrectly set.
-//     */
-//    public ApfGenerator addPacketCopy(Register register, int relativeOffset, int length)
-//            throws IllegalInstructionException {
-//        return addMemcopy(register, relativeOffset, length, ExtendedOpcodes.EPKTCOPY.value);
-//    }
-//
-//    private ApfGenerator addMemcopy(Register register, int relativeOffset, int length, int opcode)
-//            throws IllegalInstructionException {
-//        requireApfVersion(5);
-//        checkCopyLength(length);
-//        checkCopyOffset(relativeOffset);
-//        Instruction instruction = new Instruction(Opcodes.EXT, register);
-//        instruction.addUnsignedImm(opcode);
-//        // if the offset == 0, it should still be encoded with 1 byte size.
-//        if (relativeOffset == 0) {
-//            instruction.addUnsignedImm(relativeOffset, (byte) 1 /* size */);
-//        } else {
-//            instruction.addUnsignedImm(relativeOffset);
-//        }
-//        instruction.addUnsignedImm(length, (byte) 1 /* size */);
-//        addInstruction(instruction);
-//        return this;
-//    }
+    /**
+     * Add an instruction to the end of the program to write 4 bytes value to output buffer.
+     */
+    public ApfGenerator addWriteU32(long val) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(Opcodes.WRITE).overrideLenField(4).addU32(val));
+    }
 
-    private void checkCopyLength(int length) {
-        if (length < 0 || length > 255) {
-            throw new IllegalArgumentException(
-                    "copy length must between 0 to 255, length: " + length);
+    /**
+     * Add an instruction to the end of the program to write 1 byte value from register to output
+     * buffer.
+     */
+    public ApfGenerator addWriteU8(Register reg) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(ExtendedOpcodes.EWRITE1, reg));
+    }
+
+    /**
+     * Add an instruction to the end of the program to write 2 byte value from register to output
+     * buffer.
+     */
+    public ApfGenerator addWriteU16(Register reg) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(ExtendedOpcodes.EWRITE2, reg));
+    }
+
+    /**
+     * Add an instruction to the end of the program to write 4 byte value from register to output
+     * buffer.
+     */
+    public ApfGenerator addWriteU32(Register reg) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(ExtendedOpcodes.EWRITE4, reg));
+    }
+
+    /**
+     * Add an instruction to the end of the program to copy data from APF program/data region to
+     * output buffer and auto-increment the output buffer pointer.
+     *
+     * @param src the offset inside the APF program/data region for where to start copy.
+     * @param len the length of bytes needed to be copied, only <= 255 bytes can be copied at
+     *               one time.
+     * @return the ApfGenerator object
+     */
+    public ApfGenerator addDataCopy(int src, int len)
+            throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(Opcodes.PKTDATACOPY, R1).addUnsigned(src).addU8(len));
+    }
+
+    /**
+     * Add an instruction to the end of the program to copy data from input packet to output
+     * buffer and auto-increment the output buffer pointer.
+     *
+     * @param src the offset inside the input packet for where to start copy.
+     * @param len the length of bytes needed to be copied, only <= 255 bytes can be copied at
+     *               one time.
+     * @return the ApfGenerator object
+     */
+    public ApfGenerator addPacketCopy(int src, int len)
+            throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(Opcodes.PKTDATACOPY, R0).addUnsigned(src).addU8(len));
+    }
+
+    /**
+     * Add an instruction to the end of the program to copy data from APF program/data region to
+     * output buffer and auto-increment the output buffer pointer.
+     * Source offset is stored in R0.
+     *
+     * @param len the number of bytes to be copied, only <= 255 bytes can be copied at once.
+     * @return the ApfGenerator object
+     */
+    public ApfGenerator addDataCopyFromR0(int len) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(ExtendedOpcodes.EDATACOPY).addU8(len));
+    }
+
+    /**
+     * Add an instruction to the end of the program to copy data from input packet to output
+     * buffer and auto-increment the output buffer pointer.
+     * Source offset is stored in R0.
+     *
+     * @param len the number of bytes to be copied, only <= 255 bytes can be copied at once.
+     * @return the ApfGenerator object
+     */
+    public ApfGenerator addPacketCopyFromR0(int len) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(ExtendedOpcodes.EPKTCOPY).addU8(len));
+    }
+
+    /**
+     * Add an instruction to the end of the program to copy data from APF program/data region to
+     * output buffer and auto-increment the output buffer pointer.
+     * Source offset is stored in R0.
+     * Copy length is stored in R1.
+     *
+     * @return the ApfGenerator object
+     */
+    public ApfGenerator addDataCopyFromR0LenR1() throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(ExtendedOpcodes.EDATACOPY, R1));
+    }
+
+    /**
+     * Add an instruction to the end of the program to copy data from input packet to output
+     * buffer and auto-increment the output buffer pointer.
+     * Source offset is stored in R0.
+     * Copy length is stored in R1.
+     *
+     * @return the ApfGenerator object
+     */
+    public ApfGenerator addPacketCopyFromR0LenR1() throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        return append(new Instruction(ExtendedOpcodes.EPKTCOPY, R1));
+    }
+
+    /**
+     * Check if the byte is valid dns character: A-Z,0-9,-,_
+     */
+    private static boolean isValidDnsCharacter(byte c) {
+        return (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_';
+    }
+
+    private static void validateNames(@NonNull byte[] names) {
+        final int len = names.length;
+        if (len < 4) {
+            throw new IllegalArgumentException("qnames must have at least length 4");
+        }
+        final String errorMessage = "qname: " + HexDump.toHexString(names)
+                + "is not null-terminated list of TLV-encoded names";
+        int i = 0;
+        while (i < len - 1) {
+            int label_len = names[i++];
+            if (label_len < 1 || label_len > 63) {
+                throw new IllegalArgumentException(
+                        "label len: " + label_len + " must be between 1 and 63");
+            }
+            if (i + label_len >= len - 1) {
+                throw new IllegalArgumentException(errorMessage);
+            }
+            while (label_len-- > 0) {
+                if (!isValidDnsCharacter(names[i++])) {
+                    throw new IllegalArgumentException("qname: " + HexDump.toHexString(names)
+                            + " contains invalid character");
+                }
+            }
+            if (names[i] == 0) {
+                i++; // skip null terminator.
+            }
+        }
+        if (names[len - 1] != 0) {
+            throw new IllegalArgumentException(errorMessage);
         }
     }
 
-    private void checkCopyOffset(int offset) {
-        if (offset < 0) {
-            throw new IllegalArgumentException(
-                    "offset must be non less than zero, offset: " + offset);
-        }
+    /**
+     * Appends a conditional jump instruction to the program: Jumps to {@code tgt} if the UDP
+     * payload's DNS questions do NOT contain the QNAME specified in {@code qnames} and qtype
+     * equals {@code qtype}. Examines the payload starting at the offset in R0.
+     * R = 0 means check for "does not contain".
+     */
+    public ApfGenerator addJumpIfPktAtR0DoesNotContainDnsQ(@NonNull byte[] qnames, int qtype,
+            @NonNull String tgt) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        validateNames(qnames);
+        return append(new Instruction(ExtendedOpcodes.JDNSQMATCH).setTargetLabel(tgt).addU8(
+                qtype).setBytesImm(qnames));
     }
 
-    private void checkCounterNumber(int counterNumber) {
-        if (counterNumber < 1 || counterNumber > 1000) {
-            throw new IllegalArgumentException(
-                    "Counter number must be in range (0, 1000], counterNumber: " + counterNumber);
+    /**
+     * Appends a conditional jump instruction to the program: Jumps to {@code tgt} if the UDP
+     * payload's DNS questions contain the QNAME specified in {@code qnames} and qtype
+     * equals {@code qtype}. Examines the payload starting at the offset in R0.
+     * R = 1 means check for "contain".
+     */
+    public ApfGenerator addJumpIfPktAtR0ContainDnsQ(@NonNull byte[] qnames, int qtype,
+            @NonNull String tgt) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        validateNames(qnames);
+        return append(new Instruction(ExtendedOpcodes.JDNSQMATCH, R1).setTargetLabel(tgt).addU8(
+                qtype).setBytesImm(qnames));
+    }
+
+    /**
+     * Appends a conditional jump instruction to the program: Jumps to {@code tgt} if the UDP
+     * payload's DNS answers/authority/additional records do NOT contain the NAME
+     * specified in {@code Names}. Examines the payload starting at the offset in R0.
+     * R = 0 means check for "does not contain".
+     */
+    public ApfGenerator addJumpIfPktAtR0DoesNotContainDnsA(@NonNull byte[] names,
+            @NonNull String tgt) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        validateNames(names);
+        return append(new Instruction(ExtendedOpcodes.JDNSAMATCH).setTargetLabel(tgt).setBytesImm(
+                names));
+    }
+
+    /**
+     * Appends a conditional jump instruction to the program: Jumps to {@code tgt} if the UDP
+     * payload's DNS answers/authority/additional records contain the NAME
+     * specified in {@code Names}. Examines the payload starting at the offset in R0.
+     * R = 1 means check for "contain".
+     */
+    public ApfGenerator addJumpIfPktAtR0ContainDnsA(@NonNull byte[] names,
+            @NonNull String tgt) throws IllegalInstructionException {
+        requireApfVersion(MIN_APF_VERSION_IN_DEV);
+        validateNames(names);
+        return append(new Instruction(ExtendedOpcodes.JDNSAMATCH, R1).setTargetLabel(
+                tgt).setBytesImm(names));
+    }
+
+    private static void checkRange(@NonNull String variableName, long value, long lowerBound,
+            long upperBound) {
+        if (value >= lowerBound && value <= upperBound) {
+            return;
         }
+        throw new IllegalArgumentException(
+                String.format("%s: %d, must be in range [%d, %d]", variableName, value, lowerBound,
+                        upperBound));
     }
 
     /**
      * Add an instruction to the end of the program to load 32 bits from the data memory into
      * {@code register}. The source address is computed by adding the signed immediate
      * @{code offset} to the other register.
-     * Requires APF v3 or greater.
+     * Requires APF v4 or greater.
      */
-    public ApfGenerator addLoadData(Register destinationRegister, int offset)
+    public ApfGenerator addLoadData(Register dst, int ofs)
             throws IllegalInstructionException {
-        requireApfVersion(3);
-        Instruction instruction = new Instruction(Opcodes.LDDW, destinationRegister);
-        instruction.addSignedImm(offset);
-        addInstruction(instruction);
-        return this;
+        requireApfVersion(APF_VERSION_4);
+        return append(new Instruction(Opcodes.LDDW, dst).addSigned(ofs));
     }
 
     /**
      * Add an instruction to the end of the program to store 32 bits from {@code register} into the
      * data memory. The destination address is computed by adding the signed immediate
      * @{code offset} to the other register.
-     * Requires APF v3 or greater.
+     * Requires APF v4 or greater.
      */
-    public ApfGenerator addStoreData(Register sourceRegister, int offset)
+    public ApfGenerator addStoreData(Register src, int ofs)
             throws IllegalInstructionException {
-        requireApfVersion(3);
-        Instruction instruction = new Instruction(Opcodes.STDW, sourceRegister);
-        instruction.addSignedImm(offset);
-        addInstruction(instruction);
-        return this;
+        requireApfVersion(APF_VERSION_4);
+        return append(new Instruction(Opcodes.STDW, src).addSigned(ofs));
     }
 
     /**
@@ -1182,7 +1366,7 @@
     /**
      * Calculate the size of the imm.
      */
-    private static byte calculateImmSize(int imm, boolean signed) {
+    private static int calculateImmSize(int imm, boolean signed) {
         if (imm == 0) {
             return 0;
         }
diff --git a/src/android/net/apf/DnsUtils.java b/src/android/net/apf/DnsUtils.java
index 0300d34..5bd2515 100644
--- a/src/android/net/apf/DnsUtils.java
+++ b/src/android/net/apf/DnsUtils.java
@@ -301,7 +301,7 @@
         gen.addJumpIfR0NotEquals(label.length(), noMatchLabel);
         gen.addLoadFromMemory(R0, SLOT_CURRENT_PARSE_OFFSET);
         gen.addAdd(1);
-        gen.addJumpIfBytesNotEqual(R0, label.getBytes(), noMatchLabel);
+        gen.addJumpIfBytesAtR0NotEqual(label.getBytes(), noMatchLabel);
 
         // Prep offset of next label.
         gen.addAdd(label.length());
diff --git a/src/android/net/apf/LegacyApfFilter.java b/src/android/net/apf/LegacyApfFilter.java
index cc09856..6b93d89 100644
--- a/src/android/net/apf/LegacyApfFilter.java
+++ b/src/android/net/apf/LegacyApfFilter.java
@@ -982,7 +982,7 @@
                 // Generate code to match the packet bytes.
                 if (section.type == PacketSection.Type.MATCH) {
                     gen.addLoadImmediate(Register.R0, section.start);
-                    gen.addJumpIfBytesNotEqual(Register.R0,
+                    gen.addJumpIfBytesAtR0NotEqual(
                             Arrays.copyOfRange(mPacket.array(), section.start,
                                     section.start + section.length),
                             nextFilterLabel);
@@ -1065,7 +1065,7 @@
             final String nextFilterLabel = "natt_keepalive_filter" + getUniqueNumberLocked();
 
             gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN + IPV4_SRC_ADDR_OFFSET);
-            gen.addJumpIfBytesNotEqual(Register.R0, mSrcDstAddr, nextFilterLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mSrcDstAddr, nextFilterLabel);
 
             // A NAT-T keepalive packet contains 1 byte payload with the value 0xff
             // Check payload length is 1
@@ -1080,11 +1080,11 @@
             // Check that the ports match
             gen.addLoadFromMemory(Register.R0, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
             gen.addAdd(ETH_HEADER_LEN);
-            gen.addJumpIfBytesNotEqual(Register.R0, mPortFingerprint, nextFilterLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mPortFingerprint, nextFilterLabel);
 
             // Payload offset = R0 + UDP header length
             gen.addAdd(UDP_HEADER_LEN);
-            gen.addJumpIfBytesNotEqual(Register.R0, mPayload, nextFilterLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mPayload, nextFilterLabel);
 
             maybeSetupCounter(gen, Counter.DROPPED_IPV4_NATT_KEEPALIVE);
             gen.addJump(mCountAndDropLabel);
@@ -1180,7 +1180,7 @@
             final String nextFilterLabel = "keepalive_ack" + getUniqueNumberLocked();
 
             gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN + IPV4_SRC_ADDR_OFFSET);
-            gen.addJumpIfBytesNotEqual(Register.R0, mSrcDstAddr, nextFilterLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mSrcDstAddr, nextFilterLabel);
 
             // Skip to the next filter if it's not zero-sized :
             // TCP_HEADER_SIZE + IPV4_HEADER_SIZE - ipv4_total_length == 0
@@ -1202,7 +1202,7 @@
             gen.addLoadFromMemory(Register.R1, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
             gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN);
             gen.addAddR1();
-            gen.addJumpIfBytesNotEqual(Register.R0, mPortSeqAckFingerprint, nextFilterLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mPortSeqAckFingerprint, nextFilterLabel);
 
             maybeSetupCounter(gen, Counter.DROPPED_IPV4_KEEPALIVE_ACK);
             gen.addJump(mCountAndDropLabel);
@@ -1316,7 +1316,7 @@
         // Pass if not ARP IPv4.
         gen.addLoadImmediate(Register.R0, ARP_HEADER_OFFSET);
         maybeSetupCounter(gen, Counter.PASSED_ARP_NON_IPV4);
-        gen.addJumpIfBytesNotEqual(Register.R0, ARP_IPV4_HEADER, mCountAndPassLabel);
+        gen.addJumpIfBytesAtR0NotEqual(ARP_IPV4_HEADER, mCountAndPassLabel);
 
         // Pass if unknown ARP opcode.
         gen.addLoad16(Register.R0, ARP_OPCODE_OFFSET);
@@ -1332,7 +1332,7 @@
         // Pass if unicast reply.
         gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
         maybeSetupCounter(gen, Counter.PASSED_ARP_UNICAST_REPLY);
-        gen.addJumpIfBytesNotEqual(Register.R0, ETHER_BROADCAST, mCountAndPassLabel);
+        gen.addJumpIfBytesAtR0NotEqual(ETHER_BROADCAST, mCountAndPassLabel);
 
         // Either a unicast request, a unicast reply, or a broadcast reply.
         gen.defineLabel(checkTargetIPv4);
@@ -1346,7 +1346,7 @@
             // and broadcast replies with a different target IPv4 address.
             gen.addLoadImmediate(Register.R0, ARP_TARGET_IP_ADDRESS_OFFSET);
             maybeSetupCounter(gen, Counter.DROPPED_ARP_OTHER_HOST);
-            gen.addJumpIfBytesNotEqual(Register.R0, mIPv4Address, mCountAndDropLabel);
+            gen.addJumpIfBytesAtR0NotEqual(mIPv4Address, mCountAndDropLabel);
         }
 
         maybeSetupCounter(gen, Counter.PASSED_ARP);
@@ -1394,7 +1394,7 @@
             gen.addLoadImmediate(Register.R0, DHCP_CLIENT_MAC_OFFSET);
             // NOTE: Relies on R1 containing IPv4 header offset.
             gen.addAddR1();
-            gen.addJumpIfBytesNotEqual(Register.R0, mHardwareAddress, skipDhcpv4Filter);
+            gen.addJumpIfBytesAtR0NotEqual(mHardwareAddress, skipDhcpv4Filter);
             maybeSetupCounter(gen, Counter.PASSED_DHCP);
             gen.addJump(mCountAndPassLabel);
 
@@ -1428,7 +1428,7 @@
             // TODO: can we invert this condition to fall through to the common pass case below?
             maybeSetupCounter(gen, Counter.PASSED_IPV4_UNICAST);
             gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
-            gen.addJumpIfBytesNotEqual(Register.R0, ETHER_BROADCAST, mCountAndPassLabel);
+            gen.addJumpIfBytesAtR0NotEqual(ETHER_BROADCAST, mCountAndPassLabel);
             maybeSetupCounter(gen, Counter.DROPPED_IPV4_L2_BROADCAST);
             gen.addJump(mCountAndDropLabel);
         } else {
@@ -1555,8 +1555,7 @@
         // TODO: Drop only if they don't contain the address of on-link neighbours.
         final byte[] unsolicitedNaDropPrefix = Arrays.copyOf(IPV6_ALL_NODES_ADDRESS, 15);
         gen.addLoadImmediate(Register.R0, IPV6_DEST_ADDR_OFFSET);
-        gen.addJumpIfBytesNotEqual(Register.R0, unsolicitedNaDropPrefix,
-                skipUnsolicitedMulticastNALabel);
+        gen.addJumpIfBytesAtR0NotEqual(unsolicitedNaDropPrefix, skipUnsolicitedMulticastNALabel);
 
         maybeSetupCounter(gen, Counter.DROPPED_IPV6_MULTICAST_NA);
         gen.addJump(mCountAndDropLabel);
@@ -1613,7 +1612,7 @@
 
         // Check it's L2 mDNS multicast address.
         gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
-        gen.addJumpIfBytesNotEqual(Register.R0, ETH_MULTICAST_MDNS_V4_MAC_ADDRESS,
+        gen.addJumpIfBytesAtR0NotEqual(ETH_MULTICAST_MDNS_V4_MAC_ADDRESS,
                 skipMdnsv4Filter);
 
         // Checks it's IPv4.
@@ -1631,8 +1630,7 @@
 
         // Checks it's L2 mDNS multicast address.
         // Relies on R0 containing the ethernet destination mac address offset.
-        gen.addJumpIfBytesNotEqual(Register.R0, ETH_MULTICAST_MDNS_V6_MAC_ADDRESS,
-                skipMdnsFilter);
+        gen.addJumpIfBytesAtR0NotEqual(ETH_MULTICAST_MDNS_V6_MAC_ADDRESS, skipMdnsFilter);
 
         // Checks it's IPv6.
         gen.addLoad16(Register.R0, ETH_ETHERTYPE_OFFSET);
@@ -1663,7 +1661,7 @@
         for (int i = 0; i < mMdnsAllowList.size(); ++i) {
             final String mDnsNextAllowedQnameCheck = "mdns_next_allowed_qname_check" + i;
             final byte[] encodedQname = encodeQname(mMdnsAllowList.get(i));
-            gen.addJumpIfBytesNotEqual(Register.R0, encodedQname, mDnsNextAllowedQnameCheck);
+            gen.addJumpIfBytesAtR0NotEqual(encodedQname, mDnsNextAllowedQnameCheck);
             // QNAME matched
             gen.addJump(mDnsAcceptPacket);
             // QNAME not matched
@@ -1777,7 +1775,7 @@
         // Drop non-IP non-ARP broadcasts, pass the rest
         gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET);
         maybeSetupCounter(gen, Counter.PASSED_NON_IP_UNICAST);
-        gen.addJumpIfBytesNotEqual(Register.R0, ETHER_BROADCAST, mCountAndPassLabel);
+        gen.addJumpIfBytesAtR0NotEqual(ETHER_BROADCAST, mCountAndPassLabel);
         maybeSetupCounter(gen, Counter.DROPPED_ETH_BROADCAST);
         gen.addJump(mCountAndDropLabel);
 
diff --git a/src/android/net/dhcp6/Dhcp6Client.java b/src/android/net/dhcp6/Dhcp6Client.java
index 291a97a..8d53048 100644
--- a/src/android/net/dhcp6/Dhcp6Client.java
+++ b/src/android/net/dhcp6/Dhcp6Client.java
@@ -20,9 +20,7 @@
 import static android.net.dhcp6.Dhcp6Packet.PrefixDelegation;
 import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
 import static android.system.OsConstants.AF_INET6;
-import static android.system.OsConstants.IFA_F_NODAD;
 import static android.system.OsConstants.IPPROTO_UDP;
-import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
 import static android.system.OsConstants.SOCK_DGRAM;
 import static android.system.OsConstants.SOCK_NONBLOCK;
 
@@ -31,14 +29,8 @@
 import static com.android.net.module.util.NetworkStackConstants.DHCP6_SERVER_PORT;
 import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ANY;
 import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
-import static com.android.networkstack.apishim.ConstantsShim.IFA_F_MANAGETEMPADDR;
-import static com.android.networkstack.apishim.ConstantsShim.IFA_F_NOPREFIXROUTE;
-import static com.android.networkstack.util.NetworkStackUtils.createInet6AddressFromEui64;
-import static com.android.networkstack.util.NetworkStackUtils.macAddressToEui64;
 
 import android.content.Context;
-import android.net.IpPrefix;
-import android.net.LinkAddress;
 import android.net.ip.IpClient;
 import android.net.util.SocketUtils;
 import android.os.Handler;
@@ -58,12 +50,10 @@
 import com.android.net.module.util.DeviceConfigUtils;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.PacketReader;
-import com.android.net.module.util.netlink.NetlinkUtils;
 import com.android.net.module.util.structs.IaPrefixOption;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
-import java.net.Inet6Address;
 import java.net.SocketException;
 import java.nio.ByteBuffer;
 import java.util.Collections;
@@ -95,8 +85,6 @@
     // Message.arg1 arguments to CMD_DHCP6_RESULT notification
     public static final int DHCP6_PD_SUCCESS = 1;
     public static final int DHCP6_PD_PREFIX_EXPIRED = 2;
-    public static final int DHCP6_PD_PREFIX_CHANGED = 3;
-    public static final int DHCP6_PD_PREFIX_MSG_EXCHANGE_TERMINATED = 4;
 
     // Notification from DHCPv6 state machine before quitting
     public static final int CMD_ON_QUIT = PUBLIC_BASE + 4;
@@ -436,8 +424,8 @@
         Log.d(TAG, "Scheduling IA_PD expiry in " + expirationTimeout + "s");
     }
 
-    private void notifyPrefixDelegation(int result, @Nullable final PrefixDelegation pd) {
-        mController.sendMessage(CMD_DHCP6_RESULT, result, 0, pd);
+    private void notifyPrefixDelegation(int result, @Nullable final List<IaPrefixOption> ipos) {
+        mController.sendMessage(CMD_DHCP6_RESULT, result, 0, ipos);
     }
 
     private void clearDhcp6State() {
@@ -557,10 +545,15 @@
             return sendSolicitPacket(transId, elapsedTimeMs, pd.build());
         }
 
-        // TODO: support multiple prefixes.
         @Override
         protected void receivePacket(Dhcp6Packet packet) {
             final PrefixDelegation pd = packet.mPrefixDelegation;
+            // Ignore any Advertise or Reply for Solicit(with Rapid Commit) with NoPrefixAvail
+            // status code, retransmit Solicit to see if any valid response from other Servers.
+            if (pd.statusCode == Dhcp6Packet.STATUS_NO_PREFIX_AVAIL) {
+                Log.w(TAG, "Server responded to Solicit without available prefix, ignoring");
+                return;
+            }
             if (packet instanceof Dhcp6AdvertisePacket) {
                 Log.d(TAG, "Get prefix delegation option from Advertise: " + pd);
                 mAdvertise = pd;
@@ -601,6 +594,11 @@
         protected void receivePacket(Dhcp6Packet packet) {
             if (!(packet instanceof Dhcp6ReplyPacket)) return;
             final PrefixDelegation pd = packet.mPrefixDelegation;
+            if (pd.statusCode == Dhcp6Packet.STATUS_NO_PREFIX_AVAIL) {
+                Log.w(TAG, "Server responded to Request without available prefix, restart Solicit");
+                transitionTo(mSolicitState);
+                return;
+            }
             Log.d(TAG, "Get prefix delegation option from Reply: " + pd);
             mReply = pd;
             mSolMaxRtMs = packet.getSolMaxRtMs().orElse(mSolMaxRtMs);
@@ -621,7 +619,7 @@
         public boolean processMessage(Message message) {
             switch (message.what) {
                 case CMD_DHCP6_PD_EXPIRE:
-                    notifyPrefixDelegation(DHCP6_PD_PREFIX_EXPIRED, null);
+                    notifyPrefixDelegation(DHCP6_PD_PREFIX_EXPIRED, mReply.getValidIaPrefixes());
                     transitionTo(mSolicitState);
                     return HANDLED;
                 default:
@@ -639,33 +637,6 @@
         }
     }
 
-    // Create an IPv6 address from the interface mac address with IFA_F_MANAGETEMPADDR
-    // flag, kernel will create another privacy IPv6 address on behalf of user space.
-    // We don't need to remember IPv6 addresses that need to extend the lifetime every
-    // time it enters BoundState.
-    private boolean addInterfaceAddress(@NonNull final Inet6Address address,
-            @NonNull final IaPrefixOption ipo) {
-        final int flags = IFA_F_NOPREFIXROUTE | IFA_F_MANAGETEMPADDR | IFA_F_NODAD;
-        final long now = SystemClock.elapsedRealtime();
-        final long deprecationTime = now + ipo.preferred;
-        final long expirationTime = now + ipo.valid;
-        final LinkAddress la = new LinkAddress(address, RFC7421_PREFIX_LENGTH, flags,
-                RT_SCOPE_UNIVERSE /* scope */, deprecationTime, expirationTime);
-        if (!la.isGlobalPreferred()) {
-            Log.e(TAG, la + " is not a global preferred IPv6 address");
-            return false;
-        }
-        if (!NetlinkUtils.sendRtmNewAddressRequest(mIface.index, address,
-                (short) RFC7421_PREFIX_LENGTH,
-                flags, (byte) RT_SCOPE_UNIVERSE /* scope */,
-                ipo.preferred, ipo.valid)) {
-            Log.e(TAG, "Failed to set IPv6 address " + address.getHostAddress()
-                    + "%" + mIface.index);
-            return false;
-        }
-        return true;
-    }
-
     /**
      * Client has already obtained the lease(e.g. IA_PD option) from server and stays in Bound
      * state until T1 expires, and then transition to Renew state to extend the lease duration.
@@ -675,26 +646,9 @@
         public void enter() {
             super.enter();
             scheduleLeaseTimers();
-
-            // TODO: roll back to SOLICIT state after a delay if something wrong happens
-            // instead of returning directly.
-            for (IaPrefixOption ipo : mReply.getValidIaPrefixes()) {
-                // TODO: The prefix with preferred/valid lifetime of 0 is valid, but client
-                // should stop using the prefix immediately. Actually kernel doesn't accept
-                // the address with valid lifetime of 0 and returns EINVAL when it sees that.
-                // We should send RTM_DELADDR netlink message to kernel to delete these addresses
-                // from the interface if any.
-                // Configure IPv6 addresses based on the delegated prefix(es) on the interface.
-                // We've checked that delegated prefix is valid upon receiving the response from
-                // DHCPv6 server, and the server may assign a prefix with length less than 64. So
-                // for SLAAC use case we always set the prefix length to 64 even if the delegated
-                // prefix length is less than 64.
-                final IpPrefix prefix = ipo.getIpPrefix();
-                final Inet6Address address = createInet6AddressFromEui64(prefix,
-                        macAddressToEui64(mIface.macAddr));
-                if (!addInterfaceAddress(address, ipo)) continue;
-            }
-            notifyPrefixDelegation(DHCP6_PD_SUCCESS, mReply);
+            // Pass valid delegated prefix(es) to IpClient for IPv6 address configuration and
+            // active prefix(es) maintenance.
+            notifyPrefixDelegation(DHCP6_PD_SUCCESS, mReply.getValidIaPrefixes());
         }
 
         @Override
@@ -723,8 +677,8 @@
      *   That forces previous delegated prefixes to expire in a natural way, and client should
      *   also stop trying to extend the lifetime for them. That being said, the global IPv6 address
      *   lifetime won't be updated in BoundState if corresponding prefix doesn't appear in Reply
-     *   message, resulting in these global IPv6 addresses eventually and IpClient obtains these
-     *   updates via netlink message and remove the delegated prefix(es) from LinkProperties.
+     *   message, resulting in these global IPv6 addresses expire eventually and IpClient obtains
+     *   these updates via netlink message and remove the delegated prefix(es) from LinkProperties.
      * - If some binding IA_PDs were absent in Reply message, client should still stay at RenewState
      *   or RebindState and retransmit Renew/Rebind messages to see if it can get all later. So far
      *   we only support one IA_PD option per interface, if the received Reply message doesn't take
@@ -744,9 +698,15 @@
         @Override
         protected void receivePacket(Dhcp6Packet packet) {
             if (!(packet instanceof Dhcp6ReplyPacket)) return;
+            final PrefixDelegation pd = packet.mPrefixDelegation;
+            // Stay at Renew/Rebind state if the Reply message takes NoPrefixAvail status code,
+            // retransmit Renew/Rebind message to server, to retry obtaining the prefixes.
+            if (pd.statusCode == Dhcp6Packet.STATUS_NO_PREFIX_AVAIL) {
+                Log.w(TAG, "Server responded to Renew/Rebind without available prefix, ignoring");
+                return;
+            }
             // TODO: send a Request message to the server that responded if any of the IA_PDs in
             // Reply message contain NoBinding status code.
-            final PrefixDelegation pd = packet.mPrefixDelegation;
             Log.d(TAG, "Get prefix delegation option from Reply as response to Renew/Rebind " + pd);
             if (pd.ipos.isEmpty()) return;
             mReply = pd;
diff --git a/src/android/net/dhcp6/Dhcp6Packet.java b/src/android/net/dhcp6/Dhcp6Packet.java
index 4ef3195..53dd274 100644
--- a/src/android/net/dhcp6/Dhcp6Packet.java
+++ b/src/android/net/dhcp6/Dhcp6Packet.java
@@ -32,7 +32,6 @@
 import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
-import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -95,16 +94,16 @@
      * DHCPv6 Optional Type: Status Code.
      */
     public static final byte DHCP6_STATUS_CODE = 13;
+    private static final byte MIN_STATUS_CODE_OPT_LEN = 6;
     protected short mStatusCode;
-    protected String mStatusMsg;
 
     public static final short STATUS_SUCCESS           = 0;
     public static final short STATUS_UNSPEC_FAIL       = 1;
-    public static final short STATUS_NO_ADDR_AVAI      = 2;
+    public static final short STATUS_NO_ADDRS_AVAIL    = 2;
     public static final short STATUS_NO_BINDING        = 3;
-    public static final short STATUS_PREFIX_NOT_ONLINK = 4;
+    public static final short STATUS_NOT_ONLINK        = 4;
     public static final short STATUS_USE_MULTICAST     = 5;
-    public static final short STATUS_NO_PREFIX_AVAI    = 6;
+    public static final short STATUS_NO_PREFIX_AVAIL   = 6;
 
     /**
      * DHCPv6 zero-length Optional Type: Rapid Commit. Per RFC4039, both DHCPDISCOVER and DHCPACK
@@ -209,14 +208,22 @@
         public final int t2;
         @NonNull
         public final List<IaPrefixOption> ipos;
+        public final short statusCode;
 
+        @VisibleForTesting
         public PrefixDelegation(int iaid, int t1, int t2,
-                @NonNull final List<IaPrefixOption> ipos) {
+                @NonNull final List<IaPrefixOption> ipos, short statusCode) {
             Objects.requireNonNull(ipos);
             this.iaid = iaid;
             this.t1 = t1;
             this.t2 = t2;
             this.ipos = ipos;
+            this.statusCode = statusCode;
+        }
+
+        public PrefixDelegation(int iaid, int t1, int t2,
+                @NonNull final List<IaPrefixOption> ipos) {
+            this(iaid, t1, t2, ipos, STATUS_SUCCESS /* statusCode */);
         }
 
         /**
@@ -251,6 +258,7 @@
                 final int t1 = buffer.getInt();
                 final int t2 = buffer.getInt();
                 final List<IaPrefixOption> ipos = new ArrayList<IaPrefixOption>();
+                short statusCode = STATUS_SUCCESS;
                 while (buffer.remaining() > 0) {
                     final int original = buffer.position();
                     final short optionType = buffer.getShort();
@@ -262,12 +270,18 @@
                             Log.d(TAG, "IA Prefix Option: " + ipo);
                             ipos.add(ipo);
                             break;
-                        // TODO: support DHCP6_STATUS_CODE option
+                        case DHCP6_STATUS_CODE:
+                            statusCode = buffer.getShort();
+                            // Skip the status message if any.
+                            if (optionLen > 2) {
+                                skipOption(buffer, optionLen - 2);
+                            }
+                            break;
                         default:
                             skipOption(buffer, optionLen);
                     }
                 }
-                return new PrefixDelegation(iaid, t1, t2, ipos);
+                return new PrefixDelegation(iaid, t1, t2, ipos, statusCode);
             } catch (BufferUnderflowException e) {
                 throw new ParseException(e.getMessage());
             }
@@ -282,16 +296,26 @@
 
         /**
          * Build an IA_PD option from given specific parameters, including IA_PREFIX options.
+         *
+         * Per RFC8415 section 21.13 if the Status Code option does not appear in a message in
+         * which the option could appear, the status of the message is assumed to be Success. So
+         * only put the Status Code option in IA_PD when the status code is not Success.
          */
         public ByteBuffer build(@NonNull final List<IaPrefixOption> input) {
             final ByteBuffer iapd = ByteBuffer.allocate(IaPdOption.LENGTH
-                    + Struct.getSize(IaPrefixOption.class) * input.size());
+                    + Struct.getSize(IaPrefixOption.class) * input.size()
+                    + (statusCode != STATUS_SUCCESS ? MIN_STATUS_CODE_OPT_LEN : 0));
             iapd.putInt(iaid);
             iapd.putInt(t1);
             iapd.putInt(t2);
             for (IaPrefixOption ipo : input) {
                 ipo.writeToByteBuffer(iapd);
             }
+            if (statusCode != STATUS_SUCCESS) {
+                iapd.putShort(DHCP6_STATUS_CODE);
+                iapd.putShort((short) 2);
+                iapd.putShort(statusCode);
+            }
             iapd.flip();
             return iapd;
         }
@@ -314,8 +338,8 @@
 
         @Override
         public String toString() {
-            return "Prefix Delegation: iaid " + iaid + ", t1 " + t1 + ", t2 " + t2
-                    + ", IA prefix options: " + ipos;
+            return String.format("Prefix Delegation, iaid: %s, t1: %s, t2: %s, status code: %s,"
+                    + " IA prefix options: %s", iaid, t1, t2, statusCodeToString(statusCode), ipos);
         }
 
         /**
@@ -366,6 +390,27 @@
         }
     }
 
+    private static String statusCodeToString(short statusCode) {
+        switch (statusCode) {
+            case STATUS_SUCCESS:
+                return "Success";
+            case STATUS_UNSPEC_FAIL:
+                return "UnspecFail";
+            case STATUS_NO_ADDRS_AVAIL:
+                return "NoAddrsAvail";
+            case STATUS_NO_BINDING:
+                return "NoBinding";
+            case STATUS_NOT_ONLINK:
+                return "NotOnLink";
+            case STATUS_USE_MULTICAST:
+                return "UseMulticast";
+            case STATUS_NO_PREFIX_AVAIL:
+                return "NoPrefixAvail";
+            default:
+                return "Unknown";
+        }
+    }
+
     private static void skipOption(@NonNull final ByteBuffer packet, int optionLen)
             throws BufferUnderflowException {
         for (int i = 0; i < optionLen; i++) {
@@ -374,35 +419,6 @@
     }
 
     /**
-     * Reads a string of specified length from the buffer.
-     *
-     * TODO: move to a common place which can be shared with DhcpClient.
-     */
-    private static String readAsciiString(@NonNull final ByteBuffer buf, int byteCount,
-            boolean isNullOk) {
-        final byte[] bytes = new byte[byteCount];
-        buf.get(bytes);
-        return readAsciiString(bytes, isNullOk);
-    }
-
-    private static String readAsciiString(@NonNull final byte[] payload, boolean isNullOk) {
-        final byte[] bytes = payload;
-        int length = bytes.length;
-        if (!isNullOk) {
-            // Stop at the first null byte. This is because some DHCP options (e.g., the domain
-            // name) are passed to netd via FrameworkListener, which refuses arguments containing
-            // null bytes. We don't do this by default because vendorInfo is an opaque string which
-            // could in theory contain null bytes.
-            for (length = 0; length < bytes.length; length++) {
-                if (bytes[length] == 0) {
-                    break;
-                }
-            }
-        }
-        return new String(bytes, 0, length, StandardCharsets.US_ASCII);
-    }
-
-    /**
      * Creates a concrete Dhcp6Packet from the supplied ByteBuffer.
      *
      * The buffer only starts with a UDP encapsulation (i.e. DHCPv6 message). A subset of the
@@ -426,7 +442,6 @@
         byte[] serverDuid = null;
         byte[] clientDuid = null;
         short statusCode = STATUS_SUCCESS;
-        String statusMsg = null;
         boolean rapidCommit = false;
         int solMaxRt = 0;
         PrefixDelegation pd = null;
@@ -487,7 +502,12 @@
                     case DHCP6_STATUS_CODE:
                         expectedLen = optionLen;
                         statusCode = packet.getShort();
-                        statusMsg = readAsciiString(packet, expectedLen - 2, false /* isNullOk */);
+                        // Skip the status message (if any), which is a UTF-8 encoded text string
+                        // suitable for display to the end user, but is not useful for Dhcp6Client
+                        // to decide how to properly handle the status code.
+                        if (optionLen - 2 > 0) {
+                            skipOption(packet, optionLen - 2);
+                        }
                         break;
                     case DHCP6_SOL_MAX_RT:
                         expectedLen = 4;
@@ -545,7 +565,6 @@
             throw new ParseException("Missing IA_PD option");
         }
         newPacket.mStatusCode = statusCode;
-        newPacket.mStatusMsg = statusMsg;
         newPacket.mRapidCommit = rapidCommit;
         newPacket.mSolMaxRt =
                 (solMaxRt >= 60 && solMaxRt <= 86400)
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java
index 4f8eead..d7ef4df 100644
--- a/src/android/net/ip/IpClient.java
+++ b/src/android/net/ip/IpClient.java
@@ -19,7 +19,6 @@
 import static android.net.RouteInfo.RTN_UNICAST;
 import static android.net.RouteInfo.RTN_UNREACHABLE;
 import static android.net.dhcp.DhcpResultsParcelableUtil.toStableParcelable;
-import static android.net.dhcp6.Dhcp6Packet.PrefixDelegation;
 import static android.net.ip.IIpClient.PROV_IPV4_DISABLED;
 import static android.net.ip.IIpClient.PROV_IPV6_DISABLED;
 import static android.net.ip.IIpClient.PROV_IPV6_LINKLOCAL;
@@ -32,22 +31,28 @@
 import static android.system.OsConstants.AF_PACKET;
 import static android.system.OsConstants.ETH_P_ARP;
 import static android.system.OsConstants.ETH_P_IPV6;
+import static android.system.OsConstants.IFA_F_NODAD;
+import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
 import static android.system.OsConstants.SOCK_NONBLOCK;
 import static android.system.OsConstants.SOCK_RAW;
 
+import static com.android.net.module.util.LinkPropertiesUtils.CompareResult;
 import static com.android.net.module.util.NetworkStackConstants.ARP_REPLY;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST;
 import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
 import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
 import static com.android.net.module.util.NetworkStackConstants.VENDOR_SPECIFIC_IE_ID;
+import static com.android.networkstack.apishim.ConstantsShim.IFA_F_MANAGETEMPADDR;
+import static com.android.networkstack.apishim.ConstantsShim.IFA_F_NOPREFIXROUTE;
 import static com.android.networkstack.util.NetworkStackUtils.APF_HANDLE_LIGHT_DOZE_FORCE_DISABLE;
 import static com.android.networkstack.util.NetworkStackUtils.APF_NEW_RA_FILTER_VERSION;
-import static com.android.networkstack.util.NetworkStackUtils.APF_POLLING_COUNTERS_FORCE_DISABLE;
+import static com.android.networkstack.util.NetworkStackUtils.APF_POLLING_COUNTERS_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_IGNORE_LOW_RA_LIFETIME_VERSION;
-import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_MULTICAST_NS_VERSION;
+import static com.android.networkstack.util.NetworkStackUtils.createInet6AddressFromEui64;
+import static com.android.networkstack.util.NetworkStackUtils.macAddressToEui64;
 import static com.android.server.util.PermissionUtil.enforceNetworkStackCallingPermission;
 
 import android.annotation.SuppressLint;
@@ -123,8 +128,10 @@
 import com.android.internal.util.WakeupMessage;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.ConnectivityUtils;
 import com.android.net.module.util.DeviceConfigUtils;
 import com.android.net.module.util.InterfaceParams;
+import com.android.net.module.util.LinkPropertiesUtils;
 import com.android.net.module.util.SharedLog;
 import com.android.net.module.util.SocketUtils;
 import com.android.net.module.util.arp.ArpPacket;
@@ -154,7 +161,6 @@
 import java.net.SocketAddress;
 import java.net.SocketException;
 import java.net.URL;
-import java.net.UnknownHostException;
 import java.nio.BufferUnderflowException;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
@@ -541,6 +547,8 @@
 
     // IpClient shares a handler with Dhcp6Client: commands must not overlap
     public static final int DHCP6CLIENT_CMD_BASE = 2000;
+    private static final int DHCPV6_PREFIX_DELEGATION_ADDRESS_FLAGS =
+            IFA_F_MANAGETEMPADDR | IFA_F_NOPREFIXROUTE | IFA_F_NODAD;
 
     // Settings and default values.
     private static final int MAX_LOG_RECORDS = 500;
@@ -677,6 +685,8 @@
     private final Set<Inet6Address> mGratuitousNaTargetAddresses = new HashSet<>();
     // Set of IPv6 addresses from which multicast NS packets have been sent.
     private final Set<Inet6Address> mMulticastNsSourceAddresses = new HashSet<>();
+    // Set of delegated prefixes.
+    private final Set<IpPrefix> mDelegatedPrefixes = new HashSet<>();
     @Nullable
     private final DevicePolicyManager mDevicePolicyManager;
 
@@ -694,7 +704,7 @@
     private final boolean mUseNewApfFilter;
     private final boolean mEnableIpClientIgnoreLowRaLifetime;
     private final boolean mApfShouldHandleLightDoze;
-    private final boolean mApfShouldPollingCounters;
+    private final boolean mEnableApfPollingCounters;
 
     private InterfaceParams mInterfaceParams;
 
@@ -721,7 +731,6 @@
     private Integer mDadTransmits = null;
     private int mMaxDtimMultiplier = DTIM_MULTIPLIER_RESET;
     private ApfCapabilities mCurrentApfCapabilities;
-    private PrefixDelegation mPrefixDelegation;
     private WakeupMessage mIpv6AutoconfTimeoutAlarm = null;
 
     /**
@@ -928,13 +937,13 @@
                 CONFIG_APF_COUNTER_POLLING_INTERVAL_SECS,
                 DEFAULT_APF_COUNTER_POLLING_INTERVAL_SECS) * DateUtils.SECOND_IN_MILLIS;
         mUseNewApfFilter = mDependencies.isFeatureEnabled(context, APF_NEW_RA_FILTER_VERSION);
+        mEnableApfPollingCounters = mDependencies.isFeatureEnabled(context,
+                APF_POLLING_COUNTERS_VERSION);
         mEnableIpClientIgnoreLowRaLifetime = mDependencies.isFeatureEnabled(context,
                 IPCLIENT_IGNORE_LOW_RA_LIFETIME_VERSION);
         // Light doze mode status checking API is only available at T or later releases.
         mApfShouldHandleLightDoze = SdkLevel.isAtLeastT() && mDependencies.isFeatureNotChickenedOut(
                 mContext, APF_HANDLE_LIGHT_DOZE_FORCE_DISABLE);
-        mApfShouldPollingCounters = mDependencies.isFeatureNotChickenedOut(
-                mContext, APF_POLLING_COUNTERS_FORCE_DISABLE);
 
         IpClientLinkObserver.Configuration config = new IpClientLinkObserver.Configuration(
                 mMinRdnssLifetimeSec);
@@ -1135,10 +1144,6 @@
         return mDependencies.isFeatureEnabled(mContext, IPCLIENT_GARP_NA_ROAMING_VERSION);
     }
 
-    private boolean isMulticastNsEnabled() {
-        return mDependencies.isFeatureNotChickenedOut(mContext, IPCLIENT_MULTICAST_NS_VERSION);
-    }
-
     @VisibleForTesting
     static MacAddress getInitialBssid(final Layer2Information layer2Info,
             final ScanResultInfo scanResultInfo, boolean isAtLeastS) {
@@ -1460,7 +1465,6 @@
         mDhcpResults = null;
         mTcpBufferSizes = "";
         mHttpProxy = null;
-        mPrefixDelegation = null;
 
         mLinkProperties = new LinkProperties();
         mLinkProperties.setInterfaceName(mInterfaceName);
@@ -1741,6 +1745,31 @@
         addAllReachableDnsServers(newLp, netlinkLinkProperties.getDnsServers());
         mShim.setNat64Prefix(newLp, mShim.getNat64Prefix(netlinkLinkProperties));
 
+        // Check if any link address update from netlink.
+        final CompareResult<LinkAddress> results =
+                LinkPropertiesUtils.compareAddresses(mLinkProperties, newLp);
+        // In the case that there are multiple netlink update events about a global IPv6 address
+        // derived from the delegated prefix, a flag-only change event(e.g. due to the duplicate
+        // address detection) will cause an identical IP address to be put into both Added and
+        // Removed list based on the CompareResult implementation. To prevent a prefix from being
+        // mistakenly removed from the delegate prefix list, it is better to always check the
+        // removed list before checking the added list(e.g. anyway we can add the removed prefix
+        // back again).
+        for (LinkAddress la : results.removed) {
+            if (mDhcp6PrefixDelegationEnabled && isIpv6StableDelegatedAddress(la)) {
+                final IpPrefix prefix = new IpPrefix(la.getAddress(), RFC7421_PREFIX_LENGTH);
+                mDelegatedPrefixes.remove(prefix);
+            }
+            // TODO: remove onIpv6AddressRemoved callback.
+        }
+
+        for (LinkAddress la : results.added) {
+            if (mDhcp6PrefixDelegationEnabled && isIpv6StableDelegatedAddress(la)) {
+                final IpPrefix prefix = new IpPrefix(la.getAddress(), RFC7421_PREFIX_LENGTH);
+                mDelegatedPrefixes.add(prefix);
+            }
+        }
+
         // [3] Add in data from DHCPv4, if available.
         //
         // mDhcpResults is never shared with any other owner so we don't have
@@ -1777,26 +1806,22 @@
             // TODO: also look at the IPv6 RA (netlink) for captive portal URL
         }
 
-        // [4] Add in data from DHCPv6 Prefix Delegation, if available.
-        if (mPrefixDelegation != null) {
-            for (IaPrefixOption ipo : mPrefixDelegation.ipos) {
-                try {
-                    final IpPrefix destination =
-                            new IpPrefix(Inet6Address.getByAddress(ipo.prefix), ipo.prefixLen);
-                    // Direct-connected route to delegated prefix. Add RTN_UNREACHABLE to this route
-                    // based on the delegated prefix. To prevent the traffic loop between host and
-                    // upstream delegated router. Because we specify the IFA_F_NOPREFIXROUTE when
-                    // adding the IPv6 address, the kernel does not create a delegated prefix route,
-                    // as a result, the user space won't receive any RTM_NEWROUTE message about the
-                    // delegated prefix, we still need to install an unreachable route for the
-                    // delegated prefix manually in LinkProperties to notify the caller this update.
-                    // TODO: support RTN_BLACKHOLE in netd and use that on newer Android versions.
-                    final RouteInfo route = new RouteInfo(destination, null /* gateway */,
-                            mInterfaceName, RTN_UNREACHABLE);
-                    newLp.addRoute(route);
-                } catch (UnknownHostException e) {
-                    Log.wtf(mTag, "Invalid delegated prefix " + HexDump.toHexString(ipo.prefix));
-                }
+        // [4] Add route with delegated prefix according to the global address update.
+        if (mDhcp6PrefixDelegationEnabled) {
+            for (IpPrefix destination : mDelegatedPrefixes) {
+                // Direct-connected route to delegated prefix. Add RTN_UNREACHABLE to
+                // this route based on the delegated prefix. To prevent the traffic loop
+                // between host and upstream delegated router. Because we specify the
+                // IFA_F_NOPREFIXROUTE when adding the IPv6 address, the kernel does not
+                // create a delegated prefix route, as a result, the user space won't
+                // receive any RTM_NEWROUTE message about the delegated prefix, we still
+                // need to install an unreachable route for the delegated prefix manually
+                // in LinkProperties to notify the caller this update.
+                // TODO: support RTN_BLACKHOLE in netd and use that on newer Android
+                // versions.
+                final RouteInfo route = new RouteInfo(destination,
+                        null /* gateway */, mInterfaceName, RTN_UNREACHABLE);
+                newLp.addRoute(route);
             }
         }
 
@@ -1994,6 +2019,24 @@
         }
     }
 
+    private static boolean hasFlag(@NonNull final LinkAddress la, final int flags) {
+        return (la.getFlags() & flags) == flags;
+
+    }
+
+    // Check whether a global IPv6 stable address is derived from DHCPv6 prefix delegation.
+    // Address derived from delegated prefix should be:
+    // - unicast global routable address
+    // - with prefix length of 64
+    // - has IFA_F_MANAGETEMPADDR, IFA_F_NOPREFIXROUTE and IFA_F_NODAD flags
+    private static boolean isIpv6StableDelegatedAddress(@NonNull final LinkAddress la) {
+        return la.isIpv6()
+                && !ConnectivityUtils.isIPv6ULA(la.getAddress())
+                && (la.getPrefixLength() == RFC7421_PREFIX_LENGTH)
+                && (la.getScope() == (byte) RT_SCOPE_UNIVERSE)
+                && hasFlag(la, DHCPV6_PREFIX_DELEGATION_ADDRESS_FLAGS);
+    }
+
     // Returns false if we have lost provisioning, true otherwise.
     private boolean handleLinkPropertiesUpdate(boolean sendCallbacks) {
         final LinkProperties newLp = assembleLinkProperties();
@@ -2035,9 +2078,7 @@
         //
         // TODO: stop sending this multicast NS after deployment of RFC9131 in the field, leverage
         // the gratuitous NA to update the first-hop router's neighbor cache entry.
-        if (isMulticastNsEnabled()) {
-            maybeSendMulticastNSes(newLp);
-        }
+        maybeSendMulticastNSes(newLp);
 
         // Either success IPv4 or IPv6 provisioning triggers new LinkProperties update,
         // wait for the provisioning completion and record the latency.
@@ -2441,6 +2482,7 @@
             mHasDisabledAcceptRaDefrtrOnProvLoss = false;
             mGratuitousNaTargetAddresses.clear();
             mMulticastNsSourceAddresses.clear();
+            mDelegatedPrefixes.clear();
 
             resetLinkProperties();
             if (mStartTimeMillis > 0) {
@@ -2903,7 +2945,7 @@
             if (mApfFilter == null) {
                 mCallback.setFallbackMulticastFilter(mMulticastFiltering);
             }
-            if (mApfShouldPollingCounters) {
+            if (mEnableApfPollingCounters) {
                 sendMessageDelayed(CMD_UPDATE_APF_DATA_SNAPSHOT, mApfCounterPollingIntervalMs);
             }
 
@@ -2998,30 +3040,71 @@
             }
         }
 
-        private void clearIpv6PrefixDelegationAddresses() {
-            if (mPrefixDelegation == null) {
-                Log.wtf(mTag, "PrefixDelegation shouldn't be null when DHCPv6 PD fails.");
-                return;
-            }
-            final IaPrefixOption ipo = mPrefixDelegation.ipos.get(0);
-            final IpPrefix prefix;
-            try {
-                prefix = new IpPrefix(Inet6Address.getByAddress(ipo.prefix), RFC7421_PREFIX_LENGTH);
-            } catch (UnknownHostException e) {
-                Log.wtf(TAG, "Invalid delegated prefix " + HexDump.toHexString(ipo.prefix));
-                return;
-            }
-
-            // Delete the global IPv6 address based on delegated prefix from interface.
+        private void deleteIpv6PrefixDelegationAddresses(final IpPrefix prefix) {
             for (LinkAddress la : mLinkProperties.getLinkAddresses()) {
                 final InetAddress address = la.getAddress();
                 if (prefix.contains(address)) {
-                    NetlinkUtils.sendRtmDelAddressRequest(mInterfaceParams.index,
-                            (Inet6Address) address, (short) la.getPrefixLength());
+                    if (!NetlinkUtils.sendRtmDelAddressRequest(mInterfaceParams.index,
+                            (Inet6Address) address, (short) la.getPrefixLength())) {
+                        Log.e(TAG, "Failed to delete IPv6 address " + address.getHostAddress());
+                    }
                 }
             }
         }
 
+        private void addInterfaceAddress(@NonNull final Inet6Address address,
+                @NonNull final IaPrefixOption ipo) {
+            final int flags = IFA_F_NOPREFIXROUTE | IFA_F_MANAGETEMPADDR | IFA_F_NODAD;
+            final long now = SystemClock.elapsedRealtime();
+            final long deprecationTime = now + ipo.preferred;
+            final long expirationTime = now + ipo.valid;
+            final LinkAddress la = new LinkAddress(address, RFC7421_PREFIX_LENGTH, flags,
+                    RT_SCOPE_UNIVERSE /* scope */, deprecationTime, expirationTime);
+            if (!la.isGlobalPreferred()) {
+                Log.w(TAG, la + " is not a global IPv6 address");
+                return;
+            }
+            if (!NetlinkUtils.sendRtmNewAddressRequest(mInterfaceParams.index, address,
+                    (short) RFC7421_PREFIX_LENGTH,
+                    flags, (byte) RT_SCOPE_UNIVERSE /* scope */,
+                    ipo.preferred, ipo.valid)) {
+                Log.e(TAG, "Failed to set IPv6 address on " + address.getHostAddress()
+                        + "%" + mInterfaceParams.index);
+            }
+        }
+
+        private void updateDelegatedAddresses(@NonNull final List<IaPrefixOption> valid) {
+            if (valid.isEmpty()) return;
+            for (IaPrefixOption ipo : valid) {
+                final IpPrefix prefix = ipo.getIpPrefix();
+                // The prefix with preferred/valid lifetime of 0 is considered as a valid prefix,
+                // it can be passed to IpClient from Dhcp6Client, however, client should stop using
+                // the global addresses derived from this prefix immediately.
+                if (ipo.withZeroLifetimes()) {
+                    Log.d(TAG, "Delete IPv6 address derived from prefix " + prefix
+                            + " with 0 preferred/valid lifetime");
+                    deleteIpv6PrefixDelegationAddresses(prefix);
+                }
+                // Otherwise, configure IPv6 addresses derived from the delegated prefix(es) on
+                // the interface. We've checked that delegated prefix is valid upon receiving the
+                // response from DHCPv6 server, and the server may assign a prefix with length less
+                // than 64. So for SLAAC use case we always set the prefix length to 64 even if the
+                // delegated prefix length is less than 64.
+                final Inet6Address address = createInet6AddressFromEui64(prefix,
+                        macAddressToEui64(mInterfaceParams.macAddr));
+                addInterfaceAddress(address, ipo);
+            }
+        }
+
+        private void removeExpiredDelegatedAddresses(@NonNull final List<IaPrefixOption> expired) {
+            if (expired.isEmpty()) return;
+            for (IaPrefixOption ipo : expired) {
+                final IpPrefix prefix = ipo.getIpPrefix();
+                Log.d(TAG, "Delete IPv6 address derived from expired prefix " + prefix);
+                deleteIpv6PrefixDelegationAddresses(prefix);
+            }
+        }
+
         @Override
         public boolean processMessage(Message msg) {
             switch (msg.what) {
@@ -3216,15 +3299,14 @@
                 case Dhcp6Client.CMD_DHCP6_RESULT:
                     switch(msg.arg1) {
                         case Dhcp6Client.DHCP6_PD_SUCCESS:
-                            mPrefixDelegation = (PrefixDelegation) msg.obj;
+                            final List<IaPrefixOption> toBeUpdated = (List<IaPrefixOption>) msg.obj;
+                            updateDelegatedAddresses(toBeUpdated);
                             handleLinkPropertiesUpdate(SEND_CALLBACKS);
                             break;
 
                         case Dhcp6Client.DHCP6_PD_PREFIX_EXPIRED:
-                        case Dhcp6Client.DHCP6_PD_PREFIX_CHANGED:
-                        case Dhcp6Client.DHCP6_PD_PREFIX_MSG_EXCHANGE_TERMINATED:
-                            clearIpv6PrefixDelegationAddresses();
-                            mPrefixDelegation = null;
+                            final List<IaPrefixOption> toBeRemoved = (List<IaPrefixOption>) msg.obj;
+                            removeExpiredDelegatedAddresses(toBeRemoved);
                             handleLinkPropertiesUpdate(SEND_CALLBACKS);
                             break;
 
diff --git a/src/android/net/ip/IpReachabilityMonitor.java b/src/android/net/ip/IpReachabilityMonitor.java
index 40a4bb6..e252a68 100644
--- a/src/android/net/ip/IpReachabilityMonitor.java
+++ b/src/android/net/ip/IpReachabilityMonitor.java
@@ -23,7 +23,6 @@
 
 import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION;
-import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION;
 
 import android.content.Context;
 import android.net.ConnectivityManager;
@@ -236,7 +235,6 @@
     private int mInterSolicitIntervalMs;
     @NonNull
     private final Callback mCallback;
-    private final boolean mMulticastResolicitEnabled;
     private final boolean mIgnoreIncompleteIpv6DnsServerEnabled;
     private final boolean mIgnoreIncompleteIpv6DefaultRouterEnabled;
 
@@ -260,8 +258,6 @@
         mUsingMultinetworkPolicyTracker = usingMultinetworkPolicyTracker;
         mCm = context.getSystemService(ConnectivityManager.class);
         mDependencies = dependencies;
-        mMulticastResolicitEnabled = dependencies.isFeatureNotChickenedOut(context,
-                IP_REACHABILITY_MCAST_RESOLICIT_VERSION);
         mIgnoreIncompleteIpv6DnsServerEnabled = dependencies.isFeatureNotChickenedOut(context,
                 IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION);
         mIgnoreIncompleteIpv6DefaultRouterEnabled = dependencies.isFeatureEnabled(context,
@@ -274,10 +270,8 @@
         // In case the overylaid parameters specify an invalid configuration, set the parameters
         // to the hardcoded defaults first, then set them to the values used in the steady state.
         try {
-            int numResolicits = mMulticastResolicitEnabled
-                    ? NUD_MCAST_RESOLICIT_NUM
-                    : INVALID_NUD_MCAST_RESOLICIT_NUM;
-            setNeighborParameters(MIN_NUD_SOLICIT_NUM, MIN_NUD_SOLICIT_INTERVAL_MS, numResolicits);
+            setNeighborParameters(MIN_NUD_SOLICIT_NUM, MIN_NUD_SOLICIT_INTERVAL_MS,
+                    NUD_MCAST_RESOLICIT_NUM);
         } catch (Exception e) {
             Log.e(TAG, "Failed to adjust neighbor parameters with hardcoded defaults");
         }
@@ -414,8 +408,7 @@
 
     private void handleNeighborReachable(@Nullable final NeighborEvent prev,
             @NonNull final NeighborEvent event) {
-        if (mMulticastResolicitEnabled
-                && hasDefaultRouterNeighborMacAddressChanged(prev, event)) {
+        if (hasDefaultRouterNeighborMacAddressChanged(prev, event)) {
             // This implies device has confirmed the neighbor's reachability from
             // other states(e.g., NUD_PROBE or NUD_STALE), checking if the mac
             // address hasn't changed is required. If Mac address does change, then
@@ -581,8 +574,7 @@
 
     private long getProbeWakeLockDuration() {
         final long gracePeriodMs = 500;
-        final int numSolicits =
-                mNumSolicits + (mMulticastResolicitEnabled ? NUD_MCAST_RESOLICIT_NUM : 0);
+        final int numSolicits = mNumSolicits + NUD_MCAST_RESOLICIT_NUM;
         return (long) (numSolicits * mInterSolicitIntervalMs) + gracePeriodMs;
     }
 
diff --git a/src/com/android/networkstack/util/NetworkStackUtils.java b/src/com/android/networkstack/util/NetworkStackUtils.java
index 85da400..37f10ea 100755
--- a/src/com/android/networkstack/util/NetworkStackUtils.java
+++ b/src/com/android/networkstack/util/NetworkStackUtils.java
@@ -197,13 +197,6 @@
     public static final String IPCLIENT_GRATUITOUS_NA_VERSION = "ipclient_gratuitous_na_version";
 
     /**
-     * Experiment flag to send multicast NS from the global IPv6 GUA to the solicited-node
-     * multicast address based on the default router's IPv6 link-local address, which helps
-     * flush the first-hop routers' neighbor cache entry for the global IPv6 GUA.
-     */
-    public static final String IPCLIENT_MULTICAST_NS_VERSION = "ipclient_multicast_ns_version";
-
-    /**
      * Experiment flag to enable sending Gratuitous APR and Gratuitous Neighbor Advertisement for
      * all assigned IPv4 and IPv6 GUAs after completing L2 roaming.
      */
@@ -219,13 +212,6 @@
             "ipclient_accept_ipv6_link_local_dns_version";
 
     /**
-     * Experiment flag to enable "mcast_resolicit" neighbor parameter in IpReachabilityMonitor,
-     * set it to 3 by default.
-     */
-    public static final String IP_REACHABILITY_MCAST_RESOLICIT_VERSION =
-            "ip_reachability_mcast_resolicit_version";
-
-    /**
      * Experiment flag to attempt to ignore the on-link IPv6 DNS server which fails to respond to
      * address resolution.
      */
@@ -250,12 +236,22 @@
      */
     public static final String APF_NEW_RA_FILTER_VERSION = "apf_new_ra_filter_version";
     /**
+     * Experiment flag to enable the feature of polling counters in Apf.
+     */
+    public static final String APF_POLLING_COUNTERS_VERSION = "apf_polling_counters_version";
+    /**
      * Experiment flag to enable the feature of ignoring any individual RA section with lifetime
      * below accept_ra_min_lft sysctl.
      */
     public static final String IPCLIENT_IGNORE_LOW_RA_LIFETIME_VERSION =
             "ipclient_ignore_low_ra_lifetime_version";
 
+    /**
+     * Feature flag to send private DNS resolution queries and probes on a background thread.
+     */
+    public static final String NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION =
+            "networkmonitor_async_privdns_resolution";
+
     /**** BEGIN Feature Kill Switch Flags ****/
 
     /**
@@ -272,12 +268,6 @@
             "apf_handle_light_doze_force_disable";
 
     /**
-     * Kill switch flag to disable the feature of polling counters in Apf.
-     */
-    public static final String APF_POLLING_COUNTERS_FORCE_DISABLE =
-            "apf_polling_counters_force_disable";
-
-    /**
      * Kill switch flag to disable the feature of skipping Tcp socket info polling when light
      * doze mode is enabled.
      */
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java
index 9303f95..78a47ea 100755
--- a/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -23,6 +23,9 @@
 import static android.net.ConnectivityManager.EXTRA_CAPTIVE_PORTAL_PROBE_SPEC;
 import static android.net.ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL;
 import static android.net.DnsResolver.FLAG_EMPTY;
+import static android.net.DnsResolver.FLAG_NO_CACHE_LOOKUP;
+import static android.net.DnsResolver.TYPE_A;
+import static android.net.DnsResolver.TYPE_AAAA;
 import static android.net.INetworkMonitor.NETWORK_TEST_RESULT_INVALID;
 import static android.net.INetworkMonitor.NETWORK_TEST_RESULT_PARTIAL_CONNECTIVITY;
 import static android.net.INetworkMonitor.NETWORK_TEST_RESULT_VALID;
@@ -122,6 +125,7 @@
 import android.net.wifi.WifiManager;
 import android.os.Build;
 import android.os.Bundle;
+import android.os.CancellationSignal;
 import android.os.Message;
 import android.os.Process;
 import android.os.RemoteException;
@@ -140,7 +144,9 @@
 import android.telephony.SignalStrength;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
+import android.util.ArraySet;
 import android.util.Log;
+import android.util.Pair;
 import android.util.SparseArray;
 
 import androidx.annotation.ArrayRes;
@@ -234,6 +240,10 @@
     @VisibleForTesting
     static final String CONFIG_CAPTIVE_PORTAL_DNS_PROBE_TIMEOUT =
             "captive_portal_dns_probe_timeout";
+    @VisibleForTesting
+    static final String CONFIG_ASYNC_PRIVDNS_PROBE_TIMEOUT_MS =
+            "async_privdns_probe_timeout_ms";
+    private static final int DEFAULT_PRIVDNS_PROBE_TIMEOUT_MS = 10_000;
 
     private static final int SOCKET_TIMEOUT_MS = 10000;
     private static final int PROBE_TIMEOUT_MS  = 3000;
@@ -404,6 +414,32 @@
      */
     private static final int EVENT_RESOURCE_CONFIG_CHANGED = 25;
 
+    /**
+     * Message to self to notify that private DNS strict mode hostname resolution has finished.
+     *
+     * <p>arg2 = Last DNS rcode.
+     * obj = Pair&lt;List&lt;InetAddress&gt;, DnsCallback&gt;: query results and DnsCallback used.
+     */
+    private static final int CMD_STRICT_MODE_RESOLUTION_COMPLETED = 26;
+
+    /**
+     * Message to self to notify that the private DNS probe has finished.
+     *
+     * <p>arg2 = Last DNS rcode.
+     * obj = Pair&lt;List&lt;InetAddress&gt;, DnsCallback&gt;: query results and DnsCallback used.
+     */
+    private static final int CMD_PRIVATE_DNS_PROBE_COMPLETED = 27;
+
+    /**
+     * Message to self to notify that private DNS hostname resolution or probing has failed.
+     */
+    private static final int CMD_PRIVATE_DNS_EVALUATION_FAILED = 28;
+
+    /**
+     * Message to self to notify that a DNS query has timed out.
+     */
+    private static final int CMD_DNS_TIMEOUT = 29;
+
     // Start mReevaluateDelayMs at this value and double.
     @VisibleForTesting
     static final int INITIAL_REEVALUATE_DELAY_MS = 1000;
@@ -504,6 +540,10 @@
     private final State mEvaluatingState = new EvaluatingState();
     private final State mCaptivePortalState = new CaptivePortalState();
     private final State mEvaluatingPrivateDnsState = new EvaluatingPrivateDnsState();
+    private final State mStartingPrivateDnsEvaluation = new StartingPrivateDnsEvaluation();
+    private final State mResolvingPrivateDnsState = new ResolvingPrivateDnsState();
+    private final State mProbingForPrivateDnsState = new ProbingForPrivateDnsState();
+
     private final State mProbingState = new ProbingState();
     private final State mWaitingForNextProbeState = new WaitingForNextProbeState();
     private final State mEvaluatingBandwidthState = new EvaluatingBandwidthState();
@@ -544,6 +584,8 @@
 
     private final boolean mMetricsEnabled;
     private final boolean mReevaluateWhenResumeEnabled;
+    private final boolean mAsyncPrivdnsResolutionEnabled;
+
     @NonNull
     private final NetworkInformationShim mInfoShim = NetworkInformationShimImpl.newInstance();
 
@@ -614,6 +656,9 @@
                 addState(mWaitingForNextProbeState, mEvaluatingState);
             addState(mCaptivePortalState, mMaybeNotifyState);
         addState(mEvaluatingPrivateDnsState, mDefaultState);
+            addState(mStartingPrivateDnsEvaluation, mEvaluatingPrivateDnsState);
+            addState(mResolvingPrivateDnsState, mEvaluatingPrivateDnsState);
+            addState(mProbingForPrivateDnsState, mEvaluatingPrivateDnsState);
         addState(mEvaluatingBandwidthState, mDefaultState);
         addState(mValidatedState, mDefaultState);
         setInitialState(mDefaultState);
@@ -632,6 +677,8 @@
                 NetworkStackUtils.VALIDATION_METRICS_VERSION);
         mReevaluateWhenResumeEnabled = deps.isFeatureEnabled(
                 context, NetworkStackUtils.REEVALUATE_WHEN_RESUME);
+        mAsyncPrivdnsResolutionEnabled = deps.isFeatureEnabled(context,
+                NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION);
         mUseHttps = getUseHttpsValidation();
         mCaptivePortalUserAgent = getCaptivePortalUserAgent();
         mCaptivePortalFallbackSpecs =
@@ -1049,6 +1096,13 @@
                         if (tst != null) {
                             tst.setOpportunisticMode(cfg.inOpportunisticMode());
                         }
+                        if (mAsyncPrivdnsResolutionEnabled) {
+                            // When using async privdns validation, reevaluate on any change of
+                            // configuration (even if turning it off), as this will handle
+                            // cancelling current attempts and transitioning to validated state.
+                            removeMessages(CMD_EVALUATE_PRIVATE_DNS);
+                            sendMessage(CMD_EVALUATE_PRIVATE_DNS);
+                        }
                         break;
                     }
 
@@ -1599,13 +1653,28 @@
         @Override
         public boolean processMessage(Message msg) {
             switch (msg.what) {
-                case CMD_EVALUATE_PRIVATE_DNS:
+                case CMD_EVALUATE_PRIVATE_DNS: {
+                    if (mAsyncPrivdnsResolutionEnabled) {
+                        // Cancel any previously scheduled retry attempt
+                        removeMessages(CMD_EVALUATE_PRIVATE_DNS);
+
+                        if (inStrictMode()) {
+                            // Note this may happen even in the case where the current state is
+                            // resolve or probe: private DNS evaluation would then restart.
+                            transitionTo(mStartingPrivateDnsEvaluation);
+                        } else {
+                            mEvaluationState.removeProbeResult(NETWORK_VALIDATION_PROBE_PRIVDNS);
+                            transitionToPrivateDnsEvaluationSuccessState();
+                        }
+                        break;
+                    }
+
                     if (inStrictMode()) {
-                        if (!isStrictModeHostnameResolved()) {
+                        if (!isStrictModeHostnameResolved(mPrivateDnsConfig)) {
                             resolveStrictModeHostname();
 
-                            if (isStrictModeHostnameResolved()) {
-                                notifyPrivateDnsConfigResolved();
+                            if (isStrictModeHostnameResolved(mPrivateDnsConfig)) {
+                                notifyPrivateDnsConfigResolved(mPrivateDnsConfig);
                             } else {
                                 handlePrivateDnsEvaluationFailure();
                                 // The private DNS probe fails-fast if the server hostname cannot
@@ -1630,23 +1699,24 @@
                             handlePrivateDnsEvaluationFailure();
                             break;
                         }
-                        handlePrivateDnsEvaluationSuccess();
+                        mEvaluationState.noteProbeResult(NETWORK_VALIDATION_PROBE_PRIVDNS,
+                                true /* succeeded */);
                     } else {
                         mEvaluationState.removeProbeResult(NETWORK_VALIDATION_PROBE_PRIVDNS);
                     }
-
-                    if (needEvaluatingBandwidth()) {
-                        transitionTo(mEvaluatingBandwidthState);
-                    } else {
-                        // All good!
-                        transitionTo(mValidatedState);
-                    }
+                    transitionToPrivateDnsEvaluationSuccessState();
                     break;
-                case CMD_PRIVATE_DNS_SETTINGS_CHANGED:
+                }
+                case CMD_PRIVATE_DNS_SETTINGS_CHANGED: {
                     // When settings change the reevaluation timer must be reset.
                     mPrivateDnsReevalDelayMs = INITIAL_REEVALUATE_DELAY_MS;
                     // Let the message bubble up and be handled by parent states as usual.
                     return NOT_HANDLED;
+                }
+                // Only used with mAsyncPrivdnsResolutionEnabled
+                case CMD_PRIVATE_DNS_EVALUATION_FAILED: {
+                    reschedulePrivateDnsEvaluation();
+                }
                 default:
                     return NOT_HANDLED;
             }
@@ -1657,12 +1727,6 @@
             return !TextUtils.isEmpty(mPrivateDnsProviderHostname);
         }
 
-        private boolean isStrictModeHostnameResolved() {
-            return (mPrivateDnsConfig != null)
-                    && mPrivateDnsConfig.hostname.equals(mPrivateDnsProviderHostname)
-                    && (mPrivateDnsConfig.ips.length > 0);
-        }
-
         private void resolveStrictModeHostname() {
             try {
                 // Do a blocking DNS resolution using the network-assigned nameservers.
@@ -1675,24 +1739,15 @@
             }
         }
 
-        private void notifyPrivateDnsConfigResolved() {
-            try {
-                mCallback.notifyPrivateDnsConfigResolved(mPrivateDnsConfig.toParcel());
-            } catch (RemoteException e) {
-                Log.e(TAG, "Error sending private DNS config resolved notification", e);
-            }
-        }
-
-        private void handlePrivateDnsEvaluationSuccess() {
-            mEvaluationState.noteProbeResult(NETWORK_VALIDATION_PROBE_PRIVDNS,
-                    true /* succeeded */);
-        }
-
         private void handlePrivateDnsEvaluationFailure() {
             mEvaluationState.noteProbeResult(NETWORK_VALIDATION_PROBE_PRIVDNS,
                     false /* succeeded */);
             mEvaluationState.reportEvaluationResult(NETWORK_VALIDATION_RESULT_INVALID,
                     null /* redirectUrl */);
+            reschedulePrivateDnsEvaluation();
+        }
+
+        private void reschedulePrivateDnsEvaluation() {
             // Queue up a re-evaluation with backoff.
             //
             // TODO: Consider abandoning this state after a few attempts and
@@ -1730,6 +1785,285 @@
         }
     }
 
+    private void transitionToPrivateDnsEvaluationSuccessState() {
+        if (needEvaluatingBandwidth()) {
+            transitionTo(mEvaluatingBandwidthState);
+        } else {
+            // All good!
+            transitionTo(mValidatedState);
+        }
+    }
+
+    private class StartingPrivateDnsEvaluation extends State {
+        @Override
+        public void enter() {
+            transitionTo(mResolvingPrivateDnsState);
+        }
+    }
+
+    private class DnsCallback implements DnsResolver.Callback<List<InetAddress>> {
+        private final int mReplyMessage;
+        final CancellationSignal mCancellationSignal;
+        final boolean mHighPriorityResults;
+
+        DnsCallback(int replyMessage, boolean highPriorityResults) {
+            mReplyMessage = replyMessage;
+            mCancellationSignal = new CancellationSignal();
+            mHighPriorityResults = highPriorityResults;
+        }
+
+        @Override
+        public void onAnswer(List<InetAddress> answer, int rcode) {
+            sendMessage(mReplyMessage, 0, rcode, new Pair<>(answer, this));
+        }
+
+        @Override
+        public void onError(DnsResolver.DnsException error) {
+            sendMessage(mReplyMessage, 0, error.code, new Pair<>(null, this));
+        }
+    }
+
+    /**
+     * Base class for a state that is sending a DNS query, cancelled if the state is exited.
+     */
+    private abstract class DnsQueryState extends State {
+        private static final int ERROR_TIMEOUT = -1;
+        private final int mCompletedCommand;
+        private final ArraySet<DnsCallback> mPendingQueries = new ArraySet<>(2);
+        private final List<InetAddress> mResults = new ArrayList<>();
+        private String mQueryName;
+        private long mStartTime;
+
+        private DnsQueryState(int completedCommand) {
+            mCompletedCommand = completedCommand;
+        }
+
+        @Override
+        public void enter() {
+            mPendingQueries.clear();
+            mResults.clear();
+            mStartTime = SystemClock.elapsedRealtimeNanos();
+
+            mQueryName = getQueryName();
+            if (TextUtils.isEmpty(mQueryName)) {
+                // No query necessary (in particular not in strict mode): skip DNS query states
+                mEvaluationState.removeProbeResult(NETWORK_VALIDATION_PROBE_PRIVDNS);
+                transitionToPrivateDnsEvaluationSuccessState();
+                return;
+            }
+
+            final DnsResolver resolver = mDependencies.getDnsResolver();
+            mPendingQueries.addAll(sendQueries(mQueryName, resolver));
+            sendMessageDelayed(CMD_DNS_TIMEOUT, getTimeoutMs());
+        }
+
+        @Override
+        public void exit() {
+            removeMessages(CMD_DNS_TIMEOUT);
+            cancelAllQueries();
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            if (msg.what == mCompletedCommand) {
+                final Pair<List<InetAddress>, DnsCallback> result =
+                        (Pair<List<InetAddress>, DnsCallback>) msg.obj;
+                if (!mPendingQueries.remove(result.second)) {
+                    // Ignore previous queries if the state was exited and re-entered. This state
+                    // calls cancelAllQueries on exit, but this may still happen if results were
+                    // already posted when the querier processed the cancel request.
+                    return HANDLED;
+                }
+
+                if (result.first != null) {
+                    if (result.second.mHighPriorityResults) {
+                        mResults.addAll(0, result.first);
+                    } else {
+                        mResults.addAll(result.first);
+                    }
+                }
+
+                if (mPendingQueries.isEmpty()) {
+                    removeMessages(CMD_DNS_TIMEOUT);
+                    final long time = SystemClock.elapsedRealtimeNanos() - mStartTime;
+                    onQueryDone(mQueryName, mResults, msg.arg2 /* lastRCode */, time);
+                }
+                return HANDLED;
+            } else if (msg.what == CMD_DNS_TIMEOUT) {
+                cancelAllQueries();
+                // If some queries were successful, onQueryDone will still proceed, even if
+                // lastRCode is not a success code.
+                onQueryDone(mQueryName, mResults, ERROR_TIMEOUT /* lastRCode */,
+                        SystemClock.elapsedRealtimeNanos() - mStartTime);
+                return HANDLED;
+            }
+            return NOT_HANDLED;
+        }
+
+        private void cancelAllQueries() {
+            for (int i = 0; i < mPendingQueries.size(); i++) {
+                mPendingQueries.valueAt(i).mCancellationSignal.cancel();
+            }
+            mPendingQueries.clear();
+        }
+
+        abstract void onQueryDone(@NonNull String queryName, @NonNull List<InetAddress> answer,
+                int lastRCode, long elapsedNanos);
+
+        @NonNull
+        abstract String getQueryName();
+
+        abstract List<DnsCallback> sendQueries(@NonNull String queryName,
+                @NonNull DnsResolver resolver);
+
+        abstract long getTimeoutMs();
+    }
+
+    private class ResolvingPrivateDnsState extends DnsQueryState {
+        private ResolvingPrivateDnsState() {
+            super(CMD_STRICT_MODE_RESOLUTION_COMPLETED);
+        }
+
+        @Override
+        List<DnsCallback> sendQueries(@NonNull String queryName, @NonNull DnsResolver resolver) {
+            // Follow legacy behavior that sent AAAA and A queries synchronously in sequence: AAAA
+            // is marked as highPriorityResults, so they are placed first in the resulting list.
+            final DnsCallback v6Cb = new DnsCallback(CMD_STRICT_MODE_RESOLUTION_COMPLETED,
+                    true /* highPriorityResults */);
+            final DnsCallback v4Cb = new DnsCallback(CMD_STRICT_MODE_RESOLUTION_COMPLETED,
+                    false /* highPriorityResults */);
+
+            resolver.query(mCleartextDnsNetwork, queryName, TYPE_AAAA, FLAG_NO_CACHE_LOOKUP,
+                    Runnable::run, v6Cb.mCancellationSignal, v6Cb);
+            resolver.query(mCleartextDnsNetwork, queryName, TYPE_A, FLAG_NO_CACHE_LOOKUP,
+                    Runnable::run, v4Cb.mCancellationSignal, v4Cb);
+
+            return List.of(v6Cb, v4Cb);
+        }
+
+        @Override
+        void onQueryDone(@NonNull String queryName, @NonNull List<InetAddress> answer,
+                int lastRCode, long elapsedNanos) {
+            if (!Objects.equals(queryName, mPrivateDnsProviderHostname)) {
+                validationLog("Ignoring stale private DNS resolve answers for " + queryName
+                        + " (now \"" + mPrivateDnsProviderHostname + "\"): " + answer);
+                // This may happen if mPrivateDnsProviderHostname was changed, in which case
+                // reevaluation must have been queued (CMD_EVALUATE_PRIVATE_DNS), but results for
+                // the first evaluation are received before the reevaluation command gets processed.
+                // Just ignore the results and wait for reevaluation to be processed.
+                // More generally, reevaluation is scheduled every time the hostname changes, so
+                // IP addresses matching the hostname are eventually received, but intermediate
+                // results should be ignored to avoid reporting a PrivateDnsConfig with IP addresses
+                // that don't match mPrivateDnsProviderHostname.
+                return;
+            }
+
+            if (!answer.isEmpty()) {
+                final InetAddress[] ips = answer.toArray(new InetAddress[0]);
+                final PrivateDnsConfig config =
+                        new PrivateDnsConfig(mPrivateDnsProviderHostname, ips);
+                notifyPrivateDnsConfigResolved(config);
+
+                validationLog("Strict mode hostname resolution " + elapsedNanos + "ns OK "
+                        + answer + " for " + mPrivateDnsProviderHostname);
+                transitionTo(mProbingForPrivateDnsState);
+            } else {
+                mEvaluationState.noteProbeResult(NETWORK_VALIDATION_PROBE_PRIVDNS,
+                        false /* succeeded */);
+                mEvaluationState.reportEvaluationResult(NETWORK_VALIDATION_RESULT_INVALID,
+                        null /* redirectUrl */);
+
+                validationLog("Strict mode hostname resolution " + elapsedNanos + "ns FAIL "
+                        + "lastRCode " + lastRCode + " for " + mPrivateDnsProviderHostname);
+                sendMessage(CMD_PRIVATE_DNS_EVALUATION_FAILED);
+
+                // The private DNS probe fails-fast if the server hostname cannot
+                // be resolved. Record it as a failure with zero latency.
+                recordProbeEventMetrics(ProbeType.PT_PRIVDNS, 0 /* latency */,
+                        ProbeResult.PR_FAILURE, null /* capportData */);
+            }
+        }
+
+        @NonNull
+        @Override
+        String getQueryName() {
+            return mPrivateDnsProviderHostname;
+        }
+
+        @Override
+        long getTimeoutMs() {
+            return getDnsProbeTimeout();
+        }
+    }
+
+    private class ProbingForPrivateDnsState extends DnsQueryState {
+        private ProbingForPrivateDnsState() {
+            super(CMD_PRIVATE_DNS_PROBE_COMPLETED);
+        }
+
+        @Override
+        public void enter() {
+            super.enter();
+        }
+
+        @Override
+        List<DnsCallback> sendQueries(@NonNull String queryName, @NonNull DnsResolver resolver) {
+            final DnsCallback cb = new DnsCallback(CMD_PRIVATE_DNS_PROBE_COMPLETED,
+                    false /* highPriorityResults */);
+            resolver.query(mNetwork, queryName, FLAG_EMPTY, Runnable::run, cb.mCancellationSignal,
+                    cb);
+            return Collections.singletonList(cb);
+        }
+
+        @Override
+        void onQueryDone(@NonNull String queryName, @NonNull List<InetAddress> answer,
+                int lastRCode, long elapsedNanos) {
+            final boolean success = !answer.isEmpty();
+            recordProbeEventMetrics(ProbeType.PT_PRIVDNS, elapsedNanos,
+                    success ? ProbeResult.PR_SUCCESS :
+                            ProbeResult.PR_FAILURE, null /* capportData */);
+            logValidationProbe(elapsedNanos, PROBE_PRIVDNS, success ? DNS_SUCCESS : DNS_FAILURE);
+
+            final String strIps = Objects.toString(answer);
+            validationLog(PROBE_PRIVDNS, queryName,
+                    String.format("%dus: %s", elapsedNanos / 1000, strIps));
+
+            mEvaluationState.noteProbeResult(NETWORK_VALIDATION_PROBE_PRIVDNS, success);
+            if (success) {
+                transitionToPrivateDnsEvaluationSuccessState();
+            } else {
+                mEvaluationState.reportEvaluationResult(NETWORK_VALIDATION_RESULT_INVALID,
+                        null /* redirectUrl */);
+                sendMessage(CMD_PRIVATE_DNS_EVALUATION_FAILED);
+            }
+        }
+
+        @Override
+        long getTimeoutMs() {
+            return getAsyncPrivateDnsProbeTimeout();
+        }
+
+        @NonNull
+        @Override
+        String getQueryName() {
+            return UUID.randomUUID().toString().substring(0, 8) + PRIVATE_DNS_PROBE_HOST_SUFFIX;
+        }
+    }
+
+    private boolean isStrictModeHostnameResolved(PrivateDnsConfig config) {
+        return (config != null)
+                && config.hostname.equals(mPrivateDnsProviderHostname)
+                && (config.ips.length > 0);
+    }
+
+    private void notifyPrivateDnsConfigResolved(@NonNull PrivateDnsConfig config) {
+        try {
+            mCallback.notifyPrivateDnsConfigResolved(config.toParcel());
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error sending private DNS config resolved notification", e);
+        }
+    }
+
     private class ProbingState extends State {
         private Thread mThread;
 
@@ -2186,6 +2520,11 @@
                 CONFIG_CAPTIVE_PORTAL_DNS_PROBE_TIMEOUT, DEFAULT_CAPTIVE_PORTAL_DNS_PROBE_TIMEOUT);
     }
 
+    private int getAsyncPrivateDnsProbeTimeout() {
+        return mDependencies.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY,
+                CONFIG_ASYNC_PRIVDNS_PROBE_TIMEOUT_MS, DEFAULT_PRIVDNS_PROBE_TIMEOUT_MS);
+    }
+
     /**
      * Gets an integer setting from resources or device config
      *
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
index 8300c73..ec058d7 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -68,6 +68,9 @@
     static_libs: [
         "NetworkStackApiStableLib",
     ],
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Network stack integration tests.
@@ -84,6 +87,9 @@
     jarjar_rules: ":NetworkStackJarJarRules",
     host_required: ["net-tests-utils-host-common"],
     test_config_template: "AndroidTestTemplate_Integration.xml",
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Network stack next integration tests.
@@ -107,6 +113,9 @@
     jarjar_rules: ":NetworkStackJarJarRules",
     host_required: ["net-tests-utils-host-common"],
     test_config_template: "AndroidTestTemplate_Integration.xml",
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Network stack integration root tests.
@@ -124,12 +133,18 @@
         "NetworkStackApiStableLib",
     ],
     platform_apis: true,
-    test_suites: ["general-tests", "mts-networking"],
+    test_suites: [
+        "general-tests",
+        "mts-networking",
+    ],
     compile_multilib: "both",
     manifest: "AndroidManifest_root.xml",
     jarjar_rules: ":NetworkStackJarJarRules",
     host_required: ["net-tests-utils-host-common"],
     test_config_template: "AndroidTestTemplate_Integration.xml",
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Special version of the network stack tests that includes all tests necessary for code coverage
@@ -138,7 +153,10 @@
     name: "NetworkStackCoverageTests",
     certificate: "networkstack",
     platform_apis: true,
-    test_suites: ["device-tests", "mts-networking"],
+    test_suites: [
+        "device-tests",
+        "mts-networking",
+    ],
     test_config: "AndroidTest_Coverage.xml",
     defaults: [
         "NetworkStackReleaseTargetSdk",
@@ -154,4 +172,7 @@
     compile_multilib: "both",
     manifest: "AndroidManifest_coverage.xml",
     jarjar_rules: ":NetworkStackJarJarRules",
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
diff --git a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
index c733f9c..2f1f7d1 100644
--- a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
+++ b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
@@ -312,6 +312,7 @@
 
     protected static final long TEST_TIMEOUT_MS = 2_000L;
     private static final long TEST_WAIT_ENOBUFS_TIMEOUT_MS = 30_000L;
+    private static final long TEST_WAIT_RENEW_REBIND_RETRANSMIT_MS = 15_000L;
 
     @Rule
     public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
@@ -779,6 +780,8 @@
             enableRealAlarm("DhcpClient." + mIfaceName + ".KICK");
             // Enable alarm for IPv6 autoconf via SLAAC in IpClient.
             enableRealAlarm("IpClient." + mIfaceName + ".EVENT_IPV6_AUTOCONF_TIMEOUT");
+            // Enable packet retransmit alarm in Dhcp6Client.
+            enableRealAlarm("Dhcp6Client." + mIfaceName + ".KICK");
         }
 
         mIIpClient = makeIIpClient(mIfaceName, mCb);
@@ -4137,12 +4140,6 @@
         return ns;
     }
 
-    // Override this function with disabled experiment flag by default, in order not to
-    // affect those tests which are just related to basic IpReachabilityMonitor infra.
-    private void prepareIpReachabilityMonitorTest() throws Exception {
-        prepareIpReachabilityMonitorTest(false /* isMulticastResolicitEnabled */);
-    }
-
     private void assertNotifyNeighborLost(Inet6Address targetIp, NudEventType eventType)
             throws Exception {
         // For root test suite, rely on the IIpClient aidl interface version constant defined in
@@ -4175,8 +4172,7 @@
         verify(mCb, never()).onReachabilityLost(any());
     }
 
-    private void prepareIpReachabilityMonitorTest(boolean isMulticastResolicitEnabled)
-            throws Exception {
+    private void prepareIpReachabilityMonitorTest() throws Exception {
         final ScanResultInfo info = makeScanResultInfo(TEST_DEFAULT_SSID, TEST_DEFAULT_BSSID);
         ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                 .withLayer2Information(new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
@@ -4185,8 +4181,6 @@
                 .withDisplayName(TEST_DEFAULT_SSID)
                 .withoutIPv4()
                 .build();
-        setFeatureEnabled(NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION,
-                isMulticastResolicitEnabled);
         startIpClientProvisioning(config);
         verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true);
         doIpv6OnlyProvisioning();
@@ -4200,11 +4194,15 @@
 
         final List<NeighborSolicitation> nsList = waitForMultipleNeighborSolicitations();
         final int expectedNudSolicitNum = readNudSolicitNumPostRoamingFromResource();
-        assertEquals(expectedNudSolicitNum, nsList.size());
-        for (NeighborSolicitation ns : nsList) {
+        int expectedSize = expectedNudSolicitNum + NUD_MCAST_RESOLICIT_NUM;
+        assertEquals(expectedSize, nsList.size());
+        for (NeighborSolicitation ns : nsList.subList(0, expectedNudSolicitNum)) {
             assertUnicastNeighborSolicitation(ns, ROUTER_MAC /* dstMac */,
                     ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */);
         }
+        for (NeighborSolicitation ns : nsList.subList(expectedNudSolicitNum, nsList.size())) {
+            assertMulticastNeighborSolicitation(ns, ROUTER_LINK_LOCAL /* targetIp */);
+        }
     }
 
     @Test
@@ -4239,43 +4237,10 @@
         assertNeverNotifyNeighborLost();
     }
 
-    private void runIpReachabilityMonitorMcastResolicitProbeFailedTest() throws Exception {
-        prepareIpReachabilityMonitorTest(true /* isMulticastResolicitEnabled */);
-
-        final List<NeighborSolicitation> nsList = waitForMultipleNeighborSolicitations();
-        final int expectedNudSolicitNum = readNudSolicitNumPostRoamingFromResource();
-        int expectedSize = expectedNudSolicitNum + NUD_MCAST_RESOLICIT_NUM;
-        assertEquals(expectedSize, nsList.size());
-        for (NeighborSolicitation ns : nsList.subList(0, expectedNudSolicitNum)) {
-            assertUnicastNeighborSolicitation(ns, ROUTER_MAC /* dstMac */,
-                    ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */);
-        }
-        for (NeighborSolicitation ns : nsList.subList(expectedNudSolicitNum, nsList.size())) {
-            assertMulticastNeighborSolicitation(ns, ROUTER_LINK_LOCAL /* targetIp */);
-        }
-    }
-
-    @Test
-    public void testIpReachabilityMonitor_mcastResolicitProbeFailed() throws Exception {
-        runIpReachabilityMonitorMcastResolicitProbeFailedTest();
-        assertNotifyNeighborLost(ROUTER_LINK_LOCAL /* targetIp */,
-                NudEventType.NUD_POST_ROAMING_FAILED_CRITICAL);
-    }
-
-    @Test @SignatureRequiredTest(reason = "requires mock callback object")
-    public void testIpReachabilityMonitor_mcastResolicitProbeFailed_legacyCallback()
-            throws Exception {
-        when(mCb.getInterfaceVersion()).thenReturn(12 /* assign an older interface aidl version */);
-
-        runIpReachabilityMonitorMcastResolicitProbeFailedTest();
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).onReachabilityLost(any());
-        verify(mCb, never()).onReachabilityFailure(any());
-    }
-
     @Test
     public void testIpReachabilityMonitor_mcastResolicitProbeReachableWithSameLinkLayerAddress()
             throws Exception {
-        prepareIpReachabilityMonitorTest(true /* isMulticastResolicitEnabled */);
+        prepareIpReachabilityMonitorTest();
 
         final NeighborSolicitation ns = waitForUnicastNeighborSolicitation(ROUTER_MAC /* dstMac */,
                 ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */);
@@ -4292,7 +4257,7 @@
     @Test
     public void testIpReachabilityMonitor_mcastResolicitProbeReachableWithDiffLinkLayerAddress()
             throws Exception {
-        prepareIpReachabilityMonitorTest(true /* isMulticastResolicitEnabled */);
+        prepareIpReachabilityMonitorTest();
 
         final NeighborSolicitation ns = waitForUnicastNeighborSolicitation(ROUTER_MAC /* dstMac */,
                 ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */);
@@ -4664,9 +4629,6 @@
                 .withoutIPv4()
                 .build();
 
-        setFeatureEnabled(NetworkStackUtils.IPCLIENT_MULTICAST_NS_VERSION,
-                true /* isUnsolicitedNsEnabled */);
-        assertTrue(isFeatureEnabled(NetworkStackUtils.IPCLIENT_MULTICAST_NS_VERSION));
         startIpClientProvisioning(config);
 
         doIpv6OnlyProvisioning();
@@ -5216,9 +5178,7 @@
                 x -> x.isIpv6Provisioned()
                         && hasIpv6AddressPrefixedWith(x, prefix)
                         && hasIpv6AddressPrefixedWith(x, prefix1)
-                        // TODO: comment this line to make the test passed, remove the comment later
-                        // once IpClient supports multi-prefixes.
-                        // && hasRouteTo(x, "2001:db8:1::/64", RTN_UNREACHABLE)
+                        && 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
@@ -5280,14 +5240,15 @@
         mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
                 (Inet6Address) mClientIpAddress, false /* rapidCommit */));
         verify(mCb, never()).onProvisioningFailure(any());
+        // IPv6 addresses derived from prefix with 0 preferred/valid lifetime should be deleted.
         verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(argThat(
                 x -> x.isIpv6Provisioned()
-                        && hasIpv6AddressPrefixedWith(x, prefix)
+                        && !hasIpv6AddressPrefixedWith(x, prefix)
                         && hasIpv6AddressPrefixedWith(x, prefix1)
-                        && hasRouteTo(x, "2001:db8:1::/64", RTN_UNREACHABLE)
+                        && !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
+                        // IPv6 link-local, two global delegated IPv6 addresses with prefix1
+                        && x.getLinkAddresses().size() == 3
         ));
 
         handler.post(() -> renewAlarm.onAlarm());
@@ -5296,7 +5257,8 @@
         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(1, renewIpos.size()); // don't renew prefix 2001:db8:1::/64 with 0
+                                           // preferred/valid lifetime
         assertEquals(prefix1, renewIpos.get(0).getIpPrefix());
     }
 
@@ -5317,7 +5279,7 @@
 
         clearInvocations(mCb);
 
-        // Reply with the requested prefix with preferred/valid lifetime of 0.
+        // Reply with the requested prefix with the same t1/t2/lifetime.
         final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
         final IaPrefixOption ipo = buildIaPrefixOption(prefix, 3600 /* preferred */,
                 3600 /* valid */);
@@ -5339,6 +5301,220 @@
     }
 
     @Test
+    public void testDhcp6Pd_multipleIaPrefixOptions() throws Exception {
+        final InOrder inOrder = inOrder(mCb);
+        final IpPrefix prefix1 = new IpPrefix("2001:db8:1::/64");
+        final IpPrefix prefix2 = new IpPrefix("2400:db8:100::/64");
+        final IpPrefix prefix3 = new IpPrefix("fd7c:9df8:7f39:dc89::/64");
+        final IaPrefixOption ipo1 = buildIaPrefixOption(prefix1, 4500 /* preferred */,
+                7200 /* valid */);
+        final IaPrefixOption ipo2 = buildIaPrefixOption(prefix2, 5600 /* preferred */,
+                6000 /* valid */);
+        final IaPrefixOption ipo3 = buildIaPrefixOption(prefix3, 7200 /* preferred */,
+                14400 /* valid */);
+        prepareDhcp6PdTest();
+        handleDhcp6Packets(Arrays.asList(ipo1, ipo2, ipo3), 3600 /* t1 */, 4500 /* t2 */,
+                true /* shouldReplyRapidCommit */);
+
+        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
+        verifyWithTimeout(inOrder, mCb).onProvisioningSuccess(captor.capture());
+        LinkProperties lp = captor.getValue();
+
+        // Sometimes privacy address or route may appear later along with onLinkPropertiesChange
+        // callback, in this case we wait a bit longer to see all of these properties appeared and
+        // then verify if they are what we are looking for.
+        if (lp.getLinkAddresses().size() < 5 || lp.getRoutes().size() < 4) {
+            final CompletableFuture<LinkProperties> lpFuture = new CompletableFuture<>();
+            verifyWithTimeout(inOrder, mCb).onLinkPropertiesChange(argThat(x -> {
+                if (!x.isIpv6Provisioned()) return false;
+                if (x.getLinkAddresses().size() != 5) return false;
+                if (x.getRoutes().size() != 4) return false;
+                lpFuture.complete(x);
+                return true;
+            }));
+            lp = lpFuture.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        }
+        assertNotNull(lp);
+        assertTrue(hasIpv6AddressPrefixedWith(lp, prefix1));
+        assertTrue(hasIpv6AddressPrefixedWith(lp, prefix2));
+        assertFalse(hasIpv6AddressPrefixedWith(lp, prefix3));
+        assertTrue(hasRouteTo(lp, prefix1.toString(), RTN_UNREACHABLE));
+        assertTrue(hasRouteTo(lp, prefix2.toString(), RTN_UNREACHABLE));
+        assertFalse(hasRouteTo(lp, prefix3.toString(), RTN_UNREACHABLE));
+    }
+
+    private void runDhcp6PacketWithNoPrefixAvailStatusCodeTest(boolean shouldReplyWithAdvertise)
+            throws Exception {
+        prepareDhcp6PdTest();
+        Dhcp6Packet packet = getNextDhcp6Packet(PACKET_TIMEOUT_MS);
+        assertTrue(packet instanceof Dhcp6SolicitPacket);
+
+        final PrefixDelegation pd = new PrefixDelegation(packet.getIaId(), 0 /* t1 */, 0 /* t2 */,
+                new ArrayList<IaPrefixOption>() /* ipos */, Dhcp6Packet.STATUS_NO_PREFIX_AVAIL);
+        final ByteBuffer iapd = pd.build();
+        if (shouldReplyWithAdvertise) {
+            mPacketReader.sendResponse(buildDhcp6Advertise(packet, iapd.array(), mClientMac,
+                    (Inet6Address) mClientIpAddress));
+        } else {
+            mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
+                    (Inet6Address) mClientIpAddress, true /* rapidCommit */));
+        }
+
+        // Check if client will ignore Advertise or Reply for Rapid Commit Solicit and
+        // retransmit Solicit.
+        packet = getNextDhcp6Packet(PACKET_TIMEOUT_MS);
+        assertTrue(packet instanceof Dhcp6SolicitPacket);
+    }
+
+    @Test
+    public void testDhcp6AdvertiseWithNoPrefixAvailStatusCode() throws Exception {
+        // Advertise
+        runDhcp6PacketWithNoPrefixAvailStatusCodeTest(true /* shouldReplyWithAdvertise */);
+    }
+
+    @Test
+    public void testDhcp6ReplyForRapidCommitSolicitWithNoPrefixAvailStatusCode() throws Exception {
+        // Reply
+        runDhcp6PacketWithNoPrefixAvailStatusCodeTest(false /* shouldReplyWithAdvertise */);
+    }
+
+    @Test
+    public void testDhcp6ReplyForRequestWithNoPrefixAvailStatusCode() throws Exception {
+        prepareDhcp6PdTest();
+        Dhcp6Packet packet = getNextDhcp6Packet(PACKET_TIMEOUT_MS);
+        assertTrue(packet instanceof Dhcp6SolicitPacket);
+
+        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
+        final IaPrefixOption ipo = buildIaPrefixOption(prefix, 4500 /* preferred */,
+                7200 /* valid */);
+        PrefixDelegation pd = new PrefixDelegation(packet.getIaId(), 1000 /* t1 */,
+                2000 /* t2 */, Arrays.asList(ipo));
+        ByteBuffer iapd = pd.build();
+        mPacketReader.sendResponse(buildDhcp6Advertise(packet, iapd.array(), mClientMac,
+                (Inet6Address) mClientIpAddress));
+
+        packet = getNextDhcp6Packet(PACKET_TIMEOUT_MS);
+        assertTrue(packet instanceof Dhcp6RequestPacket);
+
+        // Reply for Request with NoPrefixAvail status code. Not sure if this is reasonable in
+        // practice, but Server can do everything it wants.
+        pd = new PrefixDelegation(packet.getIaId(), 0 /* t1 */, 0 /* t2 */,
+                new ArrayList<IaPrefixOption>() /* ipos */, Dhcp6Packet.STATUS_NO_PREFIX_AVAIL);
+        iapd = pd.build();
+        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
+                (Inet6Address) mClientIpAddress, false /* rapidCommit */));
+
+        // Check if client will ignore Reply for Request with NoPrefixAvail status code, and
+        // rollback to SolicitState.
+        packet = getNextDhcp6Packet(PACKET_TIMEOUT_MS);
+        assertTrue(packet instanceof Dhcp6SolicitPacket);
+    }
+
+    @Test
+    @SignatureRequiredTest(reason = "Need to mock the DHCP6 renew/rebind alarms")
+    public void testDhcp6ReplyForRenewWithNoPrefixAvailStatusCode() 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 normal IA_PD.
+        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
+        final IaPrefixOption ipo = buildIaPrefixOption(prefix, 4500 /* preferred */,
+                7200 /* valid */);
+        PrefixDelegation pd = new PrefixDelegation(packet.getIaId(), 1000 /* t1 */,
+                2000 /* t2 */, Arrays.asList(ipo));
+        ByteBuffer iapd = pd.build();
+        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
+                (Inet6Address) mClientIpAddress, false /* rapidCommit */));
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        // Trigger another Renew message.
+        handler.post(() -> renewAlarm.onAlarm());
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        packet = getNextDhcp6Packet();
+        assertTrue(packet instanceof Dhcp6RenewPacket);
+
+        // Reply for Renew with NoPrefixAvail status code, check if client will retransmit the
+        // Renew message.
+        pd = new PrefixDelegation(packet.getIaId(), 3600 /* t1 */, 4500 /* t2 */,
+                new ArrayList<IaPrefixOption>(0) /* ipos */, Dhcp6Packet.STATUS_NO_PREFIX_AVAIL);
+        iapd = pd.build();
+        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
+                (Inet6Address) mClientIpAddress, false /* rapidCommit */));
+
+        packet = getNextDhcp6Packet(TEST_WAIT_RENEW_REBIND_RETRANSMIT_MS);
+        assertTrue(packet instanceof Dhcp6RenewPacket);
+    }
+
+    @Test
+    @SignatureRequiredTest(reason = "Need to mock the DHCP6 renew/rebind alarms")
+    public void testDhcp6ReplyForRebindWithNoPrefixAvailStatusCode() throws Exception {
+        prepareDhcp6PdRenewTest();
+
+        final InOrder inOrder = inOrder(mAlarm);
+        final Handler handler = mDependencies.mDhcp6Client.getHandler();
+        final OnAlarmListener renewAlarm = expectAlarmSet(inOrder, "RENEW", 3600, handler);
+        final OnAlarmListener rebindAlarm = expectAlarmSet(inOrder, "REBIND", 4500, handler);
+
+        handler.post(() -> renewAlarm.onAlarm());
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        Dhcp6Packet packet = getNextDhcp6Packet();
+        assertTrue(packet instanceof Dhcp6RenewPacket);
+
+        handler.post(() -> rebindAlarm.onAlarm());
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        packet = getNextDhcp6Packet();
+        assertTrue(packet instanceof Dhcp6RebindPacket);
+
+        // Reply with normal IA_PD.
+        final IpPrefix prefix = new IpPrefix("2001:db8:1::/64");
+        final IaPrefixOption ipo = buildIaPrefixOption(prefix, 4500 /* preferred */,
+                7200 /* valid */);
+        PrefixDelegation pd = new PrefixDelegation(packet.getIaId(), 1000 /* t1 */,
+                2000 /* t2 */, Arrays.asList(ipo));
+        ByteBuffer iapd = pd.build();
+        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
+                (Inet6Address) mClientIpAddress, false /* rapidCommit */));
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        // Trigger another Rebind message.
+        handler.post(() -> renewAlarm.onAlarm());
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        packet = getNextDhcp6Packet();
+        assertTrue(packet instanceof Dhcp6RenewPacket);
+
+        handler.post(() -> rebindAlarm.onAlarm());
+        HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS);
+
+        packet = getNextDhcp6Packet();
+        assertTrue(packet instanceof Dhcp6RebindPacket);
+
+        // Reply for Rebind with NoPrefixAvail status code, check if client will retransmit the
+        // Rebind message.
+        pd = new PrefixDelegation(packet.getIaId(), 3600 /* t1 */,
+                4500 /* t2 */, new ArrayList<IaPrefixOption>(0) /* ipos */,
+                Dhcp6Packet.STATUS_NO_PREFIX_AVAIL);
+        iapd = pd.build();
+        mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac,
+                (Inet6Address) mClientIpAddress, false /* rapidCommit */));
+
+        packet = getNextDhcp6Packet(TEST_WAIT_RENEW_REBIND_RETRANSMIT_MS);
+        assertTrue(packet instanceof Dhcp6RebindPacket);
+    }
+
+    @Test
     @SignatureRequiredTest(reason = "InterfaceParams.getByName requires CAP_NET_ADMIN")
     public void testSendRtmDelAddressMethod() throws Exception {
         ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 464e4a1..ea29714 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -21,7 +21,10 @@
 java_defaults {
     name: "NetworkStackTestsDefaults",
     platform_apis: true,
-    srcs: ["src/**/*.java", "src/**/*.kt"],
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
     resource_dirs: ["res"],
     static_libs: [
         "androidx.test.ext.junit",
@@ -39,7 +42,7 @@
     ],
     defaults: [
         "framework-connectivity-test-defaults",
-        "libnetworkstackutilsjni_deps"
+        "libnetworkstackutilsjni_deps",
     ],
     jni_libs: [
         // For mockito extended
@@ -66,6 +69,9 @@
     static_libs: ["NetworkStackApiCurrentLib"],
     compile_multilib: "both", // Workaround for b/147785146 for mainline-presubmit
     jarjar_rules: ":NetworkStackJarJarRules",
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Library containing the unit tests. This is used by the coverage test target to pull in the
@@ -76,18 +82,24 @@
     min_sdk_version: "30",
     defaults: ["NetworkStackTestsDefaults"],
     static_libs: ["NetworkStackApiStableLib"],
-    lint: { test: true },
+    lint: {
+        test: true,
+        baseline_filename: "lint-baseline.xml",
+    },
     visibility: [
         "//packages/modules/NetworkStack/tests/integration",
         "//packages/modules/Connectivity/tests:__subpackages__",
         "//packages/modules/Connectivity/Tethering/tests:__subpackages__",
-    ]
+    ],
 }
 
 android_test {
     name: "NetworkStackTests",
     min_sdk_version: "30",
-    test_suites: ["general-tests", "mts"],
+    test_suites: [
+        "general-tests",
+        "mts",
+    ],
     defaults: [
         "NetworkStackTestsDefaults",
         "connectivity-mainline-presubmit-java-defaults",
@@ -95,6 +107,9 @@
     static_libs: ["NetworkStackApiStableLib"],
     compile_multilib: "both",
     jarjar_rules: ":NetworkStackJarJarRules",
+    lint: {
+        baseline_filename: "lint-baseline.xml",
+    },
 }
 
 // Additional dependencies of libnetworkstackutilsjni that are not provided by the system when
diff --git a/tests/unit/src/android/net/apf/ApfTest.java b/tests/unit/src/android/net/apf/ApfTest.java
index 0b61e04..4e1187b 100644
--- a/tests/unit/src/android/net/apf/ApfTest.java
+++ b/tests/unit/src/android/net/apf/ApfTest.java
@@ -16,6 +16,7 @@
 
 package android.net.apf;
 
+import static android.net.apf.ApfGenerator.APF_VERSION_4;
 import static android.net.apf.ApfGenerator.Register.R0;
 import static android.net.apf.ApfGenerator.Register.R1;
 import static android.net.apf.ApfJniUtils.compareBpfApf;
@@ -631,7 +632,7 @@
         // Test jump if bytes not equal.
         gen = new ApfGenerator(MIN_APF_VERSION);
         gen.addLoadImmediate(R0, 1);
-        gen.addJumpIfBytesNotEqual(R0, new byte[]{123}, gen.DROP_LABEL);
+        gen.addJumpIfBytesAtR0NotEqual(new byte[]{123}, gen.DROP_LABEL);
         program = gen.generate();
         assertEquals(6, program.length);
         assertEquals((13 << 3) | (1 << 1) | 0, program[0]);
@@ -643,20 +644,20 @@
         assertDrop(program, new byte[MIN_PKT_SIZE], 0);
         gen = new ApfGenerator(MIN_APF_VERSION);
         gen.addLoadImmediate(R0, 1);
-        gen.addJumpIfBytesNotEqual(R0, new byte[]{123}, gen.DROP_LABEL);
+        gen.addJumpIfBytesAtR0NotEqual(new byte[]{123}, gen.DROP_LABEL);
         byte[] packet123 = {0,123,0,0,0,0,0,0,0,0,0,0,0,0,0};
         assertPass(gen, packet123, 0);
         gen = new ApfGenerator(MIN_APF_VERSION);
-        gen.addJumpIfBytesNotEqual(R0, new byte[]{123}, gen.DROP_LABEL);
+        gen.addJumpIfBytesAtR0NotEqual(new byte[]{123}, gen.DROP_LABEL);
         assertDrop(gen, packet123, 0);
         gen = new ApfGenerator(MIN_APF_VERSION);
         gen.addLoadImmediate(R0, 1);
-        gen.addJumpIfBytesNotEqual(R0, new byte[]{1,2,30,4,5}, gen.DROP_LABEL);
+        gen.addJumpIfBytesAtR0NotEqual(new byte[]{1, 2, 30, 4, 5}, gen.DROP_LABEL);
         byte[] packet12345 = {0,1,2,3,4,5,0,0,0,0,0,0,0,0,0};
         assertDrop(gen, packet12345, 0);
         gen = new ApfGenerator(MIN_APF_VERSION);
         gen.addLoadImmediate(R0, 1);
-        gen.addJumpIfBytesNotEqual(R0, new byte[]{1,2,3,4,5}, gen.DROP_LABEL);
+        gen.addJumpIfBytesAtR0NotEqual(new byte[]{1, 2, 3, 4, 5}, gen.DROP_LABEL);
         assertPass(gen, packet12345, 0);
     }
 
@@ -747,23 +748,23 @@
         ApfGenerator gen;
 
         // Load data with no offset: lddw R0, [0 + r1]
-        gen = new ApfGenerator(3);
+        gen = new ApfGenerator(APF_VERSION_4);
         gen.addLoadData(R0, 0);
         assertProgramEquals(new byte[]{LDDW_OP | SIZE0}, gen.generate());
 
         // Store data with 8bit negative offset: lddw r0, [-42 + r1]
-        gen = new ApfGenerator(3);
+        gen = new ApfGenerator(APF_VERSION_4);
         gen.addStoreData(R0, -42);
         assertProgramEquals(new byte[]{STDW_OP | SIZE8, -42}, gen.generate());
 
         // Store data to R1 with 16bit negative offset: stdw r1, [-0x1122 + r0]
-        gen = new ApfGenerator(3);
+        gen = new ApfGenerator(APF_VERSION_4);
         gen.addStoreData(R1, -0x1122);
         assertProgramEquals(new byte[]{STDW_OP | SIZE16 | R1_REG, (byte)0xEE, (byte)0xDE},
                 gen.generate());
 
         // Load data to R1 with 32bit negative offset: lddw r1, [0xDEADBEEF + r0]
-        gen = new ApfGenerator(3);
+        gen = new ApfGenerator(APF_VERSION_4);
         gen.addLoadData(R1, 0xDEADBEEF);
         assertProgramEquals(
                 new byte[]{LDDW_OP | SIZE32 | R1_REG,
@@ -781,12 +782,12 @@
         byte[] expected_data = data.clone();
 
         // No memory access instructions: should leave the data segment untouched.
-        ApfGenerator gen = new ApfGenerator(3);
+        ApfGenerator gen = new ApfGenerator(APF_VERSION_4);
         assertDataMemoryContents(PASS, gen.generate(), packet, data, expected_data);
 
         // Expect value 0x87654321 to be stored starting from address -11 from the end of the
         // data buffer, in big-endian order.
-        gen = new ApfGenerator(3);
+        gen = new ApfGenerator(APF_VERSION_4);
         gen.addLoadImmediate(R0, 0x87654321);
         gen.addLoadImmediate(R1, -5);
         gen.addStoreData(R0, -6);  // -5 + -6 = -11 (offset +5 with data_len=16)
@@ -803,7 +804,7 @@
     @Test
     public void testApfDataRead() throws IllegalInstructionException, Exception {
         // Program that DROPs if address 10 (-6) contains 0x87654321.
-        ApfGenerator gen = new ApfGenerator(3);
+        ApfGenerator gen = new ApfGenerator(APF_VERSION_4);
         gen.addLoadImmediate(R1, 1000);
         gen.addLoadData(R0, -1006);  // 1000 + -1006 = -6 (offset +10 with data_len=16)
         gen.addJumpIfR0Equals(0x87654321, gen.DROP_LABEL);
@@ -832,7 +833,7 @@
      */
     @Test
     public void testApfDataReadModifyWrite() throws IllegalInstructionException, Exception {
-        ApfGenerator gen = new ApfGenerator(3);
+        ApfGenerator gen = new ApfGenerator(APF_VERSION_4);
         gen.addLoadImmediate(R1, -22);
         gen.addLoadData(R0, 0);  // Load from address 32 -22 + 0 = 10
         gen.addAdd(0x78453412);  // 87654321 + 78453412 = FFAA7733
@@ -859,35 +860,35 @@
         byte[] expected_data = data;
 
         // Program that DROPs unconditionally. This is our the baseline.
-        ApfGenerator gen = new ApfGenerator(3);
+        ApfGenerator gen = new ApfGenerator(APF_VERSION_4);
         gen.addLoadImmediate(R0, 3);
         gen.addLoadData(R1, 7);
         gen.addJump(gen.DROP_LABEL);
         assertDataMemoryContents(DROP, gen.generate(), packet, data, expected_data);
 
         // Same program as before, but this time we're trying to load past the end of the data.
-        gen = new ApfGenerator(3);
+        gen = new ApfGenerator(APF_VERSION_4);
         gen.addLoadImmediate(R0, 20);
         gen.addLoadData(R1, 15);  // 20 + 15 > 32
         gen.addJump(gen.DROP_LABEL);  // Not reached.
         assertDataMemoryContents(PASS, gen.generate(), packet, data, expected_data);
 
         // Subtracting an immediate should work...
-        gen = new ApfGenerator(3);
+        gen = new ApfGenerator(APF_VERSION_4);
         gen.addLoadImmediate(R0, 20);
         gen.addLoadData(R1, -4);
         gen.addJump(gen.DROP_LABEL);
         assertDataMemoryContents(DROP, gen.generate(), packet, data, expected_data);
 
         // ...and underflowing simply wraps around to the end of the buffer...
-        gen = new ApfGenerator(3);
+        gen = new ApfGenerator(APF_VERSION_4);
         gen.addLoadImmediate(R0, 20);
         gen.addLoadData(R1, -30);
         gen.addJump(gen.DROP_LABEL);
         assertDataMemoryContents(DROP, gen.generate(), packet, data, expected_data);
 
         // ...but doesn't allow accesses before the start of the buffer
-        gen = new ApfGenerator(3);
+        gen = new ApfGenerator(APF_VERSION_4);
         gen.addLoadImmediate(R0, 20);
         gen.addLoadData(R1, -1000);
         gen.addJump(gen.DROP_LABEL);  // Not reached.
diff --git a/tests/unit/src/android/net/apf/ApfV5Test.kt b/tests/unit/src/android/net/apf/ApfV5Test.kt
index 1b74c3e..162feef 100644
--- a/tests/unit/src/android/net/apf/ApfV5Test.kt
+++ b/tests/unit/src/android/net/apf/ApfV5Test.kt
@@ -16,6 +16,8 @@
 package android.net.apf
 
 import android.net.apf.ApfGenerator.IllegalInstructionException
+import android.net.apf.ApfGenerator.Register.R0
+import android.net.apf.ApfGenerator.Register.R1
 import androidx.test.filters.SmallTest
 import androidx.test.runner.AndroidJUnit4
 import java.lang.IllegalArgumentException
@@ -39,13 +41,130 @@
         assertFailsWith<IllegalInstructionException> { gen.addCountAndPass(1000) }
         assertFailsWith<IllegalInstructionException> { gen.addTransmit() }
         assertFailsWith<IllegalInstructionException> { gen.addDiscard() }
+        assertFailsWith<IllegalInstructionException> { gen.addAllocateR0() }
+        assertFailsWith<IllegalInstructionException> { gen.addAllocate(100) }
+        assertFailsWith<IllegalInstructionException> { gen.addData(ByteArray(3) { 0x01 }) }
+        assertFailsWith<IllegalInstructionException> { gen.addWriteU8(100) }
+        assertFailsWith<IllegalInstructionException> { gen.addWriteU16(100) }
+        assertFailsWith<IllegalInstructionException> { gen.addWriteU32(100) }
+        assertFailsWith<IllegalInstructionException> { gen.addPacketCopy(100, 100) }
+        assertFailsWith<IllegalInstructionException> { gen.addDataCopy(100, 100) }
+        assertFailsWith<IllegalInstructionException> { gen.addWriteU8(R0) }
+        assertFailsWith<IllegalInstructionException> { gen.addWriteU16(R0) }
+        assertFailsWith<IllegalInstructionException> { gen.addWriteU32(R0) }
+        assertFailsWith<IllegalInstructionException> { gen.addWriteU8(R1) }
+        assertFailsWith<IllegalInstructionException> { gen.addWriteU16(R1) }
+        assertFailsWith<IllegalInstructionException> { gen.addWriteU32(R1) }
+        assertFailsWith<IllegalInstructionException> { gen.addPacketCopyFromR0LenR1() }
+        assertFailsWith<IllegalInstructionException> { gen.addDataCopyFromR0LenR1() }
+        assertFailsWith<IllegalInstructionException> { gen.addPacketCopyFromR0(10) }
+        assertFailsWith<IllegalInstructionException> { gen.addDataCopyFromR0(10) }
+        assertFailsWith<IllegalInstructionException> {
+            gen.addJumpIfBytesAtR0Equal(byteArrayOf('A'.code.toByte()), ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalInstructionException> { gen.addJumpIfPktAtR0DoesNotContainDnsQ(
+                byteArrayOf(1, 'A'.code.toByte()), 0x0c, ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalInstructionException> { gen.addJumpIfPktAtR0ContainDnsQ(
+                byteArrayOf(1, 'A'.code.toByte()), 0x0c, ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalInstructionException> { gen.addJumpIfPktAtR0DoesNotContainDnsA(
+                byteArrayOf(1, 'A'.code.toByte()), ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalInstructionException> { gen.addJumpIfPktAtR0ContainDnsA(
+                byteArrayOf(1, 'A'.code.toByte()), ApfGenerator.DROP_LABEL) }
     }
 
     @Test
-    fun testApfInstructionArgumentCheck() {
+    fun testDataInstructionMustComeFirst() {
         var gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
-        assertFailsWith<IllegalArgumentException> { gen.addCountAndPass(0) }
-        assertFailsWith<IllegalArgumentException> { gen.addCountAndDrop(0) }
+        gen.addAllocateR0()
+        assertFailsWith<IllegalInstructionException> { gen.addData(ByteArray(3) { 0x01 }) }
+    }
+
+    @Test
+    fun testApfInstructionEncodingSizeCheck() {
+        var gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
+        assertFailsWith<IllegalArgumentException> { gen.addAllocate(65536) }
+        assertFailsWith<IllegalArgumentException> { gen.addAllocate(-1) }
+        assertFailsWith<IllegalArgumentException> { gen.addDataCopy(-1, 1) }
+        assertFailsWith<IllegalArgumentException> { gen.addPacketCopy(-1, 1) }
+        assertFailsWith<IllegalArgumentException> { gen.addDataCopy(1, 256) }
+        assertFailsWith<IllegalArgumentException> { gen.addPacketCopy(1, 256) }
+        assertFailsWith<IllegalArgumentException> { gen.addDataCopy(1, -1) }
+        assertFailsWith<IllegalArgumentException> { gen.addPacketCopy(1, -1) }
+        assertFailsWith<IllegalArgumentException> { gen.addPacketCopyFromR0(256) }
+        assertFailsWith<IllegalArgumentException> { gen.addDataCopyFromR0(256) }
+        assertFailsWith<IllegalArgumentException> { gen.addPacketCopyFromR0(-1) }
+        assertFailsWith<IllegalArgumentException> { gen.addDataCopyFromR0(-1) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsQ(
+                byteArrayOf(1, 'A'.code.toByte(), 0, 0), 256, ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsQ(
+                byteArrayOf(1, 'a'.code.toByte(), 0, 0), 0x0c, ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsQ(
+                byteArrayOf(1, '.'.code.toByte(), 0, 0), 0x0c, ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsQ(
+                byteArrayOf(0, 0), 0xc0, ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsQ(
+                byteArrayOf(1, 'A'.code.toByte()), 0xc0, ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsQ(
+                byteArrayOf(64) + ByteArray(64) { 'A'.code.toByte() } + byteArrayOf(0, 0),
+                0xc0, ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsQ(
+                byteArrayOf(1, 'A'.code.toByte(), 1, 'B'.code.toByte(), 0),
+                0xc0, ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsQ(
+                byteArrayOf(1, 'A'.code.toByte(), 1, 'B'.code.toByte()),
+                0xc0, ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsQ(
+                byteArrayOf(1, 'A'.code.toByte(), 0, 0), 256, ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsQ(
+                byteArrayOf(1, 'a'.code.toByte(), 0, 0), 0x0c, ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsQ(
+                byteArrayOf(1, '.'.code.toByte(), 0, 0), 0x0c, ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsQ(
+                byteArrayOf(0, 0), 0xc0, ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsQ(
+                byteArrayOf(1, 'A'.code.toByte()), 0xc0, ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsQ(
+                byteArrayOf(64) + ByteArray(64) { 'A'.code.toByte() } + byteArrayOf(0, 0),
+                0xc0, ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsQ(
+                byteArrayOf(1, 'A'.code.toByte(), 1, 'B'.code.toByte(), 0),
+                0xc0, ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsQ(
+                byteArrayOf(1, 'A'.code.toByte(), 1, 'B'.code.toByte()),
+                0xc0, ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsA(
+                byteArrayOf(1, 'a'.code.toByte(), 0, 0), ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsA(
+                byteArrayOf(1, '.'.code.toByte(), 0, 0), ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsA(
+                byteArrayOf(0, 0), ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsA(
+                byteArrayOf(1, 'A'.code.toByte()), ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsA(
+                byteArrayOf(64) + ByteArray(64) { 'A'.code.toByte() } + byteArrayOf(0, 0),
+                 ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsA(
+                byteArrayOf(1, 'A'.code.toByte(), 1, 'B'.code.toByte(), 0),
+                ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsA(
+                byteArrayOf(1, 'A'.code.toByte(), 1, 'B'.code.toByte()),
+                ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsA(
+                byteArrayOf(1, 'a'.code.toByte(), 0, 0), ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsA(
+                byteArrayOf(1, '.'.code.toByte(), 0, 0), ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsA(
+                byteArrayOf(0, 0), ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsA(
+                byteArrayOf(1, 'A'.code.toByte()), ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsA(
+                byteArrayOf(64) + ByteArray(64) { 'A'.code.toByte() } + byteArrayOf(0, 0),
+                ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsA(
+                byteArrayOf(1, 'A'.code.toByte(), 1, 'B'.code.toByte(), 0),
+                ApfGenerator.DROP_LABEL) }
+        assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsA(
+                byteArrayOf(1, 'A'.code.toByte(), 1, 'B'.code.toByte()),
+                ApfGenerator.DROP_LABEL) }
     }
 
     @Test
@@ -81,10 +200,18 @@
                         0x03, 0xe8.toByte()), program)
 
         gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
-        gen.addAlloc(ApfGenerator.Register.R0)
+        gen.addAllocateR0()
+        gen.addAllocate(1500)
         program = gen.generate()
-        assertContentEquals(byteArrayOf(encodeInstruction(21, 1, 0), 36), program)
-        assertContentEquals(arrayOf("       0: alloc r0"), ApfJniUtils.disassembleApf(program))
+        // encoding ALLOC opcode: opcode=21(EXT opcode number), imm=36(TRANS opcode number).
+        // R=0 means length stored in R0. R=1 means the length stored in imm1.
+        assertContentEquals(byteArrayOf(
+                encodeInstruction(opcode = 21, immLength = 1, register = 0), 36,
+                encodeInstruction(opcode = 21, immLength = 1, register = 1), 36, 0x05,
+                0xDC.toByte()),
+        program)
+        // TODO: add back disassembling test check after we update the apf_disassembler
+        // assertContentEquals(arrayOf("       0: alloc"), ApfJniUtils.disassembleApf(program))
 
         gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
         gen.addTransmit()
@@ -100,63 +227,134 @@
         // TODO: add back disassembling test check after we update the apf_disassembler
         // assertContentEquals(arrayOf("       0: trans"), ApfJniUtils.disassembleApf(program))
 
-        // TODO: add back when support write opcode
-//        gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
-//        gen.addWrite(0x01, 1)
-//        gen.addWrite(0x0102, 2)
-//        gen.addWrite(0x01020304, 4)
-//        program = gen.generate()
-//        assertContentEquals(byteArrayOf(
-//                encodeInstruction(24, 1, 0), 0x01,
-//                encodeInstruction(24, 2, 0), 0x01, 0x02,
-//                encodeInstruction(24, 4, 0), 0x01, 0x02, 0x03, 0x04
-//        ), program)
-//        assertContentEquals(arrayOf(
-//                "       0: write 0x01",
-//                "       2: write 0x0102",
-//                "       5: write 0x01020304"), ApfJniUtils.disassembleApf(program))
-//
-//        gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
-//        gen.addWrite(ApfGenerator.Register.R0, 1)
-//        gen.addWrite(ApfGenerator.Register.R0, 2)
-//        gen.addWrite(ApfGenerator.Register.R0, 4)
-//        program = gen.generate()
-//        assertContentEquals(byteArrayOf(
-//                encodeInstruction(21, 1, 0), 38,
-//                encodeInstruction(21, 1, 0), 39,
-//                encodeInstruction(21, 1, 0), 40
-//        ), program)
-//        assertContentEquals(arrayOf(
-//                "       0: write r0, 1",
-//                "       2: write r0, 2",
-//                "       4: write r0, 4"), ApfJniUtils.disassembleApf(program))
+        gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
+        val largeByteArray = ByteArray(256) { 0x01 }
+        gen.addData(largeByteArray)
+        program = gen.generate()
+        // encoding DATA opcode: opcode=14(JMP), R=1
+        assertContentEquals(byteArrayOf(
+                encodeInstruction(opcode = 14, immLength = 2, register = 1), 0x01, 0x00) +
+                largeByteArray, program)
 
-        // TODO: add back when we properly support copy opcode
-//        gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
-//        gen.addDataCopy(1, 5)
-//        gen.addPacketCopy(1000, 255)
-//        program = gen.generate()
-//        assertContentEquals(byteArrayOf(
-//                encodeInstruction(25, 1, 1), 1, 5,
-//                encodeInstruction(25, 2, 0),
-//                0x03.toByte(), 0xe8.toByte(), 0xff.toByte(),
-//        ), program)
+        gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
+        gen.addWriteU8(0x01)
+        gen.addWriteU16(0x0102)
+        gen.addWriteU32(0x01020304)
+        gen.addWriteU8(0x00)
+        gen.addWriteU8(0x80)
+        gen.addWriteU16(0x0000)
+        gen.addWriteU16(0x8000)
+        gen.addWriteU32(0x00000000)
+        gen.addWriteU32(0x80000000)
+        program = gen.generate()
+        assertContentEquals(byteArrayOf(
+                encodeInstruction(24, 1, 0), 0x01,
+                encodeInstruction(24, 2, 0), 0x01, 0x02,
+                encodeInstruction(24, 4, 0), 0x01, 0x02, 0x03, 0x04,
+                encodeInstruction(24, 1, 0), 0x00,
+                encodeInstruction(24, 1, 0), 0x80.toByte(),
+                encodeInstruction(24, 2, 0), 0x00, 0x00,
+                encodeInstruction(24, 2, 0), 0x80.toByte(), 0x00,
+                encodeInstruction(24, 4, 0), 0x00, 0x00, 0x00, 0x00,
+                encodeInstruction(24, 4, 0), 0x80.toByte(), 0x00, 0x00,
+                0x00), program)
+        assertContentEquals(arrayOf(
+                "       0: write 0x01",
+                "       2: write 0x0102",
+                "       5: write 0x01020304",
+                "      10: write 0x00",
+                "      12: write 0x80",
+                "      14: write 0x0000",
+                "      17: write 0x8000",
+                "      20: write 0x00000000",
+                "      25: write 0x80000000"),
+        ApfJniUtils.disassembleApf(program))
+
+        gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
+        gen.addWriteU8(R0)
+        gen.addWriteU16(R0)
+        gen.addWriteU32(R0)
+        gen.addWriteU8(R1)
+        gen.addWriteU16(R1)
+        gen.addWriteU32(R1)
+        program = gen.generate()
+        assertContentEquals(byteArrayOf(
+                encodeInstruction(21, 1, 0), 38,
+                encodeInstruction(21, 1, 0), 39,
+                encodeInstruction(21, 1, 0), 40,
+                encodeInstruction(21, 1, 1), 38,
+                encodeInstruction(21, 1, 1), 39,
+                encodeInstruction(21, 1, 1), 40
+        ), program)
+        // TODO: add back disassembling test check after we update the apf_disassembler
 //        assertContentEquals(arrayOf(
-//                "       0: dcopy 1, 5",
+//                "       0: ewrite1 r0",
+//                "       2: ewrite2 r0",
+//                "       4: ewrite4 r0",
+//                "       6: ewrite1 r1",
+//                "       8: ewrite2 r1",
+//                "      10: ewrite4 r1"), ApfJniUtils.disassembleApf(program))
+
+        gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
+        gen.addDataCopy(0, 10)
+        gen.addDataCopy(1, 5)
+        gen.addPacketCopy(1000, 255)
+        program = gen.generate()
+        assertContentEquals(byteArrayOf(
+                encodeInstruction(25, 0, 1), 10,
+                encodeInstruction(25, 1, 1), 1, 5,
+                encodeInstruction(25, 2, 0),
+                0x03.toByte(), 0xe8.toByte(), 0xff.toByte(),
+        ), program)
+        // TODO: add back disassembling test check after we update the apf_disassembler
+//        assertContentEquals(arrayOf(
+//                "       0: dcopy 0, 5",
 //                "       3: pcopy 1000, 255"), ApfJniUtils.disassembleApf(program))
-//
-//        gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
-//        gen.addDataCopy(ApfGenerator.Register.R1, 0, 5)
-//        gen.addPacketCopy(ApfGenerator.Register.R0, 1000, 255)
-//        program = gen.generate()
-//        assertContentEquals(byteArrayOf(
-//                encodeInstruction(21, 1, 1), 42, 0, 5,
-//                encodeInstruction(21, 2, 0),
-//                0, 41, 0x03.toByte(), 0xe8.toByte(), 0xff.toByte()
-//        ), program)
+
+        gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
+        gen.addPacketCopyFromR0LenR1()
+        gen.addPacketCopyFromR0(5)
+        gen.addDataCopyFromR0LenR1()
+        gen.addDataCopyFromR0(5)
+        program = gen.generate()
+        assertContentEquals(byteArrayOf(
+                encodeInstruction(21, 1, 1), 41,
+                encodeInstruction(21, 1, 0), 41, 5,
+                encodeInstruction(21, 1, 1), 42,
+                encodeInstruction(21, 1, 0), 42, 5,
+        ), program)
+        // TODO: add back the following test case when implementing EPKTCOPY, EDATACOPY opcodes.
 //        assertContentEquals(arrayOf(
 //                "       0: dcopy [r1+0], 5",
 //                "       4: pcopy [r0+1000], 255"), ApfJniUtils.disassembleApf(program))
+
+        gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
+        gen.addJumpIfBytesAtR0Equal(byteArrayOf('a'.code.toByte()), ApfGenerator.DROP_LABEL)
+        program = gen.generate()
+        assertContentEquals(
+                byteArrayOf(encodeInstruction(opcode = 20, immLength = 1, register = 1),
+                        1, 1, 'a'.code.toByte()), program)
+
+        val qnames = byteArrayOf(1, 'A'.code.toByte(), 1, 'B'.code.toByte(), 0, 0)
+        gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
+        gen.addJumpIfPktAtR0DoesNotContainDnsQ(qnames, 0x0c, ApfGenerator.DROP_LABEL)
+        gen.addJumpIfPktAtR0ContainDnsQ(qnames, 0x0c, ApfGenerator.DROP_LABEL)
+        program = gen.generate()
+        assertContentEquals(byteArrayOf(
+                encodeInstruction(21, 1, 0), 43, 11, 0x0c.toByte(),
+        ) + qnames + byteArrayOf(
+                encodeInstruction(21, 1, 1), 43, 1, 0x0c.toByte(),
+        ) + qnames, program)
+
+        gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV)
+        gen.addJumpIfPktAtR0DoesNotContainDnsA(qnames, ApfGenerator.DROP_LABEL)
+        gen.addJumpIfPktAtR0ContainDnsA(qnames, ApfGenerator.DROP_LABEL)
+        program = gen.generate()
+        assertContentEquals(byteArrayOf(
+                encodeInstruction(21, 1, 0), 44, 10,
+        ) + qnames + byteArrayOf(
+                encodeInstruction(21, 1, 1), 44, 1,
+        ) + qnames, program)
     }
 
     private fun encodeInstruction(opcode: Int, immLength: Int, register: Int): Byte {
diff --git a/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt b/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt
index b5e806f..32cf464 100644
--- a/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt
+++ b/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt
@@ -220,4 +220,168 @@
         assertEquals(423, packet.prefixDelegation.minimalPreferredLifetime)
         assertEquals(43200, packet.prefixDelegation.minimalValidLifetime)
     }
+
+    @Test
+    fun testStatusCodeOptionWithStatusMessage() {
+        val replyHex =
+            // Reply, Transaction ID
+            "07000A47" +
+            // server identifier option(option_len=10)
+            "0002000A0003000186C9B26AED4D" +
+            // client identifier option(option_len=12)
+            "0001000C0003001B02FBBAFFFEB7BC71" +
+            // SOL_MAX_RT (don't support this option yet)
+            "005200040000003c" +
+            // Rapid Commit
+            "000e0000" +
+            // DNS recursive server (don't support this opton yet)
+            "00170010fdfd9ed6795000000000000000000001" +
+            // IA_PD option (t1=t2=0, empty prefix)
+            "0019000c000000000000000000000000" +
+            // Status code option: status code=NoPrefixAvail
+            "000d00150006" +
+            // Status code option: status message="no prefix available"
+            "6e6f2070726566697820617661696c61626c65"
+        val bytes = HexDump.hexStringToByteArray(replyHex)
+        val packet = Dhcp6Packet.decode(bytes, bytes.size)
+        assertTrue(packet is Dhcp6ReplyPacket)
+        assertEquals(0, packet.mPrefixDelegation.iaid)
+        assertEquals(0, packet.mPrefixDelegation.t1)
+        assertEquals(0, packet.mPrefixDelegation.t2)
+        assertEquals(Dhcp6Packet.STATUS_NO_PREFIX_AVAIL, packet.mStatusCode)
+    }
+
+    @Test
+    fun testStatusCodeOptionWithoutStatusMessage() {
+        val replyHex =
+            // Reply, Transaction ID
+            "07000A47" +
+            // server identifier option(option_len=10)
+            "0002000A0003000186C9B26AED4D" +
+            // client identifier option(option_len=12)
+            "0001000C0003001B02FBBAFFFEB7BC71" +
+            // SOL_MAX_RT (don't support this option yet)
+            "005200040000003c" +
+            // Rapid Commit
+            "000e0000" +
+            // DNS recursive server (don't support this opton yet)
+            "00170010fdfd9ed6795000000000000000000001" +
+            // IA_PD option (t1=t2=0, empty prefix)
+            "0019000c000000000000000000000000" +
+            // Status code option: status code=NoPrefixAvail
+            "000d00020006"
+        val bytes = HexDump.hexStringToByteArray(replyHex)
+        val packet = Dhcp6Packet.decode(bytes, bytes.size)
+        assertTrue(packet is Dhcp6ReplyPacket)
+        assertEquals(0, packet.mPrefixDelegation.iaid)
+        assertEquals(0, packet.mPrefixDelegation.t1)
+        assertEquals(0, packet.mPrefixDelegation.t2)
+        assertEquals(Dhcp6Packet.STATUS_NO_PREFIX_AVAIL, packet.mStatusCode)
+    }
+
+    @Test
+    fun testStatusCodeOptionInIaPdWithStatusMessage() {
+        val replyHex =
+            // Reply, Transaction ID
+            "07000A47" +
+            // server identifier option(option_len=10)
+            "0002000A0003000186C9B26AED4D" +
+            // client identifier option(option_len=12)
+            "0001000C0003001B02FBBAFFFEB7BC71" +
+            // SOL_MAX_RT (don't support this option yet)
+            "005200040000003c" +
+            // Rapid Commit
+            "000e0000" +
+            // DNS recursive server (don't support this opton yet)
+            "00170010fdfd9ed6795000000000000000000001" +
+            // IA_PD option (t1=t2=0, status code=NoPrefixAvail,
+            // status message="no prefix available")
+            "00190025000000000000000000000000000d00150006" +
+            "6e6f2070726566697820617661696c61626c65"
+        val bytes = HexDump.hexStringToByteArray(replyHex)
+        val packet = Dhcp6Packet.decode(bytes, bytes.size)
+        assertTrue(packet is Dhcp6ReplyPacket)
+        assertEquals(0, packet.mPrefixDelegation.iaid)
+        assertEquals(0, packet.mPrefixDelegation.t1)
+        assertEquals(0, packet.mPrefixDelegation.t2)
+        assertEquals(Dhcp6Packet.STATUS_NO_PREFIX_AVAIL, packet.mPrefixDelegation.statusCode)
+    }
+
+    @Test
+    fun testStatusCodeOptionInIaPdWithoutStatusMessage() {
+        val replyHex =
+            // Reply, Transaction ID
+            "07000A47" +
+            // server identifier option(option_len=10)
+            "0002000A0003000186C9B26AED4D" +
+            // client identifier option(option_len=12)
+            "0001000C0003001B02FBBAFFFEB7BC71" +
+            // SOL_MAX_RT (don't support this option yet)
+            "005200040000003c" +
+            // Rapid Commit
+            "000e0000" +
+            // DNS recursive server (don't support this opton yet)
+            "00170010fdfd9ed6795000000000000000000001" +
+            // IA_PD option (t1=t2=0, status code=NoPrefixAvail)
+            "00190012000000000000000000000000000d00020006"
+        val bytes = HexDump.hexStringToByteArray(replyHex)
+        val packet = Dhcp6Packet.decode(bytes, bytes.size)
+        assertTrue(packet is Dhcp6ReplyPacket)
+        assertEquals(0, packet.mPrefixDelegation.iaid)
+        assertEquals(0, packet.mPrefixDelegation.t1)
+        assertEquals(0, packet.mPrefixDelegation.t2)
+        assertEquals(Dhcp6Packet.STATUS_NO_PREFIX_AVAIL, packet.mPrefixDelegation.statusCode)
+    }
+
+    @Test
+    fun testStatusCodeOptionWithTruncatedStatusMessage() {
+        val replyHex =
+            // Reply, Transaction ID
+            "07000A47" +
+            // server identifier option(option_len=10)
+            "0002000A0003000186C9B26AED4D" +
+            // client identifier option(option_len=12)
+            "0001000C0003001B02FBBAFFFEB7BC71" +
+            // SOL_MAX_RT (don't support this option yet)
+            "005200040000003c" +
+            // Rapid Commit
+            "000e0000" +
+            // DNS recursive server (don't support this opton yet)
+            "00170010fdfd9ed6795000000000000000000001" +
+            // Status code option: len=21, status code=NoPrefixAvail
+            "000d00150006" +
+            // Status code option: truncated status message="no prefix available"
+            "6e6f2070726566697820617661696c6162"
+        val bytes = HexDump.hexStringToByteArray(replyHex)
+        assertThrows(Dhcp6Packet.ParseException::class.java) {
+            Dhcp6Packet.decode(bytes, bytes.size)
+        }
+    }
+
+    @Test
+    fun testStatusCodeOptionInIaPdWithTruncatedStatusMessage() {
+        val replyHex =
+            // Reply, Transaction ID
+            "07000A47" +
+            // server identifier option(option_len=10)
+            "0002000A0003000186C9B26AED4D" +
+            // client identifier option(option_len=12)
+            "0001000C0003001B02FBBAFFFEB7BC71" +
+            // SOL_MAX_RT (don't support this option yet)
+            "005200040000003c" +
+            // Rapid Commit
+            "000e0000" +
+            // DNS recursive server (don't support this opton yet)
+            "00170010fdfd9ed6795000000000000000000001" +
+            // IA_PD option (t1=t2=0, empty prefix)
+            "00190025000000000000000000000000" +
+            // Status code option: len=21, status code=NoPrefixAvail
+            "000d00150006" +
+            // truncated status message="no prefix available")
+            "6e6f2070726566697820617661696c6162"
+        val bytes = HexDump.hexStringToByteArray(replyHex)
+        assertThrows(Dhcp6Packet.ParseException::class.java) {
+            Dhcp6Packet.decode(bytes, bytes.size)
+        }
+    }
 }
diff --git a/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt b/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt
index 6471f3a..4d57df5 100644
--- a/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt
+++ b/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt
@@ -56,7 +56,6 @@
 import com.android.net.module.util.netlink.StructNdMsg.NUD_REACHABLE
 import com.android.net.module.util.netlink.StructNdMsg.NUD_STALE
 import com.android.networkstack.metrics.IpReachabilityMonitorMetrics
-import com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION
 import com.android.testutils.makeNewNeighMessage
 import com.android.testutils.waitForIdle
 import java.io.FileDescriptor
@@ -259,8 +258,6 @@
         }.`when`(dependencies).makeIpNeighborMonitor(any(), any(), any())
         doReturn(mIpReachabilityMonitorMetrics)
                 .`when`(dependencies).getIpReachabilityMonitorMetrics()
-        doReturn(true).`when`(dependencies).isFeatureNotChickenedOut(any(),
-                eq(IP_REACHABILITY_MCAST_RESOLICIT_VERSION))
 
         val monitorFuture = CompletableFuture<IpReachabilityMonitor>()
         // IpReachabilityMonitor needs to be started from the handler thread
diff --git a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
index 77e3a12..43bee55 100644
--- a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
+++ b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
@@ -19,6 +19,7 @@
 import static android.content.Intent.ACTION_CONFIGURATION_CHANGED;
 import static android.net.CaptivePortal.APP_RETURN_DISMISSED;
 import static android.net.CaptivePortal.APP_RETURN_WANTED_AS_IS;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
 import static android.net.DnsResolver.TYPE_A;
 import static android.net.DnsResolver.TYPE_AAAA;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_DNS;
@@ -29,6 +30,7 @@
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_SKIPPED;
 import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID;
+import static android.net.InetAddresses.parseNumericAddress;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
@@ -64,6 +66,7 @@
 import static com.android.networkstack.util.NetworkStackUtils.DEFAULT_CAPTIVE_PORTAL_DNS_PROBE_TIMEOUT;
 import static com.android.networkstack.util.NetworkStackUtils.DNS_PROBE_PRIVATE_IP_NO_INTERNET_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.REEVALUATE_WHEN_RESUME;
+import static com.android.server.connectivity.NetworkMonitor.CONFIG_ASYNC_PRIVDNS_PROBE_TIMEOUT_MS;
 import static com.android.server.connectivity.NetworkMonitor.INITIAL_REEVALUATE_DELAY_MS;
 import static com.android.server.connectivity.NetworkMonitor.extractCharset;
 
@@ -123,6 +126,7 @@
 import android.net.NetworkAgentConfig;
 import android.net.NetworkCapabilities;
 import android.net.NetworkTestResultParcelable;
+import android.net.PrivateDnsConfigParcel;
 import android.net.Uri;
 import android.net.captiveportal.CaptivePortalProbeResult;
 import android.net.metrics.IpConnectivityLog;
@@ -168,6 +172,7 @@
 import com.android.networkstack.metrics.DataStallDetectionStats;
 import com.android.networkstack.metrics.DataStallStatsUtils;
 import com.android.networkstack.netlink.TcpSocketTracker;
+import com.android.networkstack.util.NetworkStackUtils;
 import com.android.server.NetworkStackService.NetworkStackServiceManager;
 import com.android.server.connectivity.nano.CellularData;
 import com.android.server.connectivity.nano.DnsEvent;
@@ -214,8 +219,12 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Random;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 import javax.net.ssl.SSLHandshakeException;
 
@@ -366,11 +375,11 @@
         class DnsEntry {
             final String mHostname;
             final int mType;
-            final List<InetAddress> mAddresses;
-            DnsEntry(String host, int type, List<InetAddress> addr) {
+            final AddressSupplier mAddressesSupplier;
+            DnsEntry(String host, int type, AddressSupplier addr) {
                 mHostname = host;
                 mType = type;
-                mAddresses = addr;
+                mAddressesSupplier = addr;
             }
             // Full match or partial match that target host contains the entry hostname to support
             // random private dns probe hostname.
@@ -378,6 +387,21 @@
                 return hostname.endsWith(mHostname) && type == mType;
             }
         }
+        interface AddressSupplier {
+            List<InetAddress> get() throws DnsResolver.DnsException;
+        }
+
+        class InstantAddressSupplier implements AddressSupplier {
+            private final List<InetAddress> mAddresses;
+            InstantAddressSupplier(List<InetAddress> addresses) {
+                mAddresses = addresses;
+            }
+            @Override
+            public List<InetAddress> get() {
+                return mAddresses;
+            }
+        }
+
         private final ArrayList<DnsEntry> mAnswers = new ArrayList<DnsEntry>();
         private boolean mNonBypassPrivateDnsWorking = true;
 
@@ -387,40 +411,76 @@
         }
 
         /** Clears all DNS entries. */
-        private synchronized void clearAll() {
-            mAnswers.clear();
+        private void clearAll() {
+            synchronized (mAnswers) {
+                mAnswers.clear();
+            }
         }
 
         /** Returns the answer for a given name and type on the given mock network. */
-        private synchronized List<InetAddress> getAnswer(Object mock, String hostname, int type) {
-            if (mock == mNetwork && !mNonBypassPrivateDnsWorking) {
-                return null;
+        private CompletableFuture<List<InetAddress>> getAnswer(Network mockNetwork, String hostname,
+                int type) {
+            if (mockNetwork == mNetwork && !mNonBypassPrivateDnsWorking) {
+                return CompletableFuture.completedFuture(null);
             }
 
-            return mAnswers.stream().filter(e -> e.matches(hostname, type))
-                    .map(answer -> answer.mAddresses).findFirst().orElse(null);
+            final AddressSupplier answerSupplier;
+
+            synchronized (mAnswers) {
+                answerSupplier = mAnswers.stream()
+                        .filter(e -> e.matches(hostname, type))
+                        .map(answer -> answer.mAddressesSupplier).findFirst().orElse(null);
+            }
+            if (answerSupplier == null) {
+                return CompletableFuture.completedFuture(null);
+            }
+
+            if (answerSupplier instanceof InstantAddressSupplier) {
+                // Save latency waiting for a query thread if the answer is hardcoded.
+                return CompletableFuture.completedFuture(
+                        ((InstantAddressSupplier) answerSupplier).get());
+            }
+            final CompletableFuture<List<InetAddress>> answerFuture = new CompletableFuture<>();
+            new Thread(() -> {
+                try {
+                    answerFuture.complete(answerSupplier.get());
+                } catch (DnsResolver.DnsException e) {
+                    answerFuture.completeExceptionally(e);
+                }
+            }).start();
+            return answerFuture;
         }
 
         /** Sets the answer for a given name and type. */
-        private synchronized void setAnswer(String hostname, String[] answer, int type)
-                throws UnknownHostException {
-            DnsEntry record = new DnsEntry(hostname, type, generateAnswer(answer));
-            // Remove the existing one.
-            mAnswers.removeIf(entry -> entry.matches(hostname, type));
-            // Add or replace a new record.
-            mAnswers.add(record);
+        private void setAnswer(String hostname, String[] answer, int type) {
+            setAnswer(hostname, new InstantAddressSupplier(generateAnswer(answer)), type);
+        }
+
+        private void setAnswer(String hostname, AddressSupplier answerSupplier, int type) {
+            DnsEntry record = new DnsEntry(hostname, type, answerSupplier);
+            synchronized (mAnswers) {
+                // Remove the existing one.
+                mAnswers.removeIf(entry -> entry.matches(hostname, type));
+                // Add or replace a new record.
+                mAnswers.add(record);
+            }
         }
 
         private List<InetAddress> generateAnswer(String[] answer) {
             if (answer == null) return new ArrayList<>();
-            return Arrays.stream(answer).map(addr -> InetAddress.parseNumericAddress(addr))
-                    .collect(toList());
+            return Arrays.stream(answer).map(InetAddresses::parseNumericAddress).collect(toList());
         }
 
         /** Simulates a getAllByName call for the specified name on the specified mock network. */
-        private InetAddress[] getAllByName(Object mock, String hostname)
+        private InetAddress[] getAllByName(Network mockNetwork, String hostname)
                 throws UnknownHostException {
-            List<InetAddress> answer = queryAllTypes(mock, hostname);
+            final List<InetAddress> answer;
+            try {
+                answer = queryAllTypes(mockNetwork, hostname).get(
+                        HANDLER_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            } catch (ExecutionException | InterruptedException | TimeoutException e) {
+                throw new AssertionError("No mock DNS reply within timeout", e);
+            }
             if (answer == null || answer.size() == 0) {
                 throw new UnknownHostException(hostname);
             }
@@ -428,57 +488,76 @@
         }
 
         // Regardless of the type, depends on what the responses contained in the network.
-        private List<InetAddress> queryAllTypes(Object mock, String hostname) {
-            List<InetAddress> answer = new ArrayList<>();
-            addAllIfNotNull(answer, getAnswer(mock, hostname, TYPE_A));
-            addAllIfNotNull(answer, getAnswer(mock, hostname, TYPE_AAAA));
-            return answer;
-        }
-
-        private void addAllIfNotNull(List<InetAddress> list, List<InetAddress> c) {
-            if (c != null) {
-                list.addAll(c);
+        private CompletableFuture<List<InetAddress>> queryAllTypes(
+                Network mockNetwork, String hostname) {
+            if (mockNetwork == mNetwork && !mNonBypassPrivateDnsWorking) {
+                return CompletableFuture.completedFuture(null);
             }
+
+            final CompletableFuture<List<InetAddress>> aFuture =
+                    getAnswer(mockNetwork, hostname, TYPE_A)
+                            .exceptionally(e -> Collections.emptyList());
+            final CompletableFuture<List<InetAddress>> aaaaFuture =
+                    getAnswer(mockNetwork, hostname, TYPE_AAAA)
+                            .exceptionally(e -> Collections.emptyList());
+
+            final CompletableFuture<List<InetAddress>> combinedFuture = new CompletableFuture<>();
+            aFuture.thenAcceptBoth(aaaaFuture, (res1, res2) -> {
+                final List<InetAddress> answer = new ArrayList<>();
+                if (res1 != null) answer.addAll(res1);
+                if (res2 != null) answer.addAll(res2);
+                combinedFuture.complete(answer);
+            });
+            return combinedFuture;
         }
 
         /** Starts mocking DNS queries. */
         private void startMocking() throws UnknownHostException {
             // Queries on mNetwork using getAllByName.
             doAnswer(invocation -> {
-                return getAllByName(invocation.getMock(), invocation.getArgument(0));
+                return getAllByName((Network) invocation.getMock(), invocation.getArgument(0));
             }).when(mNetwork).getAllByName(any());
 
             // Queries on mCleartextDnsNetwork using DnsResolver#query.
             doAnswer(invocation -> {
-                return mockQuery(invocation, 1 /* posHostname */, 3 /* posExecutor */,
-                        5 /* posCallback */, -1 /* posType */);
+                return mockQuery(invocation, 0 /* posNetwork */, 1 /* posHostname */,
+                        3 /* posExecutor */, 5 /* posCallback */, -1 /* posType */);
             }).when(mDnsResolver).query(any(), any(), anyInt(), any(), any(), any());
 
             // Queries on mCleartextDnsNetwork using DnsResolver#query with QueryType.
             doAnswer(invocation -> {
-                return mockQuery(invocation, 1 /* posHostname */, 4 /* posExecutor */,
-                        6 /* posCallback */, 2 /* posType */);
+                return mockQuery(invocation, 0 /* posNetwork */, 1 /* posHostname */,
+                        4 /* posExecutor */, 6 /* posCallback */, 2 /* posType */);
             }).when(mDnsResolver).query(any(), any(), anyInt(), anyInt(), any(), any(), any());
         }
 
         // Mocking queries on DnsResolver#query.
-        private Answer mockQuery(InvocationOnMock invocation, int posHostname, int posExecutor,
-                int posCallback, int posType) {
+        private Answer mockQuery(InvocationOnMock invocation, int posNetwork, int posHostname,
+                int posExecutor, int posCallback, int posType) {
             String hostname = (String) invocation.getArgument(posHostname);
             Executor executor = (Executor) invocation.getArgument(posExecutor);
             DnsResolver.Callback<List<InetAddress>> callback = invocation.getArgument(posCallback);
-            List<InetAddress> answer;
+            Network network = invocation.getArgument(posNetwork);
 
-            answer = posType != -1
-                    ? getAnswer(invocation.getMock(), hostname, invocation.getArgument(posType)) :
-                    queryAllTypes(invocation.getMock(), hostname);
+            final CompletableFuture<List<InetAddress>> answerFuture = posType != -1
+                    ? getAnswer(network, hostname, invocation.getArgument(posType))
+                    : queryAllTypes(network, hostname);
 
-            if (answer != null && answer.size() > 0) {
-                new Handler(Looper.getMainLooper()).post(() -> {
-                    executor.execute(() -> callback.onAnswer(answer, 0));
-                });
-            }
-            // If no answers, do nothing. sendDnsProbeWithTimeout will time out and throw UHE.
+            answerFuture.whenComplete((answer, exception) -> {
+                new Handler(Looper.getMainLooper()).post(() -> executor.execute(() -> {
+                    if (exception != null) {
+                        if (!(exception instanceof DnsResolver.DnsException)) {
+                            throw new AssertionError("Test error building DNS response", exception);
+                        }
+                        callback.onError((DnsResolver.DnsException) exception);
+                        return;
+                    }
+                    if (answer != null && answer.size() > 0) {
+                        callback.onAnswer(answer, 0);
+                    }
+                }));
+            });
+            // If the future does not complete or has no answer do nothing. The timeout should fire.
             return null;
         }
     }
@@ -528,6 +607,8 @@
         // it will fail the test because of timeout expired for querying AAAA and A sequentially.
         doReturn(200).when(mResources)
                 .getInteger(eq(R.integer.config_captive_portal_dns_probe_timeout));
+        doReturn(200).when(mDependencies).getDeviceConfigPropertyInt(
+                eq(NAMESPACE_CONNECTIVITY), eq(CONFIG_ASYNC_PRIVDNS_PROBE_TIMEOUT_MS), anyInt());
 
         doAnswer((invocation) -> {
             URL url = invocation.getArgument(0);
@@ -1239,7 +1320,7 @@
         setPortal302(mHttpConnection);
         final String httpHost = new URL(TEST_HTTP_URL).getHost();
         mFakeDns.setAnswer(httpHost, new String[] { "2001:db8::123" }, TYPE_AAAA);
-        final InetAddress parsedPrivateAddr = InetAddresses.parseNumericAddress(privateAddr);
+        final InetAddress parsedPrivateAddr = parseNumericAddress(privateAddr);
         mFakeDns.setAnswer(httpHost, new String[] { privateAddr },
                 (parsedPrivateAddr instanceof Inet6Address) ? TYPE_AAAA : TYPE_A);
     }
@@ -2199,8 +2280,7 @@
                         NETWORK_VALIDATION_RESULT_VALID, 0 /* probesSucceeded */));
     }
 
-    @Test
-    public void testPrivateDnsSuccess() throws Exception {
+    private void runPrivateDnsSuccessTest() throws Exception {
         setStatus(mHttpsConnection, 204);
         setStatus(mHttpConnection, 204);
 
@@ -2232,7 +2312,20 @@
     }
 
     @Test
-    public void testProbeStatusChanged() throws Exception {
+    public void testPrivateDnsSuccess_SyncDns() throws Exception {
+        doReturn(false).when(mDependencies).isFeatureEnabled(
+                any(), eq(NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION));
+        runPrivateDnsSuccessTest();
+    }
+
+    @Test
+    public void testPrivateDnsSuccess_AsyncDns() throws Exception {
+        doReturn(true).when(mDependencies).isFeatureEnabled(
+                any(), eq(NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION));
+        runPrivateDnsSuccessTest();
+    }
+
+    private void runProbeStatusChangedTest() throws Exception {
         // Set no record in FakeDns and expect validation to fail.
         setStatus(mHttpsConnection, 204);
         setStatus(mHttpConnection, 204);
@@ -2255,7 +2348,20 @@
     }
 
     @Test
-    public void testPrivateDnsResolutionRetryUpdate() throws Exception {
+    public void testProbeStatusChanged_SyncDns() throws Exception {
+        doReturn(false).when(mDependencies).isFeatureEnabled(
+                any(), eq(NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION));
+        runProbeStatusChangedTest();
+    }
+
+    @Test
+    public void testProbeStatusChanged_AsyncDns() throws Exception {
+        doReturn(true).when(mDependencies).isFeatureEnabled(
+                any(), eq(NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION));
+        runProbeStatusChangedTest();
+    }
+
+    private void runPrivateDnsResolutionRetryUpdateTest() throws Exception {
         // Set no record in FakeDns and expect validation to fail.
         setStatus(mHttpsConnection, 204);
         setStatus(mHttpConnection, 204);
@@ -2300,6 +2406,197 @@
     }
 
     @Test
+    public void testPrivateDnsResolutionRetryUpdate_SyncDns() throws Exception {
+        doReturn(false).when(mDependencies).isFeatureEnabled(
+                any(), eq(NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION));
+        runPrivateDnsResolutionRetryUpdateTest();
+    }
+
+    @Test
+    public void testPrivateDnsResolutionRetryUpdate_AsyncDns() throws Exception {
+        doReturn(true).when(mDependencies).isFeatureEnabled(
+                any(), eq(NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION));
+        runPrivateDnsResolutionRetryUpdateTest();
+    }
+
+    @Test
+    public void testAsyncPrivateDnsResolution_PartialTimeout() throws Exception {
+        doReturn(true).when(mDependencies).isFeatureEnabled(
+                any(), eq(NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION));
+        setStatus(mHttpsConnection, 204);
+        setStatus(mHttpConnection, 204);
+
+        WrappedNetworkMonitor wnm = makeCellNotMeteredNetworkMonitor();
+        wnm.notifyPrivateDnsSettingsChanged(new PrivateDnsConfig("dns.google", new InetAddress[0]));
+
+        // Only provide AAAA answer
+        mFakeDns.setAnswer("dns.google", new String[]{"2001:db8::1"}, TYPE_AAAA);
+
+        notifyNetworkConnected(wnm, CELL_NOT_METERED_CAPABILITIES);
+        verifyNetworkTestedValidFromPrivateDns(1 /* interactions */);
+
+        final PrivateDnsConfigParcel expectedConfig = new PrivateDnsConfigParcel();
+        expectedConfig.hostname = "dns.google";
+        expectedConfig.ips = new String[] {"2001:db8::1"};
+        expectedConfig.privateDnsMode = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+
+        verify(mCallbacks).notifyPrivateDnsConfigResolved(expectedConfig);
+    }
+
+    @Test
+    public void testAsyncPrivateDnsResolution_PartialFailure() throws Exception {
+        doReturn(true).when(mDependencies).isFeatureEnabled(
+                any(), eq(NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION));
+        setStatus(mHttpsConnection, 204);
+        setStatus(mHttpConnection, 204);
+
+        WrappedNetworkMonitor wnm = makeCellNotMeteredNetworkMonitor();
+        wnm.notifyPrivateDnsSettingsChanged(new PrivateDnsConfig("dns.google", new InetAddress[0]));
+
+        // A succeeds, AAAA fails
+        mFakeDns.setAnswer("dns.google", new String[]{"192.0.2.123"}, TYPE_A);
+        mFakeDns.setAnswer("dns.google", () -> {
+            // DnsResolver.DnsException constructor is T+, so use a mock instead
+            throw mock(DnsResolver.DnsException.class);
+        }, TYPE_AAAA);
+
+        notifyNetworkConnected(wnm, CELL_NOT_METERED_CAPABILITIES);
+        verifyNetworkTestedValidFromPrivateDns(1 /* interactions */);
+
+        final PrivateDnsConfigParcel expectedConfig = new PrivateDnsConfigParcel();
+        expectedConfig.hostname = "dns.google";
+        expectedConfig.ips = new String[] {"192.0.2.123"};
+        expectedConfig.privateDnsMode = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+
+        verify(mCallbacks).notifyPrivateDnsConfigResolved(expectedConfig);
+    }
+
+    @Test
+    public void testAsyncPrivateDnsResolution_AQuerySucceedsFirst_PrioritizeAAAA()
+            throws Exception {
+        doReturn(true).when(mDependencies).isFeatureEnabled(
+                any(), eq(NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION));
+        setStatus(mHttpsConnection, 204);
+        setStatus(mHttpConnection, 204);
+
+        WrappedNetworkMonitor wnm = makeCellNotMeteredNetworkMonitor();
+        wnm.notifyPrivateDnsSettingsChanged(new PrivateDnsConfig("dns.google", new InetAddress[0]));
+
+        final ConditionVariable v4Queried = new ConditionVariable();
+        mFakeDns.setAnswer("dns.google", () -> {
+            v4Queried.open();
+            return List.of(parseNumericAddress("192.0.2.123"));
+        }, TYPE_A);
+        mFakeDns.setAnswer("dns.google", () -> {
+            // Make sure the v6 query processing is a bit slower than the v6 one. The small delay
+            // below still does not guarantee that the v4 query will complete first, but it should
+            // the large majority of the time, which should be enough to test it. Even if it does
+            // not, the test should pass.
+            v4Queried.block(HANDLER_TIMEOUT_MS);
+            SystemClock.sleep(10L);
+            return List.of(parseNumericAddress("2001:db8::1"), parseNumericAddress("2001:db8::2"));
+        }, TYPE_AAAA);
+
+        notifyNetworkConnected(wnm, CELL_NOT_METERED_CAPABILITIES);
+        verifyNetworkTestedValidFromPrivateDns(1 /* interactions */);
+
+        final PrivateDnsConfigParcel expectedConfig = new PrivateDnsConfigParcel();
+        expectedConfig.hostname = "dns.google";
+        // The IPv6 addresses are still first
+        expectedConfig.ips = new String[] {"2001:db8::1", "2001:db8::2", "192.0.2.123"};
+        expectedConfig.privateDnsMode = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+
+        verify(mCallbacks).notifyPrivateDnsConfigResolved(expectedConfig);
+    }
+
+    @Test
+    public void testAsyncPrivateDnsResolution_ConfigChange_RestartsWithNewConfig()
+            throws Exception {
+        doReturn(true).when(mDependencies).isFeatureEnabled(
+                any(), eq(NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION));
+        setStatus(mHttpsConnection, 204);
+        setStatus(mHttpConnection, 204);
+
+        WrappedNetworkMonitor wnm = makeCellNotMeteredNetworkMonitor();
+        wnm.notifyPrivateDnsSettingsChanged(new PrivateDnsConfig("v1.google", new InetAddress[0]));
+
+        final ConditionVariable blockReplies = new ConditionVariable();
+        final CountDownLatch queriedLatch = new CountDownLatch(2);
+        mFakeDns.setAnswer("v1.google", () -> {
+            queriedLatch.countDown();
+            blockReplies.block(HANDLER_TIMEOUT_MS);
+            return List.of(parseNumericAddress("192.0.2.123"));
+        }, TYPE_A);
+        mFakeDns.setAnswer("v1.google", () -> {
+            queriedLatch.countDown();
+            blockReplies.block(HANDLER_TIMEOUT_MS);
+            return List.of(parseNumericAddress("2001:db8::1"));
+        }, TYPE_AAAA);
+
+        notifyNetworkConnected(wnm, CELL_NOT_METERED_CAPABILITIES);
+
+        queriedLatch.await(HANDLER_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+
+        // Send config update while DNS queries are in flight
+        mFakeDns.setAnswer("v2.google", new String[] { "192.0.2.124" }, TYPE_A);
+        mFakeDns.setAnswer("v2.google", new String[] { "2001:db8::2" }, TYPE_AAAA);
+        wnm.notifyPrivateDnsSettingsChanged(new PrivateDnsConfig("v2.google", new InetAddress[0]));
+
+        // Let the original queries finish. Once DNS queries finish results are posted to the
+        // handler, so they will be processed on the handler after the DNS settings change.
+        blockReplies.open();
+
+        // Expect only callbacks for the 2nd configuration
+        verifyNetworkTestedValidFromPrivateDns(1 /* interactions */);
+
+        final PrivateDnsConfigParcel expectedConfig = new PrivateDnsConfigParcel();
+        expectedConfig.hostname = "v2.google";
+        expectedConfig.ips = new String[] {"2001:db8::2", "192.0.2.124"};
+        expectedConfig.privateDnsMode = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+
+        verify(mCallbacks).notifyPrivateDnsConfigResolved(expectedConfig);
+    }
+
+    @Test
+    public void testAsyncPrivateDnsResolution_TurnOffStrictMode_SkipsDnsValidation()
+            throws Exception {
+        doReturn(true).when(mDependencies).isFeatureEnabled(
+                any(), eq(NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION));
+        setStatus(mHttpsConnection, 204);
+        setStatus(mHttpConnection, 204);
+
+        WrappedNetworkMonitor wnm = makeCellNotMeteredNetworkMonitor();
+        wnm.notifyPrivateDnsSettingsChanged(new PrivateDnsConfig("v1.google", new InetAddress[0]));
+
+        final ConditionVariable blockReplies = new ConditionVariable();
+        final CountDownLatch queriedLatch = new CountDownLatch(2);
+        mFakeDns.setAnswer("v1.google", () -> {
+            queriedLatch.countDown();
+            blockReplies.block(HANDLER_TIMEOUT_MS);
+            return List.of(parseNumericAddress("192.0.2.123"));
+        }, TYPE_A);
+        mFakeDns.setAnswer("v1.google", () -> {
+            queriedLatch.countDown();
+            blockReplies.block(HANDLER_TIMEOUT_MS);
+            return List.of(parseNumericAddress("2001:db8::1"));
+        }, TYPE_AAAA);
+
+        notifyNetworkConnected(wnm, CELL_NOT_METERED_CAPABILITIES);
+
+        queriedLatch.await(HANDLER_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+
+        // Send config update while DNS queries are in flight
+        wnm.notifyPrivateDnsSettingsChanged(new PrivateDnsConfig(true /* useTls */));
+
+        // Let the original queries finish. Once DNS queries finish results are posted to the
+        // handler, so they will be processed on the handler after the DNS settings change.
+        blockReplies.open();
+
+        verifyNetworkTestedValidFromHttps(1 /* interactions */);
+        verify(mCallbacks, never()).notifyPrivateDnsConfigResolved(any());
+    }
+
+    @Test
     public void testReevaluationInterval_networkResume() throws Exception {
         // Setup nothing and expect validation to fail.
         doReturn(true).when(mDependencies).isFeatureEnabled(any(), eq(REEVALUATE_WHEN_RESUME));
@@ -2725,8 +3022,8 @@
         } catch (UnknownHostException e) {
         }
 
-        mFakeDns.setAnswer("www.android.com", null, TYPE_A);
-        mFakeDns.setAnswer("www.android.com", null, TYPE_AAAA);
+        mFakeDns.setAnswer("www.android.com", (String[]) null, TYPE_A);
+        mFakeDns.setAnswer("www.android.com", (String[]) null, TYPE_AAAA);
         try {
             wnm.sendDnsProbeWithTimeout("www.android.com", shortTimeoutMs);
             fail("DNS query timed out, expected UnknownHostException");