Snap for 13270875 from 83ea07ed94890b52265e07d6760c7ed8f4cbd344 to sdk-release

Change-Id: I4b3a4eeb3eaee7e9ffa574466c73d28d0b10cfbc
diff --git a/res/values/config.xml b/res/values/config.xml
index 8d6b64e..d9ee36d 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -51,6 +51,8 @@
     </string-array>
     <string-array name="config_captive_portal_https_urls" translatable="false">
     </string-array>
+    <bool name="config_probe_multi_http_https_url_serial">false</bool>
+    <integer name="config_serial_url_probe_gap_time">1000</integer>
 
     <!-- Customized default DNS Servers address. -->
     <string-array name="config_default_dns_servers" translatable="false">
diff --git a/res/values/overlayable.xml b/res/values/overlayable.xml
index 0aeaaec..8e8fe08 100644
--- a/res/values/overlayable.xml
+++ b/res/values/overlayable.xml
@@ -42,6 +42,8 @@
             <item type="array" name="config_captive_portal_http_urls"/>
             <item type="array" name="config_captive_portal_https_urls"/>
             <item type="array" name="config_captive_portal_fallback_urls"/>
+            <item type="bool" name="config_probe_multi_http_https_url_serial"/>
+            <item type="integer" name="config_serial_url_probe_gap_time"/>
             <item type="bool" name="config_no_sim_card_uses_neighbor_mcc"/>
             <!-- Configuration value for DhcpResults -->
             <item type="array" name="config_default_dns_servers"/>
diff --git a/src/android/net/apf/ApfFilter.java b/src/android/net/apf/ApfFilter.java
index dfab0df..249a1e5 100644
--- a/src/android/net/apf/ApfFilter.java
+++ b/src/android/net/apf/ApfFilter.java
@@ -1812,8 +1812,8 @@
         }
 
         // Drop if not ARP IPv4.
-        gen.addLoadImmediate(R0, ARP_HEADER_OFFSET);
-        gen.addCountAndDropIfBytesAtR0NotEqual(ARP_IPV4_HEADER, DROPPED_ARP_NON_IPV4);
+        gen.addCountAndDropIfBytesAtOffsetNotEqual(ARP_HEADER_OFFSET, ARP_IPV4_HEADER,
+                DROPPED_ARP_NON_IPV4);
 
         final short checkArpRequest = gen.getUniqueLabel();
 
@@ -1830,8 +1830,8 @@
 
         // Pass if non-broadcast reply.
         // This also accepts multicast arp, but we assume those don't exist.
-        gen.addLoadImmediate(R0, ETH_DEST_ADDR_OFFSET);
-        gen.addCountAndPassIfBytesAtR0NotEqual(ETHER_BROADCAST, PASSED_ARP_UNICAST_REPLY);
+        gen.addCountAndPassIfBytesAtOffsetNotEqual(ETH_DEST_ADDR_OFFSET, ETHER_BROADCAST,
+                PASSED_ARP_UNICAST_REPLY);
 
         // It is a broadcast reply.
         if (mIPv4Address == null) {
@@ -1897,8 +1897,8 @@
         // the future, such packets will likely be dropped by multicast filters.
         // Since the device may have packet forwarding enabled, APF needs to pass any received
         // unicast IPv4 ping not destined for the device's IP address to the kernel.
-        gen.addLoadImmediate(R0, ETHER_DST_ADDR_OFFSET)
-                .addJumpIfBytesAtR0NotEqual(mHardwareAddress, skipIpv4PingFilter)
+        gen.addJumpIfBytesAtOffsetNotEqual(
+                ETH_DEST_ADDR_OFFSET, mHardwareAddress, skipIpv4PingFilter)
                 .addLoadImmediate(R0, IPV4_DEST_ADDR_OFFSET)
                 .addJumpIfBytesAtR0NotEqual(mIPv4Address, skipIpv4PingFilter);
 
@@ -1979,11 +1979,11 @@
         // address for IPv4 mDNS packet) or the device's MAC address, skip filtering.
         // We need to check both the mDNS multicast MAC address and the device's MAC address
         // because multicast to unicast conversion might have occurred.
-        gen.addLoadImmediate(R0, ETH_DEST_ADDR_OFFSET)
-                .addJumpIfBytesAtR0EqualsNoneOf(
-                        List.of(mHardwareAddress, ETH_MULTICAST_MDNS_V4_MAC_ADDRESS),
-                        skipMdnsFilter
-                );
+        gen.addJumpIfBytesAtOffsetEqualsNoneOf(
+                ETH_DEST_ADDR_OFFSET,
+                List.of(mHardwareAddress, ETH_MULTICAST_MDNS_V4_MAC_ADDRESS),
+                skipMdnsFilter
+        );
 
         // Ignore packets with IPv4 options (header size not equal to 20) as they are rare.
         gen.addLoadFromMemory(R0, MemorySlot.IPV4_HEADER_SIZE)
@@ -2153,7 +2153,10 @@
 
             // If IPv4 destination address is in multicast range, drop.
             gen.addLoad8intoR0(IPV4_DEST_ADDR_OFFSET);
-            gen.addAnd(0xf0);
+            // we just loaded a byte, so top 24 bits are zero, thus and'ing
+            // with either one of 0xF0 and 0xFFFFFFF0 accomplishes the same thing,
+            // we thus choose the one which encodes shorter
+            gen.addAnd((gen instanceof ApfV4Generator) ? 0xF0 : 0xFFFFFFF0);
             gen.addCountAndDropIfR0Equals(0xe0, DROPPED_IPV4_MULTICAST);
 
             // If IPv4 broadcast packet, drop regardless of L2 (b/30231088).
@@ -2182,8 +2185,8 @@
             // Otherwise, this is an IPv4 unicast, pass
             // If L2 broadcast packet, drop.
             // TODO: can we invert this condition to fall through to the common pass case below?
-            gen.addLoadImmediate(R0, ETH_DEST_ADDR_OFFSET);
-            gen.addCountAndPassIfBytesAtR0NotEqual(ETHER_BROADCAST, PASSED_IPV4_UNICAST);
+            gen.addCountAndPassIfBytesAtOffsetNotEqual(ETH_DEST_ADDR_OFFSET, ETHER_BROADCAST,
+                    PASSED_IPV4_UNICAST);
             gen.addCountAndDrop(DROPPED_IPV4_L2_BROADCAST);
         }
 
@@ -2326,20 +2329,21 @@
         // used by processes other than clatd. This is because APF cannot reliably detect signal
         // on when IPV6_{JOIN,LEAVE}_ANYCAST is triggered.
         final List<byte[]> allMACs = getKnownMacAddresses();
-        v6Gen.addLoadImmediate(R0, ETH_DEST_ADDR_OFFSET)
-                .addCountAndDropIfBytesAtR0EqualsNoneOf(allMACs, DROPPED_IPV6_NS_OTHER_HOST);
+        v6Gen.addCountAndDropIfBytesAtOffsetEqualsNoneOf(ETH_DEST_ADDR_OFFSET, allMACs,
+                DROPPED_IPV6_NS_OTHER_HOST);
 
         // Dst IPv6 address check:
         final List<byte[]> allSuffixes = getSolicitedNodeMcastAddressSuffix(allIPv6Addrs);
         final short notIpV6SolicitedNodeMcast = v6Gen.getUniqueLabel();
         final short endOfIpV6DstCheck = v6Gen.getUniqueLabel();
-        v6Gen.addLoadImmediate(R0, IPV6_DEST_ADDR_OFFSET)
-                .addJumpIfBytesAtR0NotEqual(IPV6_SOLICITED_NODES_PREFIX, notIpV6SolicitedNodeMcast)
-                .addAdd(13)
+        v6Gen.addJumpIfBytesAtOffsetNotEqual(
+                IPV6_DEST_ADDR_OFFSET, IPV6_SOLICITED_NODES_PREFIX, notIpV6SolicitedNodeMcast)
+                .addLoadImmediate(R0, IPV6_DEST_ADDR_OFFSET + 13)
                 .addCountAndDropIfBytesAtR0EqualsNoneOf(allSuffixes, DROPPED_IPV6_NS_OTHER_HOST)
                 .addJump(endOfIpV6DstCheck)
                 .defineLabel(notIpV6SolicitedNodeMcast)
-                .addCountAndDropIfBytesAtR0EqualsNoneOf(allIPv6Addrs, DROPPED_IPV6_NS_OTHER_HOST)
+                .addCountAndDropIfBytesAtOffsetEqualsNoneOf(
+                        IPV6_DEST_ADDR_OFFSET, allIPv6Addrs, DROPPED_IPV6_NS_OTHER_HOST)
                 .defineLabel(endOfIpV6DstCheck);
 
         // Hop limit not 255, NS requires hop limit to be 255 -> drop
@@ -2362,10 +2366,10 @@
                 true, /* includeTentative */
                 false /* includeAnycast */
         );
-        v6Gen.addLoadImmediate(R0, ICMP6_NS_TARGET_IP_OFFSET);
+
         if (!tentativeIPv6Addrs.isEmpty()) {
-            v6Gen.addCountAndPassIfBytesAtR0EqualsAnyOf(
-                    tentativeIPv6Addrs, PASSED_IPV6_ICMP);
+            v6Gen.addCountAndPassIfBytesAtOffsetEqualsAnyOf(
+                    ICMP6_NS_TARGET_IP_OFFSET, tentativeIPv6Addrs, PASSED_IPV6_ICMP);
         }
 
         final List<byte[]> nonTentativeIpv6Addrs = getIpv6Addresses(
@@ -2377,12 +2381,12 @@
             v6Gen.addCountAndDrop(DROPPED_IPV6_NS_OTHER_HOST);
             return;
         }
-        v6Gen.addCountAndDropIfBytesAtR0EqualsNoneOf(
-                nonTentativeIpv6Addrs, DROPPED_IPV6_NS_OTHER_HOST);
+        v6Gen.addCountAndDropIfBytesAtOffsetEqualsNoneOf(
+                ICMP6_NS_TARGET_IP_OFFSET, nonTentativeIpv6Addrs, DROPPED_IPV6_NS_OTHER_HOST);
 
         // if source ip is unspecified (::), it's DAD request -> pass
-        v6Gen.addLoadImmediate(R0, IPV6_SRC_ADDR_OFFSET)
-                .addCountAndPassIfBytesAtR0Equal(IPV6_UNSPECIFIED_ADDRESS, PASSED_IPV6_ICMP);
+        v6Gen.addCountAndPassIfBytesAtOffsetEqual(
+                IPV6_SRC_ADDR_OFFSET, IPV6_UNSPECIFIED_ADDRESS, PASSED_IPV6_ICMP);
 
         // Only offload NUD/Address resolution packets that have SLLA as the their first option.
         // For option-less NUD packets or NUD/Address resolution packets where
@@ -2440,11 +2444,11 @@
         // address for IPv6 mDNS packet) or the device's MAC address, skip filtering.
         // We need to check both the mDNS multicast MAC address and the device's MAC address
         // because multicast to unicast conversion might have occurred.
-        gen.addLoadImmediate(R0, ETH_DEST_ADDR_OFFSET)
-                .addJumpIfBytesAtR0EqualsNoneOf(
-                        List.of(mHardwareAddress, ETH_MULTICAST_MDNS_V6_MAC_ADDRESS),
-                        skipMdnsFilter
-                );
+        gen.addJumpIfBytesAtOffsetEqualsNoneOf(
+                ETH_DEST_ADDR_OFFSET,
+                List.of(mHardwareAddress, ETH_MULTICAST_MDNS_V6_MAC_ADDRESS),
+                skipMdnsFilter
+        );
 
         // Skip filtering if the packet is not an IPv6 UDP packet.
         gen.addLoad8intoR0(IPV6_NEXT_HEADER_OFFSET)
@@ -2455,8 +2459,7 @@
         // Some devices can use unicast queries for mDNS to improve performance and reliability.
         // These packets are not currently offloaded and will be passed by APF and handled
         // by NsdService.
-        gen.addLoadImmediate(R0, IPV6_DEST_ADDR_OFFSET)
-                .addJumpIfBytesAtR0NotEqual(MDNS_IPV6_ADDR, skipMdnsFilter);
+        gen.addJumpIfBytesAtOffsetNotEqual(IPV6_DEST_ADDR_OFFSET, MDNS_IPV6_ADDR, skipMdnsFilter);
 
         // We now know that the packet is an mDNS packet,
         // i.e., an IPv6 UDP packet destined for port 5353 with the expected destination MAC and IP
@@ -2505,10 +2508,10 @@
                 true /* includeNonTentative */,
                 false /* includeTentative */,
                 false /* includeAnycast */);
-        gen.addLoadImmediate(R0, ETHER_DST_ADDR_OFFSET)
-                .addJumpIfBytesAtR0NotEqual(mHardwareAddress, skipPing6Offload)
-                .addLoadImmediate(R0, IPV6_DEST_ADDR_OFFSET)
-                .addJumpIfBytesAtR0EqualsNoneOf(nonTentativeIPv6Addrs, skipPing6Offload);
+        gen.addJumpIfBytesAtOffsetNotEqual(
+                ETHER_DST_ADDR_OFFSET, mHardwareAddress, skipPing6Offload)
+                .addJumpIfBytesAtOffsetEqualsNoneOf(
+                        IPV6_DEST_ADDR_OFFSET, nonTentativeIPv6Addrs, skipPing6Offload);
 
         // We need to check if the packet is sufficiently large to be a valid ICMPv6 echo packet.
         gen.addLoadFromMemory(R0, MemorySlot.PACKET_SIZE)
@@ -2731,8 +2734,8 @@
         // This is a way to cover ff02::1 and ff02::2 with a single JNEBS.
         // 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(R0, IPV6_DEST_ADDR_OFFSET);
-        gen.addJumpIfBytesAtR0NotEqual(unsolicitedNaDropPrefix, skipUnsolicitedMulticastNALabel);
+        gen.addJumpIfBytesAtOffsetNotEqual(
+                IPV6_DEST_ADDR_OFFSET, unsolicitedNaDropPrefix, skipUnsolicitedMulticastNALabel);
 
         gen.addCountAndDrop(DROPPED_IPV6_MULTICAST_NA);
         gen.defineLabel(skipUnsolicitedMulticastNALabel);
@@ -3268,15 +3271,14 @@
 
         // If the multicast address is not "::", it is an MLD2 multicast-address-specific query,
         // then pass.
-        gen.addLoadImmediate(R0, IPV6_MLD_MULTICAST_ADDR_OFFSET)
-                .addCountAndPassIfBytesAtR0NotEqual(IPV6_ADDR_ANY.getAddress(), PASSED_IPV6_ICMP);
+        gen.addCountAndPassIfBytesAtOffsetNotEqual(
+                IPV6_MLD_MULTICAST_ADDR_OFFSET, IPV6_ADDR_ANY.getAddress(), PASSED_IPV6_ICMP);
 
         // If we reach here, we know it is an MLDv1/MLDv2 general query.
 
         // The general query IPv6 destination address must be ff02::1.
-        gen.addLoadImmediate(R0, IPV6_DEST_ADDR_OFFSET)
-                .addCountAndDropIfBytesAtR0NotEqual(IPV6_ALL_NODES_ADDRESS,
-                        DROPPED_IPV6_MLD_INVALID);
+        gen.addCountAndDropIfBytesAtOffsetNotEqual(
+                        IPV6_DEST_ADDR_OFFSET, IPV6_ALL_NODES_ADDRESS, DROPPED_IPV6_MLD_INVALID);
 
         // If the MLD payload length is 24, it is an MLDv1 packet, otherwise, it is an MLDv2 packet.
         gen.addLoadFromMemory(R0, MemorySlot.SLOT_0)
@@ -3588,11 +3590,13 @@
         //   pass
         // insert IPv6 filter to drop, pass, or fall off the end for ICMPv6 packets
 
-        gen.addLoadImmediate(R0, ETHER_SRC_ADDR_OFFSET);
         if (NetworkStackUtils.isAtLeast25Q2()) {
-            gen.addCountAndDropIfBytesAtR0Equal(mHardwareAddress, DROPPED_ETHER_OUR_SRC_MAC);
+            gen.addCountAndDropIfBytesAtOffsetEqual(ETHER_SRC_ADDR_OFFSET, mHardwareAddress,
+                    DROPPED_ETHER_OUR_SRC_MAC);
         } else {
-            gen.addCountAndPassIfBytesAtR0Equal(mHardwareAddress, PASSED_ETHER_OUR_SRC_MAC);
+            // TODO: we don't have test coverage for this line
+            gen.addCountAndPassIfBytesAtOffsetEqual(ETHER_SRC_ADDR_OFFSET, mHardwareAddress,
+                    PASSED_ETHER_OUR_SRC_MAC);
         }
 
         gen.addLoad16intoR0(ETH_ETHERTYPE_OFFSET);
@@ -3600,8 +3604,8 @@
             // Pass unicast TDLS packet but drop non-unicast TDLS packet.
             short skipTDLScheck = gen.getUniqueLabel();
             gen.addJumpIfR0NotEquals(0x890DL, skipTDLScheck)
-                    .addLoadImmediate(R0, ETH_DEST_ADDR_OFFSET)
-                    .addCountAndDropIfBytesAtR0NotEqual(mHardwareAddress, DROPPED_NON_UNICAST_TDLS)
+                    .addCountAndDropIfBytesAtOffsetNotEqual(
+                            ETH_DEST_ADDR_OFFSET, mHardwareAddress, DROPPED_NON_UNICAST_TDLS)
                     .addCountAndPass(PASSED_NON_IP_UNICAST)
                     .defineLabel(skipTDLScheck);
 
@@ -3646,8 +3650,8 @@
         gen.addJumpIfR0Equals(ETH_P_IPV6, ipv6FilterLabel);
 
         // Drop non-IP non-ARP broadcasts, pass the rest
-        gen.addLoadImmediate(R0, ETH_DEST_ADDR_OFFSET);
-        gen.addCountAndPassIfBytesAtR0NotEqual(ETHER_BROADCAST, PASSED_NON_IP_UNICAST);
+        gen.addCountAndPassIfBytesAtOffsetNotEqual(ETH_DEST_ADDR_OFFSET, ETHER_BROADCAST,
+                PASSED_NON_IP_UNICAST);
         gen.addCountAndDrop(DROPPED_ETH_BROADCAST);
 
         // Add IPv6 filters:
@@ -3728,10 +3732,80 @@
     private int calcMdnsOffloadProgramSizeOverEstimate(int numOfMdnsRuleToOffload)
             throws IllegalInstructionException {
         ApfV6GeneratorBase<?> gen = (ApfV6GeneratorBase<?>) createApfGenerator();
+        // We need to preload data for size estimation because the preloaded data contains mDNS
+        // data chunks. If we don't preload, generateMdnsQueryOffload() will add data to the data
+        // region, resulting in an incorrect estimated size.
+        if (gen instanceof ApfV61GeneratorBase<?>) {
+            preloadData((ApfV61GeneratorBase<?>) gen);
+        }
+        final int programLengthOverEstimateBefore = gen.programLengthOverEstimate();
         short tmpLabelCheckMdnsQueryPayload = gen.getUniqueLabel();
         generateMdnsQueryOffload(gen, tmpLabelCheckMdnsQueryPayload,
                 numOfMdnsRuleToOffload);
-        return gen.programLengthOverEstimate() - gen.getBaseProgramSize();
+        return gen.programLengthOverEstimate() - programLengthOverEstimateBefore;
+    }
+
+    void preloadData(ApfV61GeneratorBase<?> gen) throws IllegalInstructionException {
+        final List<byte[]> preloadedMacAddress = getKnownMacAddresses();
+        final List<byte[]> preloadedIPv6Address = getIpv6Addresses(true /* includeNonTentative */,
+                true /* includeTentative */, true /* includeAnycast */);
+        preloadedIPv6Address.add(IPV6_ADDR_ALL_NODES_MULTICAST.getAddress());
+        preloadedIPv6Address.add(IPV6_ADDR_ANY.getAddress());
+        byte[] mdns6NextHdrToUdpDport = new byte[0];
+        byte[] mdns6EthDstToFlowLabel = new byte[0];
+        byte[] mdns4EthDstToTos = new byte[0];
+        if (enableMdns6Offload()) {
+            mdns6NextHdrToUdpDport = createMdns6PktFromIPv6NextHdrToUdpDport(true);
+            preloadedIPv6Address.removeIf(
+                    addr -> Arrays.equals(addr, mIPv6LinkLocalAddress.getAddress()));
+            mdns6EthDstToFlowLabel = createMdns6PktFromEthDstToIPv6FlowLabel(true);
+            preloadedMacAddress.removeIf(
+                    addr -> Arrays.equals(addr, mHardwareAddress) || Arrays.equals(addr,
+                            ETH_MULTICAST_MDNS_V6_MAC_ADDRESS));
+        }
+
+        if (enableMdns4Offload()) {
+            mdns4EthDstToTos = createMdns4PktFromEthDstToIPv4Tos(true);
+            preloadedMacAddress.removeIf(
+                    addr -> Arrays.equals(addr, mHardwareAddress) || Arrays.equals(addr,
+                            ETH_MULTICAST_MDNS_V4_MAC_ADDRESS));
+        }
+
+        int preloadDataSize = mdns6NextHdrToUdpDport.length + mdns6EthDstToFlowLabel.length
+                + mdns4EthDstToTos.length + preloadedIPv6Address.size() * 16
+                + preloadedMacAddress.size() * 6;
+
+        if (enableArpOffload()) {
+            preloadDataSize += FIXED_ARP_REPLY_HEADER.length;
+        }
+
+        final byte[] preloadData = new byte[preloadDataSize];
+        int offset = 0;
+        System.arraycopy(mdns6NextHdrToUdpDport, 0, preloadData, offset,
+                mdns6NextHdrToUdpDport.length);
+        offset += mdns6NextHdrToUdpDport.length;
+        System.arraycopy(mdns6EthDstToFlowLabel, 0, preloadData, offset,
+                mdns6EthDstToFlowLabel.length);
+        offset += mdns6EthDstToFlowLabel.length;
+        System.arraycopy(mdns4EthDstToTos, 0, preloadData, offset, mdns4EthDstToTos.length);
+        offset += mdns4EthDstToTos.length;
+        for (byte[] addr : preloadedMacAddress) {
+            System.arraycopy(addr, 0, preloadData, offset, 6);
+            offset += 6;
+        }
+        for (byte[] addr : preloadedIPv6Address) {
+            System.arraycopy(addr, 0, preloadData, offset, 16);
+            offset += 16;
+        }
+        if (enableArpOffload()) {
+            System.arraycopy(FIXED_ARP_REPLY_HEADER, 0, preloadData, offset,
+                    FIXED_ARP_REPLY_HEADER.length);
+            offset += FIXED_ARP_REPLY_HEADER.length;
+        }
+
+        if (preloadDataSize > 0) {
+            gen.addPreloadData(preloadData);
+        }
     }
 
     /**
@@ -3755,6 +3829,9 @@
         try {
             // Step 1: Determine how many RA filters/mDNS offloads we can fit in the program.
             ApfV4GeneratorBase<?> gen = createApfGenerator();
+            if (gen instanceof ApfV61GeneratorBase<?>) {
+                preloadData((ApfV61GeneratorBase<?>) gen);
+            }
             short labelCheckMdnsQueryPayload = gen.getUniqueLabel();
 
             emitPrologue(gen, labelCheckMdnsQueryPayload);
diff --git a/src/android/net/apf/ApfV4GeneratorBase.java b/src/android/net/apf/ApfV4GeneratorBase.java
index f142e31..d750229 100644
--- a/src/android/net/apf/ApfV4GeneratorBase.java
+++ b/src/android/net/apf/ApfV4GeneratorBase.java
@@ -482,6 +482,17 @@
     }
 
     /**
+     * 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 offset} don't match {@code bytes}.
+     * This method needs to be non-final because APFv4 and APFv6 share the same implementation,
+     * but in APFv6.1, this method will be overridden to use the JBSPTRMATCH instruction.
+     */
+    public Type addJumpIfBytesAtOffsetNotEqual(int offset, @NonNull byte[] bytes, short tgt)
+            throws IllegalInstructionException {
+        return addLoadImmediate(R0, offset).addJumpIfBytesAtR0NotEqual(bytes, tgt);
+    }
+
+    /**
      * Add instructions to the end of the program to increase counter and drop packet if the
      * bytes of the packet at an offset specified by register0 don't match {@code bytes}.
      * WARNING: may modify R1
@@ -499,6 +510,50 @@
 
     /**
      * Add instructions to the end of the program to increase counter and drop packet if the
+     * bytes of the packet at an offset specified by {@code offset} don't match {@code bytes}.
+     * This method needs to be non-final because APFv4 and APFv6 share the same implementation,
+     * but in APFv6.1, this method will be overridden to use the JBSPTRMATCH instruction.
+     */
+    public Type addCountAndDropIfBytesAtOffsetNotEqual(int offset, byte[] bytes,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addLoadImmediate(R0, offset).addCountAndDropIfBytesAtR0NotEqual(bytes, cnt);
+    }
+
+    /**
+     * Add instructions to the end of the program to increase counter and pass packet if the
+     * bytes of the packet at an offset specified by {@code offset} don't match {@code bytes}.
+     * This method needs to be non-final because APFv4 and APFv6 share the same implementation,
+     * but in APFv6.1, this method will be overridden to use the JBSPTRMATCH instruction.
+     */
+    public Type addCountAndPassIfBytesAtOffsetNotEqual(int offset, byte[] bytes,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addLoadImmediate(R0, offset).addCountAndPassIfBytesAtR0NotEqual(bytes, cnt);
+    }
+
+    /**
+     * Add instructions to the end of the program to increase counter and drop packet if the
+     * bytes of the packet at an offset specified by {@code offset} does match {@code bytes}.
+     * This method needs to be non-final because APFv4 and APFv6 share the same implementation,
+     * but in APFv6.1, this method will be overridden to use the JBSPTRMATCH instruction.
+     */
+    public Type addCountAndDropIfBytesAtOffsetEqual(int offset, byte[] bytes,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addLoadImmediate(R0, offset).addCountAndDropIfBytesAtR0Equal(bytes, cnt);
+    }
+
+    /**
+     * Add instructions to the end of the program to increase counter and pass packet if the
+     * bytes of the packet at an offset specified by {@code offset} does match {@code bytes}.
+     * This method needs to be non-final because APFv4 and APFv6 share the same implementation,
+     * but in APFv6.1, this method will be overridden to use the JBSPTRMATCH instruction.
+     */
+    public Type addCountAndPassIfBytesAtOffsetEqual(int offset, byte[] bytes,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addLoadImmediate(R0, offset).addCountAndPassIfBytesAtR0Equal(bytes, cnt);
+    }
+
+    /**
+     * Add instructions to the end of the program to increase counter and drop packet if the
      * bytes of the packet at an offset specified by register0 match {@code bytes}.
      * WARNING: may modify R1
      */
diff --git a/src/android/net/apf/ApfV61GeneratorBase.java b/src/android/net/apf/ApfV61GeneratorBase.java
index e5859e7..c60de72 100644
--- a/src/android/net/apf/ApfV61GeneratorBase.java
+++ b/src/android/net/apf/ApfV61GeneratorBase.java
@@ -15,11 +15,13 @@
  */
 package android.net.apf;
 
+import static android.net.apf.BaseApfGenerator.Rbit.Rbit0;
 import static android.net.apf.BaseApfGenerator.Rbit.Rbit1;
 import static android.net.apf.BaseApfGenerator.Register.R0;
 
 import androidx.annotation.NonNull;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
 
@@ -199,6 +201,160 @@
         return self();
     }
 
+    @Override
+    public Type addAllocate(int size) {
+        final int imm = (size > 266) ? (size - 266 + 7) / 8 : 0;
+        return append(new Instruction(Opcodes.ALLOC_XMIT, Rbit1).addUnsigned(imm));
+    }
+
+    @Override
+    public Type addTransmitWithoutChecksum() {
+        return append(new Instruction(Opcodes.ALLOC_XMIT, Rbit0));
+    }
+
+    @Override
+    protected boolean handleOptimizedTransmit(int ipOfs, int csumOfs, int csumStart,
+                                              int partialCsum, boolean isUdp) {
+        if (ipOfs != 14) return false;
+        int v = -1;
+        if ( isUdp && csumStart == 26 && csumOfs == 40) v = 0;  // ether/ipv4/udp
+        if (!isUdp && csumStart == 26 && csumOfs == 44) v = 1;  // ether/ipv4/tcp
+        if (!isUdp && csumStart == 34 && csumOfs == 36) v = 2;  // ether/ipv4/icmp
+        if (!isUdp && csumStart == 38 && csumOfs == 40) v = 3;  // ether/ipv4/routeralert/icmp
+        if ( isUdp && csumStart == 22 && csumOfs == 60) v = 4;  // ether/ipv6/udp
+        if (!isUdp && csumStart == 22 && csumOfs == 64) v = 5;  // ether/ipv6/tcp
+        if (!isUdp && csumStart == 22 && csumOfs == 56) v = 6;  // ether/ipv6/icmp
+        if (!isUdp && csumStart == 22 && csumOfs == 64) v = 7;  // ether/ipv6/routeralert/icmp
+        if (v < 0) return false;
+        v |= partialCsum << 3;
+        append(new Instruction(Opcodes.ALLOC_XMIT, Rbit0).addUnsigned(v));
+        return true;
+    }
+
+    private List<byte[]> addJumpIfBytesAtOffsetEqualsHelper(int offset,
+            @NonNull List<byte[]> bytesList, short tgt, boolean jumpOnMatch)
+            throws IllegalInstructionException {
+        final List<byte[]> deduplicatedList =
+                bytesList.size() == 1 ? bytesList : validateDeduplicateBytesList(bytesList);
+        if (offset < 0 || offset > 255) {
+            return deduplicatedList;
+        }
+        final int count = deduplicatedList.size();
+        final int compareLength = deduplicatedList.get(0).length;
+        if (compareLength > 16) {
+            return deduplicatedList;
+        }
+        final List<byte[]> failbackList = new ArrayList<>();
+        final List<Integer> ptrs = new ArrayList<>();
+        for (int i = 0; i < count; ++i) {
+            final byte[] bytes = deduplicatedList.get(i);
+            int relativeOffset = mInstructions.get(0).findMatchInDataBytes(bytes, 0, bytes.length);
+            if (relativeOffset < 0 || relativeOffset % 2 == 1 || relativeOffset > 510) {
+                failbackList.add(bytes);
+                continue;
+            }
+            ptrs.add(relativeOffset / 2);
+        }
+        final Rbit rbit = jumpOnMatch ? Rbit1 : Rbit0;
+        int totalPtrs = ptrs.size();
+        for (int i = 0; i < totalPtrs; i += 16) {
+            final int currentCount = Math.min(totalPtrs - i, 16);
+            final Instruction instruction = new Instruction(Opcodes.JBSPTRMATCH, rbit)
+                    .addU8(offset)
+                    .addU8((currentCount - 1) * 16 + (compareLength - 1))
+                    .setTargetLabel(tgt);
+            for (int j = 0; j < currentCount; j++) {
+                instruction.addU8(ptrs.get(i + j));
+            }
+            append(instruction);
+        }
+        return failbackList;
+    }
+
+    /**
+     * 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 offset} match any of the elements in
+     * {@code bytesList}.
+     */
+    public Type addJumpIfBytesAtOffsetEqualsAnyOf(int offset, @NonNull List<byte[]> bytesList,
+            short tgt) throws IllegalInstructionException {
+        final List<byte[]> failbackList = addJumpIfBytesAtOffsetEqualsHelper(offset, bytesList, tgt,
+                true /* jumpOnMatch */);
+        if (failbackList.isEmpty()) {
+            return self();
+        }
+        return addLoadImmediate(R0, offset).addJumpIfBytesAtR0EqualsAnyOf(failbackList, tgt);
+    }
+
+    /**
+     * 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 offset} match none of the elements in
+     * {@code bytesList}.
+     */
+    public Type addJumpIfBytesAtOffsetEqualsNoneOf(int offset, @NonNull List<byte[]> bytesList,
+            short tgt) throws IllegalInstructionException {
+        final List<byte[]> failbackList = addJumpIfBytesAtOffsetEqualsHelper(offset, bytesList, tgt,
+                false /* jumpOnMatch */);
+        if (failbackList.isEmpty()) {
+            return self();
+        }
+        return addLoadImmediate(R0, offset).addJumpIfBytesAtR0EqualsNoneOf(failbackList, tgt);
+    }
+
+    @Override
+    public Type addCountAndDropIfBytesAtOffsetEqualsAnyOf(int offset, List<byte[]> bytesList,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addJumpIfBytesAtOffsetEqualsAnyOf(offset, bytesList, cnt.getJumpDropLabel());
+    }
+
+    @Override
+    public Type addCountAndPassIfBytesAtOffsetEqualsAnyOf(int offset, List<byte[]> bytesList,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addJumpIfBytesAtOffsetEqualsAnyOf(offset, bytesList, cnt.getJumpPassLabel());
+    }
+
+    @Override
+    public Type addCountAndDropIfBytesAtOffsetEqualsNoneOf(int offset, List<byte[]> bytesList,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addJumpIfBytesAtOffsetEqualsNoneOf(offset, bytesList, cnt.getJumpDropLabel());
+    }
+
+    @Override
+    public Type addCountAndPassIfBytesAtOffsetEqualsNoneOf(int offset, List<byte[]> bytesList,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addJumpIfBytesAtOffsetEqualsNoneOf(offset, bytesList, cnt.getJumpPassLabel());
+    }
+
+    @Override
+    public Type addCountAndPassIfBytesAtOffsetNotEqual(int offset, byte[] bytes,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addJumpIfBytesAtOffsetEqualsNoneOf(offset, List.of(bytes), cnt.getJumpPassLabel());
+    }
+
+    @Override
+    public Type addCountAndDropIfBytesAtOffsetNotEqual(int offset, byte[] bytes,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addJumpIfBytesAtOffsetEqualsNoneOf(offset, List.of(bytes), cnt.getJumpDropLabel());
+    }
+
+    @Override
+    public Type addCountAndPassIfBytesAtOffsetEqual(int offset, byte[] bytes,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addJumpIfBytesAtOffsetEqualsAnyOf(offset, List.of(bytes), cnt.getJumpPassLabel());
+    }
+
+    @Override
+    public Type addCountAndDropIfBytesAtOffsetEqual(int offset, byte[] bytes,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addJumpIfBytesAtOffsetEqualsAnyOf(offset, List.of(bytes), cnt.getJumpDropLabel());
+    }
+
+    @Override
+    public Type addJumpIfBytesAtOffsetNotEqual(int offset, @NonNull byte[] bytes, short tgt)
+            throws IllegalInstructionException {
+        return addJumpIfBytesAtOffsetEqualsNoneOf(offset, List.of(bytes), tgt);
+    }
+
     /**
      * Appends a conditional jump instruction to the program: Jumps to {@code tgt} if the UDP
      * payload's DNS questions contain the QNAMEs specified in {@code qnames} and qtype
@@ -211,4 +367,12 @@
         return append(new Instruction(ExtendedOpcodes.JDNSQMATCH2, Rbit1).setTargetLabel(tgt)
                 .addU8(qtype1).addU8(qtype2).setBytesImm(qnames));
     }
+
+    /**
+     * Preload the content of the data region.
+     */
+    public Type addPreloadData(@NonNull byte[] data) throws IllegalInstructionException {
+        mInstructions.get(0).maybeUpdateBytesImm(data, 0, data.length);
+        return self();
+    }
 }
diff --git a/src/android/net/apf/ApfV6Generator.java b/src/android/net/apf/ApfV6Generator.java
index ccd5490..07bd191 100644
--- a/src/android/net/apf/ApfV6Generator.java
+++ b/src/android/net/apf/ApfV6Generator.java
@@ -15,6 +15,7 @@
  */
 package android.net.apf;
 
+import static android.net.apf.BaseApfGenerator.Rbit.Rbit1;
 import static android.net.apf.BaseApfGenerator.Register.R0;
 
 import android.annotation.NonNull;
@@ -233,6 +234,46 @@
     }
 
     @Override
+    public ApfV6Generator addCountAndDropIfBytesAtOffsetEqualsAnyOf(int offset,
+            List<byte[]> bytesList, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException {
+        return addLoadImmediate(R0, offset).addCountAndDropIfBytesAtR0EqualsAnyOf(bytesList, cnt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndPassIfBytesAtOffsetEqualsAnyOf(int offset,
+            List<byte[]> bytesList, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException {
+        return addLoadImmediate(R0, offset).addCountAndPassIfBytesAtR0EqualsAnyOf(bytesList, cnt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndDropIfBytesAtOffsetEqualsNoneOf(int offset,
+            List<byte[]> bytesList, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException {
+        return addLoadImmediate(R0, offset).addCountAndDropIfBytesAtR0EqualsNoneOf(bytesList, cnt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndPassIfBytesAtOffsetEqualsNoneOf(int offset,
+            List<byte[]> bytesList, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException {
+        return addLoadImmediate(R0, offset).addCountAndPassIfBytesAtR0EqualsNoneOf(bytesList, cnt);
+    }
+
+    @Override
+    public ApfV6Generator addJumpIfBytesAtOffsetEqualsAnyOf(int offset, List<byte[]> bytesList,
+            short tgt) throws IllegalInstructionException {
+        return addLoadImmediate(R0, offset).addJumpIfBytesAtR0EqualsAnyOf(bytesList, tgt);
+    }
+
+    @Override
+    public ApfV6Generator addJumpIfBytesAtOffsetEqualsNoneOf(int offset, List<byte[]> bytesList,
+            short tgt) throws IllegalInstructionException {
+        return addLoadImmediate(R0, offset).addJumpIfBytesAtR0EqualsNoneOf(bytesList, tgt);
+    }
+
+    @Override
     public ApfV6Generator addCountAndDropIfR0IsNoneOf(@NonNull Set<Long> values,
             ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
         if (values.isEmpty()) {
@@ -252,4 +293,21 @@
         }
         return self();
     }
+
+    @Override
+    public ApfV6Generator addAllocate(int size) {
+        // Rbit1 means the extra be16 immediate is present
+        return append(new Instruction(ExtendedOpcodes.ALLOCATE, Rbit1).addU16(size));
+    }
+
+    @Override
+    public ApfV6Generator addTransmitWithoutChecksum() {
+        return addTransmit(-1 /* ipOfs */);
+    }
+
+    @Override
+    protected boolean handleOptimizedTransmit(int ipOfs, int csumOfs, int csumStart,
+                                              int partialCsum, boolean isUdp) {
+        return false;
+    }
 }
diff --git a/src/android/net/apf/ApfV6GeneratorBase.java b/src/android/net/apf/ApfV6GeneratorBase.java
index 658e4d1..90f0a28 100644
--- a/src/android/net/apf/ApfV6GeneratorBase.java
+++ b/src/android/net/apf/ApfV6GeneratorBase.java
@@ -114,10 +114,7 @@
      *
      * @param size the buffer length to be allocated.
      */
-    public final Type addAllocate(int size) {
-        // Rbit1 means the extra be16 immediate is present
-        return append(new Instruction(ExtendedOpcodes.ALLOCATE, Rbit1).addU16(size));
-    }
+    public abstract Type addAllocate(int size);
 
     /**
      * Add an instruction to the beginning of the program to reserve the empty data region.
@@ -153,9 +150,7 @@
      * Add an instruction to the end of the program to transmit the allocated buffer without
      * checksum.
      */
-    public final Type addTransmitWithoutChecksum() {
-        return addTransmit(-1 /* ipOfs */);
-    }
+    public abstract Type addTransmitWithoutChecksum();
 
     /**
      * Add an instruction to the end of the program to transmit the allocated buffer.
@@ -168,6 +163,8 @@
         return append(new Instruction(ExtendedOpcodes.TRANSMIT, Rbit0).addU8(ipOfs).addU8(255));
     }
 
+    protected abstract boolean handleOptimizedTransmit(int ipOfs, int csumOfs, int csumStart, int partialCsum, boolean isUdp);
+
     /**
      * Add an instruction to the end of the program to transmit the allocated buffer.
      */
@@ -181,6 +178,8 @@
             throw new IllegalArgumentException("L4 checksum requires csum offset of "
                                                + csumOfs + " < 255");
         }
+        if (handleOptimizedTransmit(ipOfs, csumOfs, csumStart, partialCsum, isUdp))
+            return self();
         return append(new Instruction(ExtendedOpcodes.TRANSMIT, isUdp ? Rbit1 : Rbit0)
                 .addU8(ipOfs).addU8(csumOfs).addU8(csumStart).addU16(partialCsum));
     }
@@ -397,6 +396,45 @@
             short tgt);
 
     /**
+     * Add an instruction to the end of the program to count and drop if the bytes of the
+     * packet at an offset specified by {@code offset} match any of the elements in
+     * {@code bytesList}.
+     * This method will use JBSPTRMATCH in APFv6.1 when possible.
+     */
+    public abstract Type addCountAndDropIfBytesAtOffsetEqualsAnyOf(int offset,
+            @NonNull List<byte[]> bytesList, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException;
+
+    /**
+     * Add an instruction to the end of the program to count and pass if the bytes of the
+     * packet at an offset specified by {@code offset} match any of the elements in
+     * {@code bytesList}.
+     * This method will use JBSPTRMATCH in APFv6.1 when possible.
+     */
+    public abstract Type addCountAndPassIfBytesAtOffsetEqualsAnyOf(int offset,
+            @NonNull List<byte[]> bytesList, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException;
+
+    /**
+     * Add an instruction to the end of the program to count and drop if the bytes of the
+     * packet at an offset specified by {@code offset} match none the elements in {@code bytesList}.
+     * This method will use JBSPTRMATCH in APFv6.1 when possible.
+     */
+    public abstract Type addCountAndDropIfBytesAtOffsetEqualsNoneOf(int offset,
+            @NonNull List<byte[]> bytesList, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException;
+
+    /**
+     * Add an instruction to the end of the program to count and pass if the bytes of the
+     * packet at an offset specified by {@code offset} match none of the elements in
+     * {@code bytesList}.
+     * This method will use JBSPTRMATCH in APFv6.1 when possible.
+     */
+    public abstract Type addCountAndPassIfBytesAtOffsetEqualsNoneOf(int offset,
+            @NonNull List<byte[]> bytesList, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException;
+
+    /**
      * Appends a conditional jump instruction to the program: Jumps to {@code tgt} if the UDP
      * payload's DNS questions contain the QNAMEs specified in {@code qnames} and qtype
      * equals {@code qtype}. Examines the payload starting at the offset in R0.
@@ -514,6 +552,21 @@
         return addJumpIfBytesAtR0EqualsHelper(bytesList, tgt, false /* jumpOnMatch */);
     }
 
+    /**
+     * 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 offset} match any of the elements in
+     * {@code bytesSet}.
+     */
+    public abstract Type addJumpIfBytesAtOffsetEqualsAnyOf(int offset,
+            @NonNull List<byte[]> bytesList, short tgt) throws IllegalInstructionException;
+
+    /**
+     * 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 offset} match none of the elements in
+     * {@code bytesSet}.
+     */
+    public abstract Type addJumpIfBytesAtOffsetEqualsNoneOf(int offset,
+            @NonNull List<byte[]> bytesList, short tgt) throws IllegalInstructionException;
 
     /**
      * Check if the byte is valid dns character: A-Z,0-9,-,_,%,@
diff --git a/src/android/net/apf/BaseApfGenerator.java b/src/android/net/apf/BaseApfGenerator.java
index c505f5d..21d8be3 100644
--- a/src/android/net/apf/BaseApfGenerator.java
+++ b/src/android/net/apf/BaseApfGenerator.java
@@ -106,7 +106,33 @@
         // 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);
+        PKTDATACOPY(25),
+        // JSET with reverse condition (jump if no bits set)
+        JNSET(26),
+        // APFv6.1: Compare byte sequence [R=0 not] equal, e.g. "jbsptrne 22,16,label,<dataptr>"
+        // imm1 is jmp target
+        // imm2(u8) is offset [0..255] into packet
+        // imm3(u8) is (count - 1) * 16 + (compare_len - 1), thus both count & compare_len are in
+        // [1..16] which is followed by compare_len u8 'even offset' ptrs into max 526 byte data
+        // section to compare against - ie. they are multipied by 2 and have 3 added to them
+        // (to skip over 'datajmp u16')
+        // Warning: do not specify the same byte sequence multiple times.
+        JBSPTRMATCH(27),
+        // APFv6.1: Bytecode optimized allocate | transmit instruction.
+        // R=1 -> allocate(266 + imm * 8)
+        // R=0 -> transmit
+        //   immlen=0 -> no checksum offload (transmit ip_ofs=255)
+        //   immlen>0 -> with checksum offload (transmit(udp) ip_ofs=14 ...)
+        //     imm & 7 | type of offload      | ip_ofs | udp | csum_start  | csum_ofs      | partial_csum |
+        //         0   | ip4/udp              |   14   |  X  | 14+20-8 =26 | 14+20   +6=40 |   imm >> 3   |
+        //         1   | ip4/tcp              |   14   |     | 14+20-8 =26 | 14+20  +10=44 |     --"--    |
+        //         2   | ip4/icmp             |   14   |     | 14+20   =34 | 14+20   +2=36 |     --"--    |
+        //         3   | ip4/routeralert/icmp |   14   |     | 14+20+4 =38 | 14+20+4 +2=40 |     --"--    |
+        //         4   | ip6/udp              |   14   |  X  | 14+40-32=22 | 14+40   +6=60 |     --"--    |
+        //         5   | ip6/tcp              |   14   |     | 14+40-32=22 | 14+40  +10=64 |     --"--    |
+        //         6   | ip6/icmp             |   14   |     | 14+40-32=22 | 14+40   +2=56 |     --"--    |
+        //         7   | ip6/routeralert/icmp |   14   |     | 14+40-32=22 | 14+40+8 +2=64 |     --"--    |
+        ALLOC_XMIT(28);
 
         final int value;
 
@@ -496,7 +522,23 @@
             return this;
         }
 
-        private int findMatchInDataBytes(@NonNull byte[] content, int fromIndex, int toIndex) {
+        int findMatchInDataBytes(@NonNull byte[] content, int fromIndex, int toIndex)
+                throws IllegalInstructionException {
+            if (fromIndex >= toIndex || fromIndex < 0 || toIndex > content.length) {
+                throw new IllegalArgumentException(
+                        String.format("fromIndex: %d, toIndex: %d, content length: %d", fromIndex,
+                                toIndex, content.length));
+            }
+            if (mOpcode != Opcodes.JMP || mBytesImm == null) {
+                throw new IllegalInstructionException(String.format(
+                        "this method is only valid for jump data instruction, mOpcode "
+                                + ":%s, mBytesImm: %s", Opcodes.JMP,
+                        mBytesImm == null ? "(empty)" : HexDump.toHexString(mBytesImm)));
+            }
+            if (mImmSizeOverride != 2) {
+                throw new IllegalInstructionException(
+                        "mImmSizeOverride must be 2, mImmSizeOverride: " + mImmSizeOverride);
+            }
             final int subArrayLength = toIndex - fromIndex;
             for (int i = 0; i < mBytesImm.length - subArrayLength + 1; i++) {
                 boolean found = true;
@@ -532,21 +574,6 @@
          */
         int maybeUpdateBytesImm(byte[] content, int fromIndex, int toIndex)
                 throws IllegalInstructionException {
-            if (fromIndex >= toIndex || fromIndex < 0 || toIndex > content.length) {
-                throw new IllegalArgumentException(
-                        String.format("fromIndex: %d, toIndex: %d, content length: %d", fromIndex,
-                                toIndex, content.length));
-            }
-            if (mOpcode != Opcodes.JMP || mBytesImm == null) {
-                throw new IllegalInstructionException(String.format(
-                        "maybeUpdateBytesImm() is only valid for jump data instruction, mOpcode "
-                                + ":%s, mBytesImm: %s", Opcodes.JMP,
-                        mBytesImm == null ? "(empty)" : HexDump.toHexString(mBytesImm)));
-            }
-            if (mImmSizeOverride != 2) {
-                throw new IllegalInstructionException(
-                        "mImmSizeOverride must be 2, mImmSizeOverride: " + mImmSizeOverride);
-            }
             int offsetInDataBytes = findMatchInDataBytes(content, fromIndex, toIndex);
             if (offsetInDataBytes == -1) {
                 offsetInDataBytes = mBytesImm.length;
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java
index eaed8e5..3ae8557 100755
--- a/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -517,6 +517,8 @@
     private final boolean mIsCaptivePortalCheckEnabled;
 
     private boolean mUseHttps;
+    private final boolean mUseSerialProbe;
+    private final int mSerialProbeGapTime;
     /**
      * The total number of completed validation attempts (network validated or a captive portal was
      * detected) for this NetworkMonitor instance.
@@ -679,6 +681,8 @@
                 && deps.isFeatureSupported(mContext, FEATURE_DDR_IN_CONNECTIVITY)
                 && deps.isFeatureSupported(mContext, FEATURE_DDR_IN_DNSRESOLVER);
         mUseHttps = getUseHttpsValidation();
+        mUseSerialProbe = getUseSerialProbeValidation();
+        mSerialProbeGapTime = getSerialProbeGapTime();
         mCaptivePortalUserAgent = getCaptivePortalUserAgent();
         mCaptivePortalFallbackSpecs =
                 makeCaptivePortalFallbackProbeSpecs(getCustomizedContextOrDefault());
@@ -2392,6 +2396,16 @@
                         R.bool.config_force_dns_probe_private_ip_no_internet);
     }
 
+    private boolean getUseSerialProbeValidation() {
+        return mContext.getResources().getBoolean(
+                R.bool.config_probe_multi_http_https_url_serial);
+    }
+
+    private int getSerialProbeGapTime() {
+        return mContext.getResources().getInteger(
+                R.integer.config_serial_url_probe_gap_time);
+    }
+
     private boolean getUseHttpsValidation() {
         return mDependencies.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY,
                 CAPTIVE_PORTAL_USE_HTTPS, 1) == 1;
@@ -3426,14 +3440,26 @@
             // Probe capport API with the first HTTP probe.
             // TODO: Have the capport probe as a different probe for cleanliness.
             final URL urlMaybeWithCapport = httpUrls[0];
+            int delayCount=0;
             for (final URL url : httpUrls) {
-                futures.add(ecs.submit(() -> new HttpProbe(properties, proxy, url,
-                        url.equals(urlMaybeWithCapport) ? capportApiUrl : null).sendProbe()));
+                final int cnt = delayCount++;
+                futures.add(ecs.submit(() -> {
+                    if (mUseSerialProbe && cnt > 0) {
+                        mDependencies.sleep(mSerialProbeGapTime * cnt);
+                    }
+                    return new HttpProbe(properties, proxy, url,
+                            url.equals(urlMaybeWithCapport) ? capportApiUrl : null).sendProbe();
+                }));
             }
-
+            delayCount=0;
             for (final URL url : httpsUrls) {
-                futures.add(ecs.submit(() -> new HttpsProbe(properties, proxy, url, capportApiUrl)
-                        .sendProbe()));
+                final int cnt = delayCount++;
+                futures.add(ecs.submit(() -> {
+                    if (mUseSerialProbe && cnt > 0) {
+                        mDependencies.sleep(mSerialProbeGapTime * cnt);
+                    }
+                    return new HttpsProbe(properties, proxy, url, capportApiUrl).sendProbe();
+                }));
             }
 
             final ArrayList<CaptivePortalProbeResult> completedProbes = new ArrayList<>();
@@ -3778,6 +3804,13 @@
         public void onExecutorServiceCreated(@NonNull ExecutorService ecs) {
         }
 
+        /**
+         * Wait for another round of serial probe
+         */
+        public void sleep(int time) throws InterruptedException {
+            Thread.sleep((long)time);
+        }
+
         public static final Dependencies DEFAULT = new Dependencies();
     }
 
diff --git a/tests/unit/src/android/net/apf/ApfFilterTest.kt b/tests/unit/src/android/net/apf/ApfFilterTest.kt
index fc612a2..5cfeaad 100644
--- a/tests/unit/src/android/net/apf/ApfFilterTest.kt
+++ b/tests/unit/src/android/net/apf/ApfFilterTest.kt
@@ -22,6 +22,8 @@
 import android.net.MacAddress
 import android.net.NattKeepalivePacketDataParcelable
 import android.net.TcpKeepalivePacketDataParcelable
+import android.net.apf.ApfConstants.ETH_MULTICAST_MDNS_V4_MAC_ADDRESS
+import android.net.apf.ApfConstants.ETH_MULTICAST_MDNS_V6_MAC_ADDRESS
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_NON_IPV4
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_OTHER_HOST
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_REPLY_SPA_NO_HOST
@@ -240,6 +242,8 @@
         intArrayOf(0x33, 0x33, 0xff, 0x55, 0x66, 0x77).map { it.toByte() }.toByteArray(),
         // 33:33:ff:bb:cc:dd
         intArrayOf(0x33, 0x33, 0xff, 0xbb, 0xcc, 0xdd).map { it.toByte() }.toByteArray(),
+        ETH_MULTICAST_MDNS_V4_MAC_ADDRESS,
+        ETH_MULTICAST_MDNS_V6_MAC_ADDRESS
     )
 
     // Using scapy to generate payload:
@@ -5566,6 +5570,18 @@
             val apfFilter = getApfFilter(apfConfig)
             apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
 
+            val srcAddr = byteArrayOf(10, 0, 0, 5)
+            val dstAddr = byteArrayOf(10, 0, 0, 6)
+            val srcPort = 1024
+            val dstPort = 4500
+            val parcel = NattKeepalivePacketDataParcelable()
+            parcel.srcAddress = InetAddress.getByAddress(srcAddr).address
+            parcel.srcPort = srcPort
+            parcel.dstAddress = InetAddress.getByAddress(dstAddr).address
+            parcel.dstPort = dstPort
+            apfFilter.addNattKeepalivePacketFilter(1, parcel)
+            apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+
             val captor = ArgumentCaptor.forClass(OffloadEngine::class.java)
             verify(localNsdManager).registerOffloadEngine(
                 eq(ifParams.name),
@@ -5785,7 +5801,8 @@
             )
             assertThat(program.size).isLessThan(apfRamSize + 1)
             assertThat(program).isNotEqualTo(ByteArray(apfRamSize) { 0 })
-            val step = Random.nextInt(1, 16)
+            // TODO: reduce after fixing 'Failed to receive adb shell test output within 66000 ms'
+            val step = Random.nextInt(1, 64)
             apfRamSize += step
         }
     }
@@ -5805,7 +5822,8 @@
             val availableRam = apfRamSize - ApfCounterTracker.Counter.totalSize()
             assertThat(program.size).isLessThan(availableRam + 1)
             assertThat(program).isNotEqualTo(ByteArray(availableRam) { 0 })
-            val step = Random.nextInt(1, 16)
+            // TODO: reduce after fixing 'Failed to receive adb shell test output within 66000 ms'
+            val step = Random.nextInt(1, 64)
             apfRamSize += step
         }
     }
@@ -5824,81 +5842,102 @@
             val availableRam = apfRamSize - ApfCounterTracker.Counter.totalSize()
             assertThat(program.size).isLessThan(availableRam + 1)
             assertThat(program).isNotEqualTo(ByteArray(availableRam) { 0 })
-            val step = Random.nextInt(1, 16)
+            // TODO: reduce after fixing 'Failed to receive adb shell test output within 66000 ms'
+            val step = Random.nextInt(1, 64)
             apfRamSize += step
         }
     }
 
+    private fun getProgramForRaSizeEstimation(
+        apfRamSize: Int,
+    ): Pair<Int, ByteArray> {
+        val localRaWriterSocket = FileDescriptor()
+        val localRaReaderSocket = FileDescriptor()
+        Os.socketpair(AF_UNIX, SOCK_STREAM, 0, localRaWriterSocket, localRaReaderSocket)
+        doReturn(localRaReaderSocket).`when`(dependencies).createPacketReaderSocket(anyInt())
+        var overEstimatedProgramSize = 0
+        var program = byteArrayOf(0)
+        tryTest {
+            val apfConfig = getDefaultConfig()
+            apfConfig.apfRamSize = apfRamSize
+            val apfFilter = getApfFilter(apfConfig)
+            apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
+
+            val lp = LinkProperties()
+            val ipv4LinkAddress = LinkAddress(InetAddress.getByAddress(hostIpv4Address), 24)
+            lp.addLinkAddress(ipv4LinkAddress)
+            val ipv6LinkAddress = LinkAddress(hostLinkLocalIpv6Address, 64)
+            lp.addLinkAddress(ipv6LinkAddress)
+            for (addr in hostIpv6Addresses) {
+                lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
+            }
+            apfFilter.setLinkProperties(lp)
+            program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+
+            val ra1 = """
+                333300000001f434f06452fe86dd60010c0000503afffe800000000000001cb6b5bc353b7cfdff0
+                2000000000000000000000000000186000fab000000000000000000000000030440c00000070800
+                00070800000000fdeed0c47546534400000000000000001802400000000708fd0c8be643ee00001
+                a018000000000000101f434f06452fe
+            """.replace("\\s+".toRegex(), "").trim()
+            val ra1Bytes = HexDump.hexStringToByteArray(ra1)
+            Os.write(localRaWriterSocket, ra1Bytes, 0, ra1Bytes.size)
+            apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+
+            // Using scapy to generate packet:
+            // eth = Ether(src="E8:9F:80:66:60:BC", dst="f2:9c:70:2c:39:5a")
+            // ip6 = IPv6(src="fe80::2", dst="ff02::1")
+            // icmpra = ICMPv6ND_RA(routerlifetime=360, retranstimer=360)
+            // pio1 = ICMPv6NDOptPrefixInfo(prefixlen=64, prefix="2002:db8::")
+            // rio = ICMPv6NDOptRouteInfo(prefix="2002:db8:cafe::")
+            // ra = eth/ip6/icmpra/pio1/rio
+            val ra2 = """
+                f29c702c395ae89f806660bc86dd6000000000483afffe800000000000000000000000000002ff0
+                200000000000000000000000000018600f6e3000801680000000000000168030440c0ffffffffff
+                ffffff0000000020020db800000000000000000000000018030000ffffffff20020db8cafe00000
+                000000000000000
+            """.replace("\\s+".toRegex(), "").trim()
+            val ra2Bytes = HexDump.hexStringToByteArray(ra2)
+            Os.write(localRaWriterSocket, ra2Bytes, 0, ra2Bytes.size)
+            program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+            overEstimatedProgramSize = apfFilter.overEstimatedProgramSize
+        } cleanup {
+            IoUtils.closeQuietly(localRaWriterSocket)
+        }
+        return Pair(overEstimatedProgramSize, program)
+    }
+
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Test
     fun testRaFilterSizeEstimation() {
-        val apfConfig = getDefaultConfig()
-        apfConfig.apfRamSize = 1500
-        val apfFilter = getApfFilter(apfConfig)
-        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
+        val (overEstimatedProgramSize, _) = getProgramForRaSizeEstimation(apfRamSize = 8096)
+        val apfRam = overEstimatedProgramSize - 1
+        val (_, program) = getProgramForRaSizeEstimation(apfRamSize = apfRam)
 
-        val lp = LinkProperties()
-        val ipv4LinkAddress = LinkAddress(InetAddress.getByAddress(hostIpv4Address), 24)
-        lp.addLinkAddress(ipv4LinkAddress)
-        val ipv6LinkAddress = LinkAddress(hostLinkLocalIpv6Address, 64)
-        lp.addLinkAddress(ipv6LinkAddress)
-        for (addr in hostIpv6Addresses) {
-            lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
-        }
-        apfFilter.setLinkProperties(lp)
-        var program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
-
-        // Using scapy to generate packet:
-        // eth = Ether(src="E8:9F:80:66:60:BB", dst="f2:9c:70:2c:39:5a")
-        // ip6 = IPv6(src="fe80::1", dst="ff02::1")
-        // icmpra = ICMPv6ND_RA(routerlifetime=3600, retranstimer=3600)
-        // pio1 = ICMPv6NDOptPrefixInfo(prefixlen=64, prefix="2001:db8::")
-        // rio = ICMPv6NDOptRouteInfo(prefix="2001:db8:cafe::")
-        // ra = eth/ip6/icmpra/pio1/rio
         val ra1 = """
-            f29c702c395ae89f806660bb86dd6000000000483afffe800000000000000000000000000001ff0
-            200000000000000000000000000018600dd9600080e100000000000000e10030440c0ffffffffff
-            ffffff0000000020010db800000000000000000000000018030000ffffffff20010db8cafe00000
-            000000000000000
+            333300000001f434f06452fe86dd60010c0000503afffe800000000000001cb6b5bc353b7cfdff0
+            2000000000000000000000000000186000fab000000000000000000000000030440c00000070800
+            00070800000000fdeed0c47546534400000000000000001802400000000708fd0c8be643ee00001
+            a018000000000000101f434f06452fe
         """.replace("\\s+".toRegex(), "").trim()
-        val ra1Bytes = HexDump.hexStringToByteArray(ra1)
-        Os.write(raWriterSocket, ra1Bytes, 0, ra1Bytes.size)
 
-        program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
-        apfTestHelpers.verifyProgramRun(
-            apfFilter.mApfVersionSupported,
-            program,
-            ra1Bytes,
-            DROPPED_RA
-        )
-
-        // Using scapy to generate packet:
-        // eth = Ether(src="E8:9F:80:66:60:BC", dst="f2:9c:70:2c:39:5a")
-        // ip6 = IPv6(src="fe80::2", dst="ff02::1")
-        // icmpra = ICMPv6ND_RA(routerlifetime=360, retranstimer=360)
-        // pio1 = ICMPv6NDOptPrefixInfo(prefixlen=64, prefix="2002:db8::")
-        // rio = ICMPv6NDOptRouteInfo(prefix="2002:db8:cafe::")
-        // ra = eth/ip6/icmpra/pio1/rio
         val ra2 = """
             f29c702c395ae89f806660bc86dd6000000000483afffe800000000000000000000000000002ff0
             200000000000000000000000000018600f6e3000801680000000000000168030440c0ffffffffff
             ffffff0000000020020db800000000000000000000000018030000ffffffff20020db8cafe00000
             000000000000000
         """.replace("\\s+".toRegex(), "").trim()
-        val ra2Bytes = HexDump.hexStringToByteArray(ra2)
-        Os.write(raWriterSocket, ra2Bytes, 0, ra2Bytes.size)
 
-        program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
         apfTestHelpers.verifyProgramRun(
-            apfFilter.mApfVersionSupported,
+            apfInterpreterVersion,
             program,
-            ra2Bytes,
+            HexDump.hexStringToByteArray(ra2),
             DROPPED_RA
         )
         apfTestHelpers.verifyProgramRun(
-            apfFilter.mApfVersionSupported,
+            apfInterpreterVersion,
             program,
-            ra1Bytes,
+            HexDump.hexStringToByteArray(ra1),
             PASSED_IPV6_ICMP
         )
     }
diff --git a/tests/unit/src/android/net/apf/ApfGeneratorTest.kt b/tests/unit/src/android/net/apf/ApfGeneratorTest.kt
index 03d360f..1c383bc 100644
--- a/tests/unit/src/android/net/apf/ApfGeneratorTest.kt
+++ b/tests/unit/src/android/net/apf/ApfGeneratorTest.kt
@@ -987,6 +987,95 @@
     }
 
     @Test
+    fun testJBSPTRMATCHOpcodeEncoding() {
+        assumeTrue(apfInterpreterVersion != ApfJniUtils.APF_INTERPRETER_VERSION_V6)
+        val dataBytes = HexDump.hexStringToByteArray(
+            "01020304050607080910111213141516171819202122232425262728293031323334353637383940"
+        )
+        val bytes1 = HexDump.hexStringToByteArray("0102")
+        val bytes2 = HexDump.hexStringToByteArray("0304")
+        val bytes3 = HexDump.hexStringToByteArray("0506")
+        val bytes4 = HexDump.hexStringToByteArray("0708")
+        val bytes5 = HexDump.hexStringToByteArray("0910")
+        val bytes6 = HexDump.hexStringToByteArray("1112")
+        val bytes7 = HexDump.hexStringToByteArray("1314")
+        val bytes8 = HexDump.hexStringToByteArray("1516")
+        val bytes9 = HexDump.hexStringToByteArray("1718")
+        val bytes10 = HexDump.hexStringToByteArray("1920")
+        val bytes11 = HexDump.hexStringToByteArray("2122")
+        val bytes12 = HexDump.hexStringToByteArray("2324")
+        val bytes13 = HexDump.hexStringToByteArray("2526")
+        val bytes14 = HexDump.hexStringToByteArray("2728")
+        val bytes15 = HexDump.hexStringToByteArray("2930")
+        val bytes16 = HexDump.hexStringToByteArray("3132")
+        val bytes17 = HexDump.hexStringToByteArray("3334")
+        val bytesAtOddIndex = HexDump.hexStringToByteArray("0203")
+        val notExistBytes = HexDump.hexStringToByteArray("ffff")
+        val total17BytesList = listOf(
+            bytes1,
+            bytes2,
+            bytes3,
+            bytes4,
+            bytes5,
+            bytes6,
+            bytes7,
+            bytes8,
+            bytes9,
+            bytes10,
+            bytes11,
+            bytes12,
+            bytes13,
+            bytes14,
+            bytes15,
+            bytes16,
+            bytes17,
+        )
+        val joinedBytes: ByteArray = total17BytesList.flatMap { it.toList() }.toByteArray()
+        var program = ApfV61Generator(apfInterpreterVersion, ramSize, clampSize)
+            .addPreloadData(dataBytes)
+            .addJumpIfBytesAtOffsetEqualsNoneOf(0, listOf(bytes1, notExistBytes), PASS_LABEL)
+            .addJumpIfBytesAtOffsetEqualsAnyOf(1, listOf(bytes1, bytes2), PASS_LABEL)
+            .addJumpIfBytesAtOffsetEqualsNoneOf(2, listOf(notExistBytes), PASS_LABEL)
+            .addJumpIfBytesAtOffsetEqualsAnyOf(3, total17BytesList, PASS_LABEL)
+            .addJumpIfBytesAtOffsetEqualsNoneOf(4, listOf(bytesAtOddIndex), PASS_LABEL)
+            .addJumpIfBytesAtOffsetEqualsNoneOf(6, listOf(joinedBytes), PASS_LABEL)
+            .generate()
+        var debugBufferSize = ramSize - program.size - Counter.totalSize()
+        assertContentEquals(listOf(
+            "0: data        40, ${HexDump.toHexString(dataBytes)}",
+            "43: debugbuf    size=$debugBufferSize",
+            "47: jbsptrne    pktofs=0, (2), PASS, @0[0102]",
+            "52: li          r0, 0",
+            "53: jbsne       r0, (2), PASS, ffff",
+            "58: jbsptreq    pktofs=1, (2), PASS, { @0[0102], @2[0304] }[2]",
+            "64: li          r0, 2",
+            "66: jbsne       r0, (2), PASS, ffff",
+            "71: jbsptreq    pktofs=3, (2), PASS, { @0[0102], @2[0304], @4[0506], @6[0708], " +
+                "@8[0910], @10[1112], @12[1314], @14[1516], @16[1718], @18[1920], @20[2122], " +
+                "@22[2324], @24[2526], @26[2728], @28[2930], @30[3132] }[16]",
+            "91: jbsptreq    pktofs=3, (2), PASS, @32[3334]",
+            "96: li          r0, 4",
+            "98: jbsne       r0, (2), PASS, 0203",
+            "103: li          r0, 6",
+            "105: jbsne       r0, (34), PASS, ${HexDump.toHexString(joinedBytes)}",
+        ), apfTestHelpers.disassembleApf(program).map{ it.trim() })
+
+        val largePrefix = ByteArray(510) { 0 }
+        program = ApfV61Generator(apfInterpreterVersion, ramSize, clampSize)
+            .addPreloadData(largePrefix + dataBytes)
+            .addJumpIfBytesAtOffsetEqualsAnyOf(1, listOf(bytes1, bytes2), PASS_LABEL)
+            .generate()
+        debugBufferSize = ramSize - program.size - Counter.totalSize()
+        assertContentEquals(listOf(
+            "0: data        550, ${HexDump.toHexString(largePrefix + dataBytes)}",
+            "553: debugbuf    size=$debugBufferSize",
+            "557: jbsptreq    pktofs=1, (2), PASS, @510[0102]",
+            "562: li          r0, 1",
+            "564: jbseq       r0, (2), PASS, 0304",
+        ), apfTestHelpers.disassembleApf(program).map{ it.trim() })
+    }
+
+    @Test
     fun testPassDrop() {
         var program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addDrop()
diff --git a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
index 37b157b..e9bd616 100644
--- a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
+++ b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
@@ -226,6 +226,7 @@
 import java.util.Random;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Predicate;
 
@@ -320,6 +321,7 @@
     private static final int DEFAULT_DNS_TIMEOUT_THRESHOLD = 5;
 
     private static final int HANDLER_TIMEOUT_MS = 1000;
+    private static final int SERIAL_PROBE_GAP_TIME_MS = 500;
     private static final int TEST_MIN_STALL_EVALUATE_INTERVAL_MS = 500;
     private static final int TEST_MIN_VALID_STALL_DNS_TIME_THRESHOLD_MS = 5000;
     private static final int STALL_EXPECTED_LAST_PROBE_TIME_MS =
@@ -373,6 +375,7 @@
                 .removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
 
     private FakeDns mFakeDns;
+    private Semaphore mSerialProbeLock;
 
     @GuardedBy("mThreadsToBeCleared")
     private final ArrayList<Thread> mThreadsToBeCleared = new ArrayList<>();
@@ -474,6 +477,7 @@
         initHttpConnection(mFallbackConnection);
         initHttpConnection(mOtherFallbackConnection);
 
+        mSerialProbeLock = new Semaphore(0);
         mFakeDns = new FakeDns(mNetwork, mDnsResolver);
         mFakeDns.startMocking();
         // Set private dns suffix answer. sendPrivateDnsProbe() in NetworkMonitor send probe with
@@ -3417,6 +3421,36 @@
     }
 
     @Test
+    public void testSerialProbesOnFirstValidNetwork() throws Exception {
+        setupResourceForSerialProbes();
+        setStatus(mOtherHttpsConnection1, 204);
+        runSerialProbesNetworkTest(NETWORK_VALIDATION_RESULT_VALID,
+                NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS);
+        verify(mDependencies, timeout(HANDLER_TIMEOUT_MS).times(2)).sleep(anyInt());
+        verify(mCleartextDnsNetwork, timeout(HANDLER_TIMEOUT_MS).times(2)).openConnection(any());
+    }
+
+    @Test
+    public void testSerialProbesOnSecondValidNetwork() throws Exception {
+        setupResourceForSerialProbes();
+        setStatus(mOtherHttpsConnection2, 204);
+        mSerialProbeLock.release(2);
+        runSerialProbesNetworkTest(NETWORK_VALIDATION_RESULT_VALID,
+                NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS);
+        verify(mDependencies, timeout(HANDLER_TIMEOUT_MS).times(2)).sleep(anyInt());
+        verify(mCleartextDnsNetwork, timeout(HANDLER_TIMEOUT_MS).times(4)).openConnection(any());
+    }
+
+    @Test
+    public void testSerialProbesOnInValidNetwork() throws Exception {
+        setupResourceForSerialProbes();
+        mSerialProbeLock.release(2);
+        runSerialProbesNetworkTest(VALIDATION_RESULT_INVALID, 0);
+        verify(mDependencies, timeout(HANDLER_TIMEOUT_MS).times(2)).sleep(anyInt());
+        verify(mCleartextDnsNetwork, timeout(HANDLER_TIMEOUT_MS).times(4)).openConnection(any());
+    }
+
+    @Test
     public void testIsCaptivePortal_FromExternalSource() throws Exception {
         assumeTrue(CaptivePortalDataShimImpl.isSupported());
         assumeTrue(ShimUtils.isAtLeastS());
@@ -3523,6 +3557,18 @@
         verify(mCallbacks, never()).showProvisioningNotification(any(), any());
     }
 
+    private void waitForSerialProbes(int time) throws InterruptedException {
+        mSerialProbeLock.tryAcquire(time, TimeUnit.MILLISECONDS);
+    }
+
+    private void setupResourceForSerialProbes() {
+        doReturn(true).when(mResources)
+                .getBoolean(R.bool.config_probe_multi_http_https_url_serial);
+        doReturn(SERIAL_PROBE_GAP_TIME_MS).when(mResources)
+                .getInteger(R.integer.config_serial_url_probe_gap_time);
+        setupResourceForMultipleProbes();
+    }
+
     private void setupResourceForMultipleProbes() {
         // Configure the resource to send multiple probe.
         doReturn(TEST_HTTPS_URLS).when(mResources)
@@ -3620,6 +3666,17 @@
         return nm;
     }
 
+    private void runSerialProbesNetworkTest(int testResult, int probesSucceeded) throws Exception {
+        final WrappedNetworkMonitor monitor = makeMonitor(CELL_METERED_CAPABILITIES);
+        notifyNetworkConnected(monitor, TEST_AGENT_CONFIG,
+                TEST_LINK_PROPERTIES, CELL_METERED_CAPABILITIES);
+        doAnswer(invocation -> {
+            waitForSerialProbes(invocation.getArgument(0));
+            return null;
+        }).when(mDependencies).sleep(anyInt());
+        verifyNetworkTested(testResult, probesSucceeded, 1);
+    }
+
     private NetworkMonitor runPartialConnectivityNetworkTest(int probesSucceeded)
             throws Exception {
         final NetworkMonitor nm = runNetworkTest(NETWORK_VALIDATION_RESULT_PARTIAL,