Snap for 11391017 from f2b16d3294f249f759e7e6ee2abe42917b6a7018 to mainline-media-release

Change-Id: I62defc27ba6ac5abe9fb6c83d867a4f21ab434ed
diff --git a/src/android/net/apf/ApfV4Generator.java b/src/android/net/apf/ApfV4Generator.java
index 970c887..bc48bef 100644
--- a/src/android/net/apf/ApfV4Generator.java
+++ b/src/android/net/apf/ApfV4Generator.java
@@ -22,7 +22,6 @@
 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;
@@ -45,7 +44,7 @@
             super(msg);
         }
     }
-    private enum Opcodes {
+    enum Opcodes {
         LABEL(-1),
         // Unconditionally pass (if R=0) or drop (if R=1) packet.
         // An optional unsigned immediate value can be provided to encode the counter number.
@@ -54,8 +53,7 @@
         // It is a U32 big-endian value and is always incremented by 1.
         // This is more or less equivalent to: lddw R0, -N4; add R0,1; stdw R0, -N4; {pass,drop}
         // e.g. "pass", "pass 1", "drop", "drop 1"
-        PASS(0),
-        DROP(0),
+        PASSDROP(0),
         LDB(1),    // Load 1 byte from immediate offset, e.g. "ldb R0, [5]"
         LDH(2),    // Load 2 bytes from immediate offset, e.g. "ldh R0, [5]"
         LDW(3),    // Load 4 bytes from immediate offset, e.g. "ldw R0, [5]"
@@ -104,7 +102,7 @@
     }
     // Extended opcodes. Primary opcode is Opcodes.EXT. ExtendedOpcodes are encoded in the immediate
     // field.
-    private enum ExtendedOpcodes {
+    enum ExtendedOpcodes {
         LDM(0),   // Load from memory, e.g. "ldm R0,5"
         STM(16),  // Store to memory, e.g. "stm R0,5"
         NOT(32),  // Not, e.g. "not R0"
@@ -118,10 +116,9 @@
         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. transmit"
         // "e.g. discard"
-        TRANSMIT(37),
-        DISCARD(37),
+        TRANSMITDISCARD(37),
         // Write 1, 2 or 4 byte value from register to the output buffer and auto-increment the
         // output buffer pointer.
         // e.g. "ewrite1 r0"
@@ -297,7 +294,7 @@
         }
     }
 
-    private class Instruction {
+    class Instruction {
         private final byte mOpcode;   // A "Opcode" value.
         private final byte mRegister; // A "Register" value.
         public final List<IntImmediate> mIntImms = new ArrayList<>();
@@ -630,7 +627,7 @@
     public static final int APF_VERSION_4 = 4;
 
 
-    private final ArrayList<Instruction> mInstructions = new ArrayList<Instruction>();
+    final ArrayList<Instruction> mInstructions = new ArrayList<Instruction>();
     private final HashMap<String, Instruction> mLabels = new HashMap<String, Instruction>();
     private final Instruction mDropLabel = new Instruction(Opcodes.LABEL);
     private final Instruction mPassLabel = new Instruction(Opcodes.LABEL);
@@ -655,13 +652,13 @@
         return version >= MIN_APF_VERSION;
     }
 
-    private void requireApfVersion(int minimumVersion) throws IllegalInstructionException {
+    void requireApfVersion(int minimumVersion) throws IllegalInstructionException {
         if (mVersion < minimumVersion) {
             throw new IllegalInstructionException("Requires APF >= " + minimumVersion);
         }
     }
 
-    private ApfV4Generator append(Instruction instruction) {
+    ApfV4Generator append(Instruction instruction) {
         if (mGenerated) {
             throw new IllegalStateException("Program already generated");
         }
@@ -1003,322 +1000,11 @@
      */
     public ApfV4Generator addPass() {
         // PASS requires using R0 because it shares opcode with DROP
-        return append(new Instruction(Opcodes.PASS));
+        return append(new Instruction(Opcodes.PASSDROP));
     }
 
-    /**
-     * Add an instruction to the end of the program to increment the counter value and
-     * immediately return PASS.
-     */
-    public ApfV4Generator addCountAndPass(int cnt) throws IllegalInstructionException {
-        requireApfVersion(MIN_APF_VERSION_IN_DEV);
-        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));
-    }
-
-    /**
-     * Add an instruction to the end of the program to let the program immediately return DROP.
-     */
-    public ApfV4Generator addDrop() throws IllegalInstructionException {
-        requireApfVersion(MIN_APF_VERSION_IN_DEV);
-        // 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 ApfV4Generator addCountAndDrop(int cnt) throws IllegalInstructionException {
-        requireApfVersion(MIN_APF_VERSION_IN_DEV);
-        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 ApfV4Generator 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 size the buffer length to be allocated.
-     */
-    public ApfV4Generator 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 ApfV4Generator 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));
-    }
-
-    /**
-     * Add an instruction to the end of the program to transmit the allocated buffer.
-     */
-    public ApfV4Generator addTransmit() throws IllegalInstructionException {
-        requireApfVersion(MIN_APF_VERSION_IN_DEV);
-        // TRANSMIT requires using R0 because it shares opcode with DISCARD
-        return append(new Instruction(ExtendedOpcodes.TRANSMIT));
-    }
-
-    /**
-     * Add an instruction to the end of the program to discard the allocated buffer.
-     */
-    public ApfV4Generator addDiscard() throws IllegalInstructionException {
-        requireApfVersion(MIN_APF_VERSION_IN_DEV);
-        // DISCARD requires using R1 because it shares opcode with TRANSMIT
-        return append(new Instruction(ExtendedOpcodes.DISCARD, R1));
-    }
-
-    /**
-     * Add an instruction to the end of the program to write 1 byte value to output buffer.
-     */
-    public ApfV4Generator addWriteU8(int val) throws IllegalInstructionException {
-        requireApfVersion(MIN_APF_VERSION_IN_DEV);
-        return append(new Instruction(Opcodes.WRITE).overrideLenField(1).addU8(val));
-    }
-
-    /**
-     * Add an instruction to the end of the program to write 2 bytes value to output buffer.
-     */
-    public ApfV4Generator addWriteU16(int val) throws IllegalInstructionException {
-        requireApfVersion(MIN_APF_VERSION_IN_DEV);
-        return append(new Instruction(Opcodes.WRITE).overrideLenField(2).addU16(val));
-    }
-
-    /**
-     * Add an instruction to the end of the program to write 4 bytes value to output buffer.
-     */
-    public ApfV4Generator addWriteU32(long val) throws IllegalInstructionException {
-        requireApfVersion(MIN_APF_VERSION_IN_DEV);
-        return append(new Instruction(Opcodes.WRITE).overrideLenField(4).addU32(val));
-    }
-
-    /**
-     * Add an instruction to the end of the program to write 1 byte value from register to output
-     * buffer.
-     */
-    public ApfV4Generator 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 ApfV4Generator 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 ApfV4Generator 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 ApfV4Generator 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 ApfV4Generator 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 ApfV4Generator 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 ApfV4Generator 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 ApfV4Generator 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 ApfV4Generator 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);
-        }
-    }
-
-    /**
-     * Appends a conditional jump instruction to the program: Jumps to {@code tgt} if the UDP
-     * payload's DNS questions do NOT contain the QNAMEs 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 ApfV4Generator 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));
-    }
-
-    /**
-     * 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.
-     * R = 1 means check for "contain".
-     */
-    public ApfV4Generator 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 NAMEs
-     * specified in {@code Names}. Examines the payload starting at the offset in R0.
-     * R = 0 means check for "does not contain".
-     */
-    public ApfV4Generator 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 NAMEs
-     * specified in {@code Names}. Examines the payload starting at the offset in R0.
-     * R = 1 means check for "contain".
-     */
-    public ApfV4Generator 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) {
+    static void checkRange(@NonNull String variableName, long value, long lowerBound,
+                           long upperBound) {
         if (value >= lowerBound && value <= upperBound) {
             return;
         }
diff --git a/src/android/net/apf/ApfV6Generator.java b/src/android/net/apf/ApfV6Generator.java
new file mode 100644
index 0000000..ff6bb4d
--- /dev/null
+++ b/src/android/net/apf/ApfV6Generator.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.apf;
+
+import static android.net.apf.ApfV4Generator.Register.R0;
+import static android.net.apf.ApfV4Generator.Register.R1;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.HexDump;
+
+/**
+ * APFv6 assembler/generator. A tool for generating an APFv6 program.
+ *
+ * @hide
+ */
+public class ApfV6Generator extends ApfV4Generator {
+
+    /**
+     * Creates an ApfV6Generator instance which is able to emit instructions for the specified
+     * {@code version} of the APF interpreter. Throws {@code IllegalInstructionException} if
+     * the requested version is unsupported.
+     *
+     */
+    public ApfV6Generator() throws IllegalInstructionException {
+        super(MIN_APF_VERSION_IN_DEV);
+    }
+
+    /**
+     * Add an instruction to the end of the program to increment the counter value and
+     * immediately return PASS.
+     */
+    public ApfV4Generator addCountAndPass(int cnt) {
+        checkRange("CounterNumber", cnt /* value */, 1 /* lowerBound */,
+                1000 /* upperBound */);
+        // PASS requires using R0 because it shares opcode with DROP
+        return append(new Instruction(Opcodes.PASSDROP).addUnsigned(cnt));
+    }
+
+    /**
+     * Add an instruction to the end of the program to let the program immediately return DROP.
+     */
+    public ApfV4Generator addDrop() {
+        // DROP requires using R1 because it shares opcode with PASS
+        return append(new Instruction(Opcodes.PASSDROP, R1));
+    }
+
+    /**
+     * Add an instruction to the end of the program to increment the counter value and
+     * immediately return DROP.
+     */
+    public ApfV4Generator addCountAndDrop(int cnt) {
+        checkRange("CounterNumber", cnt /* value */, 1 /* lowerBound */,
+                1000 /* upperBound */);
+        // DROP requires using R1 because it shares opcode with PASS
+        return append(new Instruction(Opcodes.PASSDROP, 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 ApfV4Generator addAllocateR0() {
+        return append(new Instruction(ExtendedOpcodes.ALLOCATE));
+    }
+
+    /**
+     * Add an instruction to the end of the program to call the apf_allocate_buffer() function.
+     *
+     * @param size the buffer length to be allocated.
+     */
+    public ApfV4Generator addAllocate(int size) {
+        // 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 ApfV4Generator addData(byte[] data) throws IllegalInstructionException {
+        if (!mInstructions.isEmpty()) {
+            throw new IllegalInstructionException("data instruction has to come first");
+        }
+        return append(new Instruction(Opcodes.JMP, R1).addUnsigned(data.length).setBytesImm(data));
+    }
+
+    /**
+     * Add an instruction to the end of the program to transmit the allocated buffer.
+     */
+    public ApfV4Generator addTransmit() {
+        // TRANSMIT requires using R0 because it shares opcode with DISCARD
+        return append(new Instruction(ExtendedOpcodes.TRANSMITDISCARD));
+    }
+
+    /**
+     * Add an instruction to the end of the program to discard the allocated buffer.
+     */
+    public ApfV4Generator addDiscard() {
+        // DISCARD requires using R1 because it shares opcode with TRANSMIT
+        return append(new Instruction(ExtendedOpcodes.TRANSMITDISCARD, R1));
+    }
+
+    /**
+     * Add an instruction to the end of the program to write 1 byte value to output buffer.
+     */
+    public ApfV4Generator addWriteU8(int val) {
+        return append(new Instruction(Opcodes.WRITE).overrideLenField(1).addU8(val));
+    }
+
+    /**
+     * Add an instruction to the end of the program to write 2 bytes value to output buffer.
+     */
+    public ApfV4Generator addWriteU16(int val) {
+        return append(new Instruction(Opcodes.WRITE).overrideLenField(2).addU16(val));
+    }
+
+    /**
+     * Add an instruction to the end of the program to write 4 bytes value to output buffer.
+     */
+    public ApfV4Generator addWriteU32(long val) {
+        return append(new Instruction(Opcodes.WRITE).overrideLenField(4).addU32(val));
+    }
+
+    /**
+     * Add an instruction to the end of the program to write 1 byte value from register to output
+     * buffer.
+     */
+    public ApfV4Generator addWriteU8(Register reg) {
+        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 ApfV4Generator addWriteU16(Register reg) {
+        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 ApfV4Generator addWriteU32(Register reg) {
+        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 ApfV4Generator addDataCopy(int src, int len) {
+        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 ApfV4Generator addPacketCopy(int src, int len) {
+        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 ApfV4Generator addDataCopyFromR0(int len) {
+        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 ApfV4Generator addPacketCopyFromR0(int len) {
+        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 ApfV4Generator addDataCopyFromR0LenR1() {
+        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 ApfV4Generator addPacketCopyFromR0LenR1() {
+        return append(new Instruction(ExtendedOpcodes.EPKTCOPY, R1));
+    }
+
+    /**
+     * Appends a conditional jump instruction to the program: Jumps to {@code tgt} if the UDP
+     * payload's DNS questions do NOT contain the QNAMEs 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 ApfV4Generator addJumpIfPktAtR0DoesNotContainDnsQ(@NonNull byte[] qnames, int qtype,
+                                                             @NonNull String tgt) {
+        validateNames(qnames);
+        return append(new Instruction(ExtendedOpcodes.JDNSQMATCH).setTargetLabel(tgt).addU8(
+                qtype).setBytesImm(qnames));
+    }
+
+    /**
+     * 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.
+     * R = 1 means check for "contain".
+     */
+    public ApfV4Generator addJumpIfPktAtR0ContainDnsQ(@NonNull byte[] qnames, int qtype,
+                                                      @NonNull String tgt) {
+        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 NAMEs
+     * specified in {@code Names}. Examines the payload starting at the offset in R0.
+     * R = 0 means check for "does not contain".
+     */
+    public ApfV4Generator addJumpIfPktAtR0DoesNotContainDnsA(@NonNull byte[] names,
+                                                             @NonNull String tgt) {
+        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 NAMEs
+     * specified in {@code Names}. Examines the payload starting at the offset in R0.
+     * R = 1 means check for "contain".
+     */
+    public ApfV4Generator addJumpIfPktAtR0ContainDnsA(@NonNull byte[] names,
+                                                      @NonNull String tgt) {
+        validateNames(names);
+        return append(new Instruction(ExtendedOpcodes.JDNSAMATCH, R1).setTargetLabel(
+                tgt).setBytesImm(names));
+    }
+
+    /**
+     * 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);
+        }
+    }
+}
diff --git a/src/android/net/dhcp/DhcpClient.java b/src/android/net/dhcp/DhcpClient.java
index 5633b54..4e6a222 100644
--- a/src/android/net/dhcp/DhcpClient.java
+++ b/src/android/net/dhcp/DhcpClient.java
@@ -1490,7 +1490,8 @@
             // IpClient sees the IP address appear, it will enter provisioned state without any
             // configuration information from DHCP. http://b/146850745.
             notifySuccess();
-            mController.sendMessage(CMD_CONFIGURE_LINKADDRESS, mDhcpLease.ipAddress);
+            mController.sendMessage(CMD_CONFIGURE_LINKADDRESS, mDhcpLease.leaseDuration, 0,
+                    mDhcpLease.ipAddress);
         }
 
         @Override
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java
index b06a632..9d1ba0e 100644
--- a/src/android/net/ip/IpClient.java
+++ b/src/android/net/ip/IpClient.java
@@ -51,6 +51,7 @@
 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_POPULATE_LINK_ADDRESS_LIFETIME_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;
@@ -705,6 +706,7 @@
     private final boolean mEnableIpClientIgnoreLowRaLifetime;
     private final boolean mApfShouldHandleLightDoze;
     private final boolean mEnableApfPollingCounters;
+    private final boolean mPopulateLinkAddressLifetime;
 
     private InterfaceParams mInterfaceParams;
 
@@ -944,6 +946,8 @@
         // 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);
+        mPopulateLinkAddressLifetime = mDependencies.isFeatureEnabled(context,
+                IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION);
 
         IpClientLinkObserver.Configuration config = new IpClientLinkObserver.Configuration(
                 mMinRdnssLifetimeSec);
@@ -3259,7 +3263,24 @@
 
                 case DhcpClient.CMD_CONFIGURE_LINKADDRESS: {
                     final LinkAddress ipAddress = (LinkAddress) msg.obj;
-                    if (mInterfaceCtrl.setIPv4Address(ipAddress)) {
+                    final boolean success;
+                    if (mPopulateLinkAddressLifetime) {
+                        // For IPv4 link addresses, there is no concept of preferred/valid
+                        // lifetimes. Populate the ifa_cacheinfo attribute in the netlink
+                        // message with the DHCP lease duration, which is used by the kernel
+                        // to maintain the validity of the IP addresses.
+                        final int leaseDuration = msg.arg1;
+                        success = NetlinkUtils.sendRtmNewAddressRequest(mInterfaceParams.index,
+                                ipAddress.getAddress(),
+                                (short) ipAddress.getPrefixLength(),
+                                0 /* flags */,
+                                (byte) RT_SCOPE_UNIVERSE /* scope */,
+                                leaseDuration /* preferred */,
+                                leaseDuration /* valid */);
+                    } else {
+                        success = mInterfaceCtrl.setIPv4Address(ipAddress);
+                    }
+                    if (success) {
                         // Although it's impossible to happen that DHCP client becomes null in
                         // RunningState and then NPE is thrown when it attempts to send a message
                         // on an null object, sometimes it's found during stress tests. If this
diff --git a/src/android/net/ip/IpClientLinkObserver.java b/src/android/net/ip/IpClientLinkObserver.java
index bf113b0..f91f569 100644
--- a/src/android/net/ip/IpClientLinkObserver.java
+++ b/src/android/net/ip/IpClientLinkObserver.java
@@ -30,6 +30,7 @@
 import static com.android.net.module.util.netlink.NetlinkConstants.RT_SCOPE_UNIVERSE;
 import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_ACCEPT_IPV6_LINK_LOCAL_DNS_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_PARSE_NETLINK_EVENTS_FORCE_DISABLE;
+import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION;
 
 import android.app.AlarmManager;
 import android.content.Context;
@@ -160,8 +161,9 @@
     private final Handler mHandler;
     private final IpClient.Dependencies mDependencies;
     private final String mClatInterfaceName;
-    private final MyNetlinkMonitor mNetlinkMonitor;
+    private final IpClientNetlinkMonitor mNetlinkMonitor;
     private final boolean mNetlinkEventParsingEnabled;
+    private final boolean mPopulateLinkAddressLifetime;
 
     private boolean mClatInterfaceExists;
 
@@ -194,7 +196,9 @@
         mDependencies = deps;
         mNetlinkEventParsingEnabled = deps.isFeatureNotChickenedOut(context,
                 IPCLIENT_PARSE_NETLINK_EVENTS_FORCE_DISABLE);
-        mNetlinkMonitor = new MyNetlinkMonitor(h, log, mTag);
+        mPopulateLinkAddressLifetime = deps.isFeatureEnabled(context,
+                IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION);
+        mNetlinkMonitor = new IpClientNetlinkMonitor(h, log, mTag);
         mHandler.post(() -> {
             if (!mNetlinkMonitor.start()) {
                 Log.wtf(mTag, "Fail to start NetlinkMonitor.");
@@ -427,10 +431,10 @@
      * Simple NetlinkMonitor. Listen for netlink events from kernel.
      * All methods except the constructor must be called on the handler thread.
      */
-    private class MyNetlinkMonitor extends NetlinkMonitor {
+    private class IpClientNetlinkMonitor extends NetlinkMonitor {
         private final Handler mHandler;
 
-        MyNetlinkMonitor(Handler h, SharedLog log, String tag) {
+        IpClientNetlinkMonitor(Handler h, SharedLog log, String tag) {
             super(h, log, tag, OsConstants.NETLINK_ROUTE,
                     !mNetlinkEventParsingEnabled
                         ? NetlinkConstants.RTMGRP_ND_USEROPT
@@ -634,9 +638,14 @@
 
         // The preferred/valid in ifa_cacheinfo expressed in units of seconds, convert
         // it to milliseconds for deprecationTime or expirationTime used in LinkAddress.
-        private static long getDeprecationOrExpirationTime(
-                @Nullable final StructIfacacheInfo cacheInfo, long lifetime, long now) {
-            if (cacheInfo == null) return LinkAddress.LIFETIME_UNKNOWN;
+        // If the experiment flag is not enabled, LinkAddress.LIFETIME_UNKNOWN is retuend,
+        // the same as before.
+        private long getDeprecationOrExpirationTime(@Nullable final StructIfacacheInfo cacheInfo,
+                long now, boolean deprecationTime) {
+            if (!mPopulateLinkAddressLifetime || (cacheInfo == null)) {
+                return LinkAddress.LIFETIME_UNKNOWN;
+            }
+            final long lifetime = deprecationTime ? cacheInfo.preferred : cacheInfo.valid;
             return (lifetime == Integer.toUnsignedLong(INFINITE_LEASE))
                     ? LinkAddress.LIFETIME_PERMANENT
                     : now + lifetime * 1000;
@@ -651,9 +660,9 @@
             final StructIfacacheInfo cacheInfo = msg.getIfacacheInfo();
             final long now = SystemClock.elapsedRealtime();
             final long deprecationTime =
-                    getDeprecationOrExpirationTime(cacheInfo, cacheInfo.preferred, now);
+                    getDeprecationOrExpirationTime(cacheInfo, now, true /* deprecationTime */);
             final long expirationTime =
-                    getDeprecationOrExpirationTime(cacheInfo, cacheInfo.valid, now);
+                    getDeprecationOrExpirationTime(cacheInfo, now, false /* deprecationTime */);
             final LinkAddress la = new LinkAddress(msg.getIpAddress(), ifaddrMsg.prefixLen,
                     msg.getFlags(), ifaddrMsg.scope, deprecationTime, expirationTime);
 
diff --git a/src/android/net/ip/IpReachabilityMonitor.java b/src/android/net/ip/IpReachabilityMonitor.java
index 12202ad..daf9e51 100644
--- a/src/android/net/ip/IpReachabilityMonitor.java
+++ b/src/android/net/ip/IpReachabilityMonitor.java
@@ -23,6 +23,7 @@
 
 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_IGNORE_ORGANIC_NUD_FAILURE_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_ROUTER_MAC_CHANGE_FAILURE_ONLY_AFTER_ROAM_VERSION;
 
@@ -241,6 +242,7 @@
     private final boolean mIgnoreIncompleteIpv6DnsServerEnabled;
     private final boolean mIgnoreIncompleteIpv6DefaultRouterEnabled;
     private final boolean mMacChangeFailureOnlyAfterRoam;
+    private final boolean mIgnoreOrganicNudFailure;
 
     public IpReachabilityMonitor(
             Context context, InterfaceParams ifParams, Handler h, SharedLog log, Callback callback,
@@ -270,6 +272,8 @@
                 IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION);
         mMacChangeFailureOnlyAfterRoam = dependencies.isFeatureNotChickenedOut(context,
                 IP_REACHABILITY_ROUTER_MAC_CHANGE_FAILURE_ONLY_AFTER_ROAM_VERSION);
+        mIgnoreOrganicNudFailure = dependencies.isFeatureEnabled(context,
+                IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION);
         mMetricsLog = metricsLog;
         mNetd = netd;
         Preconditions.checkNotNull(mNetd);
@@ -527,11 +531,22 @@
                 isNudFailureDueToRoam(), lostProvisioning);
 
         if (lostProvisioning) {
-            final String logMsg = "FAILURE: LOST_PROVISIONING, " + event;
+            final boolean isOrganicNudFailureAndToBeIgnored =
+                    ((type == NudEventType.NUD_ORGANIC_FAILED_CRITICAL)
+                            && mIgnoreOrganicNudFailure);
+            final String logMsg = "FAILURE: LOST_PROVISIONING, " + event
+                    + ", NUD event type: " + type.name()
+                    + (isOrganicNudFailureAndToBeIgnored ? ", to be ignored" : "");
             Log.w(TAG, logMsg);
             // TODO: remove |ip| when the callback signature no longer has
             // an InetAddress argument.
-            mCallback.notifyLost(ip, logMsg, type);
+            // Notify critical neighbor lost as long as the NUD failures
+            // are not from kernel organic or the NUD failure event type is
+            // NUD_ORGANIC_FAILED_CRITICAL but the experiment flag is not
+            // enabled. Regardless, the event metrics are still recoreded.
+            if (!isOrganicNudFailureAndToBeIgnored) {
+                mCallback.notifyLost(ip, logMsg, type);
+            }
         }
         logNudFailed(event, type);
     }
diff --git a/src/com/android/networkstack/util/NetworkStackUtils.java b/src/com/android/networkstack/util/NetworkStackUtils.java
index c495315..0cb31fe 100755
--- a/src/com/android/networkstack/util/NetworkStackUtils.java
+++ b/src/com/android/networkstack/util/NetworkStackUtils.java
@@ -234,6 +234,12 @@
             "ip_reachability_router_mac_change_failure_only_after_roam_version";
 
     /**
+     * Experiment flag to ignore all NUD failures from kernel organic.
+     */
+    public static final String IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION =
+            "ip_reachability_ignore_organic_nud_failure_version";
+
+    /**
      * Experiment flag to enable DHCPv6 Prefix Delegation(RFC8415) in IpClient.
      */
     public static final String IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION =
@@ -243,10 +249,12 @@
      * Experiment flag to enable new ra filter.
      */
     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.
@@ -260,6 +268,14 @@
     public static final String NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION =
             "networkmonitor_async_privdns_resolution";
 
+    /**
+     * Experiment flag to populate the IP link address lifetime such as deprecationTime and
+     * expirationtTime.
+     */
+    public static final String IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION =
+            "ipclient_populate_link_address_lifetime_version";
+
+
     /**** BEGIN Feature Kill Switch Flags ****/
 
     /**
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java
index 78a47ea..c62fb90 100755
--- a/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -2092,6 +2092,7 @@
             mThread = new Thread(() -> sendMessage(obtainMessage(CMD_PROBE_COMPLETE, token, 0,
                     isCaptivePortal(deps, httpsUrls, httpUrls, fallbackUrl))));
             mThread.start();
+            mDependencies.onThreadCreated(mThread);
         }
 
         @Override
@@ -3205,6 +3206,7 @@
                     ? new HttpsProbe(properties, proxy, url, captivePortalApiUrl)
                     : new HttpProbe(properties, proxy, url, captivePortalApiUrl);
             mResult = CaptivePortalProbeResult.failed(probeType);
+            mDependencies.onThreadCreated(this);
         }
 
         private volatile CaptivePortalProbeResult mResult;
@@ -3382,6 +3384,7 @@
         // Fixed pool to prevent configuring too many urls to exhaust system resource.
         final ExecutorService executor = Executors.newFixedThreadPool(
                 Math.min(num, MAX_PROBE_THREAD_POOL_SIZE));
+        mDependencies.onExecutorServiceCreated(executor);
         final CompletionService<CaptivePortalProbeResult> ecs =
                 new ExecutorCompletionService<CaptivePortalProbeResult>(executor);
         final Uri capportApiUrl = getCaptivePortalApiUrl(mLinkProperties);
@@ -3726,6 +3729,24 @@
             DataStallStatsUtils.write(stats, result);
         }
 
+        /**
+         * Callback to be called when a probing thread instance is created.
+         *
+         * This method is designed for overriding in test classes to collect
+         * created threads and waits for the termination.
+         */
+        public void onThreadCreated(@NonNull Thread thread) {
+        }
+
+        /**
+         * Callback to be called when a ExecutorService instance is created.
+         *
+         * This method is designed for overriding in test classes to collect
+         * created threads and waits for the termination.
+         */
+        public void onExecutorServiceCreated(@NonNull ExecutorService ecs) {
+        }
+
         public static final Dependencies DEFAULT = new Dependencies();
     }
 
diff --git a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
index 2bbb879..c4dc40a 100644
--- a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
+++ b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
@@ -746,6 +746,10 @@
         setFeatureEnabled(NetworkStackUtils.IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION,
                 true /* isDhcp6PrefixDelegationEnabled */);
 
+        // Enable populating the IP Link Address lifetime.
+        setFeatureEnabled(NetworkStackUtils.IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION,
+                true /* enabled */);
+
         setUpTapInterface();
         // It turns out that Router Solicitation will also be sent out even after the tap interface
         // is brought up, however, we want to wait for RS which is sent due to IPv6 stack is enabled
@@ -1495,9 +1499,6 @@
             final boolean shouldFirePreconnectionTimeout,
             final boolean timeoutBeforePreconnectionComplete) throws Exception {
         final long currentTime = System.currentTimeMillis();
-        final ArgumentCaptor<InterfaceConfigurationParcel> ifConfig =
-                ArgumentCaptor.forClass(InterfaceConfigurationParcel.class);
-
         startIpClientProvisioning(shouldReplyRapidCommitAck,
                 true /* isDhcpPreConnectionEnabled */,
                 false /* isDhcpIpConflictDetectEnabled */);
@@ -1554,12 +1555,7 @@
             }
         }
         verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true);
-
-        final LinkAddress ipAddress = new LinkAddress(CLIENT_ADDR, PREFIX_LENGTH);
-        verify(mNetd, timeout(TEST_TIMEOUT_MS).times(1)).interfaceSetCfg(ifConfig.capture());
-        assertEquals(ifConfig.getValue().ifName, mIfaceName);
-        assertEquals(ifConfig.getValue().ipv4Addr, ipAddress.getAddress().getHostAddress());
-        assertEquals(ifConfig.getValue().prefixLength, PREFIX_LENGTH);
+        verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR));
         assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU);
     }
 
@@ -4244,8 +4240,7 @@
                 NudEventType.NUD_POST_ROAMING_MAC_ADDRESS_CHANGED);
     }
 
-    private void doTestIpReachabilityMonitor_replyBroadcastArpRequestWithDiffMacAddresses(
-            boolean disconnect) throws Exception {
+    private void prepareIpReachabilityMonitorIpv4AddressResolutionTest() throws Exception {
         mNetworkAgentThread =
                 new HandlerThread(IpClientIntegrationTestCommon.class.getSimpleName());
         mNetworkAgentThread.start();
@@ -4270,6 +4265,11 @@
         final byte[] data = new byte[100];
         random.nextBytes(data);
         sendUdpPacketToNetwork(mNetworkAgent.getNetwork(), SERVER_ADDR, 1234 /* port */, data);
+    }
+
+    private void doTestIpReachabilityMonitor_replyBroadcastArpRequestWithDiffMacAddresses(
+            boolean disconnect) throws Exception {
+        prepareIpReachabilityMonitorIpv4AddressResolutionTest();
 
         // Respond to the broadcast ARP request.
         final ArpPacket request = getNextArpPacket();
@@ -4311,6 +4311,37 @@
         doTestIpReachabilityMonitor_replyBroadcastArpRequestWithDiffMacAddresses(true);
     }
 
+    @Test
+    public void testIpReachabilityMonitor_ignoreIpv4DefaultRouterOrganicNudFailure()
+            throws Exception {
+        setFeatureEnabled(NetworkStackUtils.IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION,
+                true /* ignoreOrganicNudFailure */);
+        prepareIpReachabilityMonitorIpv4AddressResolutionTest();
+
+        ArpPacket packet;
+        while ((packet = getNextArpPacket(TEST_TIMEOUT_MS)) != null) {
+            // wait address resolution to complete.
+        }
+        verify(mCb, never()).onReachabilityFailure(any());
+    }
+
+    @Test
+    public void testIpReachabilityMonitor_ignoreIpv4DefaultRouterOrganicNudFailure_flagoff()
+            throws Exception {
+        setFeatureEnabled(NetworkStackUtils.IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION,
+                false /* ignoreOrganicNudFailure */);
+        prepareIpReachabilityMonitorIpv4AddressResolutionTest();
+
+        ArpPacket packet;
+        while ((packet = getNextArpPacket(TEST_TIMEOUT_MS)) != null) {
+            // wait address resolution to complete.
+        }
+        final ArgumentCaptor<ReachabilityLossInfoParcelable> lossInfoCaptor =
+                ArgumentCaptor.forClass(ReachabilityLossInfoParcelable.class);
+        verify(mCb).onReachabilityFailure(lossInfoCaptor.capture());
+        assertEquals(ReachabilityLossReason.ORGANIC, lossInfoCaptor.getValue().reason);
+    }
+
     private void sendUdpPacketToNetwork(final Network network, final InetAddress remoteIp,
             int port, final byte[] data) throws Exception {
         final InetAddress laddr =
@@ -4325,6 +4356,7 @@
             final Inet6Address targetIp,
             final boolean isIgnoreIncompleteIpv6DnsServerEnabled,
             final boolean isIgnoreIncompleteIpv6DefaultRouterEnabled,
+            final boolean isIgnoreOrganicNudFailureEnabled,
             final boolean expectNeighborLost) throws Exception {
         mNetworkAgentThread =
                 new HandlerThread(IpClientIntegrationTestCommon.class.getSimpleName());
@@ -4338,6 +4370,9 @@
         setFeatureEnabled(
                 NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION,
                 isIgnoreIncompleteIpv6DefaultRouterEnabled);
+        setFeatureEnabled(
+                NetworkStackUtils.IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION,
+                isIgnoreOrganicNudFailureEnabled);
         final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                 .build();
         startIpClientProvisioning(config);
@@ -4395,6 +4430,7 @@
         runIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER, targetIp,
                 true /* isIgnoreIncompleteIpv6DnsServerEnabled */,
                 false /* isIgnoreIncompleteIpv6DefaultRouterEnabled */,
+                false /* isIgnoreOrganicNudFailureEnabled */,
                 false /* expectNeighborLost */);
     }
 
@@ -4406,6 +4442,7 @@
         runIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER, targetIp,
                 false /* isIgnoreIncompleteIpv6DnsServerEnabled */,
                 false /* isIgnoreIncompleteIpv6DefaultRouterEnabled */,
+                false /* isIgnoreOrganicNudFailureEnabled */,
                 true /* expectNeighborLost */);
     }
 
@@ -4416,6 +4453,7 @@
                 ROUTER_LINK_LOCAL /* targetIp */,
                 false /* isIgnoreIncompleteIpv6DnsServerEnabled */,
                 true /* isIgnoreIncompleteIpv6DefaultRouterEnabled */,
+                false /* isIgnoreOrganicNudFailureEnabled */,
                 false /* expectNeighborLost */);
     }
 
@@ -4426,6 +4464,53 @@
                 ROUTER_LINK_LOCAL /* targetIp */,
                 false /* isIgnoreIncompleteIpv6DnsServerEnabled */,
                 false /* isIgnoreIncompleteIpv6DefaultRouterEnabled */,
+                false /* isIgnoreOrganicNudFailureEnabled */,
+                true /* expectNeighborLost */);
+    }
+
+    @Test
+    public void testIpReachabilityMonitor_ignoreOnLinkIpv6DnsOrganicNudFailure()
+            throws Exception {
+        final Inet6Address targetIp =
+                (Inet6Address) InetAddresses.parseNumericAddress(IPV6_ON_LINK_DNS_SERVER);
+        runIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER, targetIp,
+                false /* isIgnoreIncompleteIpv6DnsServerEnabled */,
+                false /* isIgnoreIncompleteIpv6DefaultRouterEnabled */,
+                true /* isIgnoreOrganicNudFailureEnabled */,
+                false /* expectNeighborLost */);
+    }
+
+    @Test
+    public void testIpReachabilityMonitor_ignoreOnLinkIpv6DnsOrganicNudFailure_flagoff()
+            throws Exception {
+        final Inet6Address targetIp =
+                (Inet6Address) InetAddresses.parseNumericAddress(IPV6_ON_LINK_DNS_SERVER);
+        runIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER, targetIp,
+                false /* isIgnoreIncompleteIpv6DnsServerEnabled */,
+                false /* isIgnoreIncompleteIpv6DefaultRouterEnabled */,
+                false /* isIgnoreOrganicNudFailureEnabled */,
+                true /* expectNeighborLost */);
+    }
+
+    @Test
+    public void testIpReachabilityMonitor_ignoreIpv6DefaultRouterOrganicNudFailure()
+            throws Exception {
+        runIpReachabilityMonitorAddressResolutionTest(IPV6_OFF_LINK_DNS_SERVER,
+                ROUTER_LINK_LOCAL /* targetIp */,
+                false /* isIgnoreIncompleteIpv6DnsServerEnabled */,
+                false /* isIgnoreIncompleteIpv6DefaultRouterEnabled */,
+                true /* isIgnoreOrganicNudFailureEnabled */,
+                false /* expectNeighborLost */);
+    }
+
+    @Test
+    public void testIpReachabilityMonitor_ignoreIpv6DefaultRouterOrganicNudFailure_flagoff()
+            throws Exception {
+        runIpReachabilityMonitorAddressResolutionTest(IPV6_OFF_LINK_DNS_SERVER,
+                ROUTER_LINK_LOCAL /* targetIp */,
+                false /* isIgnoreIncompleteIpv6DnsServerEnabled */,
+                false /* isIgnoreIncompleteIpv6DefaultRouterEnabled */,
+                false /* isIgnoreOrganicNudFailureEnabled */,
                 true /* expectNeighborLost */);
     }
 
@@ -5700,10 +5785,9 @@
         long when = 0;
         for (LinkAddress la : lp.getLinkAddresses()) {
             if (la.isIpv4()) {
-                // TODO: assert the deprecationTime and expirationTime for IPv4 address
-                // once we populate the correct lifetime with dhcp lease(3600s) for IPv4
-                // link address.
-                assertLinkAddressPermanentLifetime(la);
+                when = now + 3600 * 1000; // DHCP lease duration
+                assertLinkAddressDeprecationTime(la, when);
+                assertLinkAddressExpirationTime(la, when);
             } else if (la.isIpv6() && la.getAddress().isLinkLocalAddress()) {
                 assertLinkAddressPermanentLifetime(la);
             } else if (la.isIpv6() && la.isGlobalPreferred()) {
@@ -5714,4 +5798,59 @@
             }
         }
     }
+
+    @Test
+    public void testPopulateLinkAddressLifetime_infiniteLeaseDuration() throws Exception {
+        // Only run the test when the flag of parsing netlink events is enabled.
+        assumeTrue(mIsNetlinkEventParseEnabled);
+
+        final ProvisioningConfiguration cfg = new ProvisioningConfiguration.Builder()
+                .withoutIPv6()
+                .build();
+
+        startIpClientProvisioning(cfg);
+        handleDhcpPackets(true /* isSuccessLease */, DhcpPacket.INFINITE_LEASE,
+                false /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU,
+                null /* captivePortalApiUrl */, null /* ipv6OnlyWaitTime */,
+                null /* domainName */, null /* domainSearchList */);
+
+        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
+        final LinkProperties lp = captor.getValue();
+        assertNotNull(lp);
+        for (LinkAddress la : lp.getLinkAddresses()) {
+            if (la.isIpv4()) {
+                assertLinkAddressPermanentLifetime(la);
+            }
+        }
+    }
+
+    @Test
+    public void testPopulateLinkAddressLifetime_minimalLeaseDuration() throws Exception {
+        // Only run the test when the flag of parsing netlink events is enabled.
+        assumeTrue(mIsNetlinkEventParseEnabled);
+
+        final ProvisioningConfiguration cfg = new ProvisioningConfiguration.Builder()
+                .withoutIPv6()
+                .build();
+
+        startIpClientProvisioning(cfg);
+        handleDhcpPackets(true /* isSuccessLease */, 59 /* lease duration */,
+                false /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU,
+                null /* captivePortalApiUrl */, null /* ipv6OnlyWaitTime */,
+                null /* domainName */, null /* domainSearchList */);
+
+        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
+        verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture());
+        final LinkProperties lp = captor.getValue();
+        assertNotNull(lp);
+        for (LinkAddress la : lp.getLinkAddresses()) {
+            if (la.isIpv4()) {
+                final long now = SystemClock.elapsedRealtime();
+                final long when = now + 60 * 1000; // minimal lease duration
+                assertLinkAddressDeprecationTime(la, when);
+                assertLinkAddressExpirationTime(la, when);
+            }
+        }
+    }
 }
diff --git a/tests/unit/src/android/net/apf/ApfV5Test.kt b/tests/unit/src/android/net/apf/ApfV5Test.kt
index 58ee706..f9766fd 100644
--- a/tests/unit/src/android/net/apf/ApfV5Test.kt
+++ b/tests/unit/src/android/net/apf/ApfV5Test.kt
@@ -17,7 +17,6 @@
 
 import android.net.apf.ApfV4Generator.IllegalInstructionException
 import android.net.apf.ApfV4Generator.MIN_APF_VERSION
-import android.net.apf.ApfV4Generator.MIN_APF_VERSION_IN_DEV
 import android.net.apf.ApfV4Generator.Register.R0
 import android.net.apf.ApfV4Generator.Register.R1
 import androidx.test.filters.SmallTest
@@ -36,53 +35,15 @@
 class ApfV5Test {
 
     @Test
-    fun testApfInstructionVersionCheck() {
-        var gen = ApfV4Generator(MIN_APF_VERSION)
-        assertFailsWith<IllegalInstructionException> { gen.addDrop() }
-        assertFailsWith<IllegalInstructionException> { gen.addCountAndDrop(12) }
-        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()), ApfV4Generator.DROP_LABEL) }
-        assertFailsWith<IllegalInstructionException> { gen.addJumpIfPktAtR0DoesNotContainDnsQ(
-                byteArrayOf(1, 'A'.code.toByte()), 0x0c, ApfV4Generator.DROP_LABEL) }
-        assertFailsWith<IllegalInstructionException> { gen.addJumpIfPktAtR0ContainDnsQ(
-                byteArrayOf(1, 'A'.code.toByte()), 0x0c, ApfV4Generator.DROP_LABEL) }
-        assertFailsWith<IllegalInstructionException> { gen.addJumpIfPktAtR0DoesNotContainDnsA(
-                byteArrayOf(1, 'A'.code.toByte()), ApfV4Generator.DROP_LABEL) }
-        assertFailsWith<IllegalInstructionException> { gen.addJumpIfPktAtR0ContainDnsA(
-                byteArrayOf(1, 'A'.code.toByte()), ApfV4Generator.DROP_LABEL) }
-    }
-
-    @Test
     fun testDataInstructionMustComeFirst() {
-        var gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV)
+        var gen = ApfV6Generator()
         gen.addAllocateR0()
         assertFailsWith<IllegalInstructionException> { gen.addData(ByteArray(3) { 0x01 }) }
     }
 
     @Test
     fun testApfInstructionEncodingSizeCheck() {
-        var gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV)
+        var gen = ApfV6Generator()
         assertFailsWith<IllegalArgumentException> { gen.addAllocate(65536) }
         assertFailsWith<IllegalArgumentException> { gen.addAllocate(-1) }
         assertFailsWith<IllegalArgumentException> { gen.addDataCopy(-1, 1) }
@@ -177,31 +138,43 @@
         // encoding PASS opcode: opcode=0, imm_len=0, R=0
         assertContentEquals(
                 byteArrayOf(encodeInstruction(opcode = 0, immLength = 0, register = 0)), program)
+        assertContentEquals(
+            listOf("0: pass"),
+            ApfJniUtils.disassembleApf(program).map { it.trim() } )
 
-        gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV)
+        gen = ApfV6Generator()
         gen.addDrop()
         program = gen.generate()
         // encoding DROP opcode: opcode=0, imm_len=0, R=1
         assertContentEquals(
                 byteArrayOf(encodeInstruction(opcode = 0, immLength = 0, register = 1)), program)
+        assertContentEquals(
+            listOf("0: drop"),
+            ApfJniUtils.disassembleApf(program).map { it.trim() } )
 
-        gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV)
+        gen = ApfV6Generator()
         gen.addCountAndPass(129)
         program = gen.generate()
         // encoding COUNT(PASS) opcode: opcode=0, imm_len=size_of(imm), R=0, imm=counterNumber
         assertContentEquals(
                 byteArrayOf(encodeInstruction(opcode = 0, immLength = 1, register = 0),
                         0x81.toByte()), program)
+        assertContentEquals(
+            listOf("0: pass         129"),
+            ApfJniUtils.disassembleApf(program).map { it.trim() } )
 
-        gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV)
+        gen = ApfV6Generator()
         gen.addCountAndDrop(1000)
         program = gen.generate()
         // encoding COUNT(DROP) opcode: opcode=0, imm_len=size_of(imm), R=1, imm=counterNumber
         assertContentEquals(
                 byteArrayOf(encodeInstruction(opcode = 0, immLength = 2, register = 1),
                         0x03, 0xe8.toByte()), program)
+        assertContentEquals(
+            listOf("0: drop         1000"),
+            ApfJniUtils.disassembleApf(program).map { it.trim() } )
 
-        gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV)
+        gen = ApfV6Generator()
         gen.addAllocateR0()
         gen.addAllocate(1500)
         program = gen.generate()
@@ -212,10 +185,10 @@
                 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))
+        assertContentEquals(listOf("0: allocate    r0", "2: allocate    1500"),
+            ApfJniUtils.disassembleApf(program).map { it.trim() })
 
-        gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV)
+        gen = ApfV6Generator()
         gen.addTransmit()
         gen.addDiscard()
         program = gen.generate()
@@ -226,10 +199,10 @@
                 encodeInstruction(opcode = 21, immLength = 1, register = 0), 37,
                 encodeInstruction(opcode = 21, immLength = 1, register = 1), 37,
         ), program)
-        // TODO: add back disassembling test check after we update the apf_disassembler
-        // assertContentEquals(arrayOf("       0: trans"), ApfJniUtils.disassembleApf(program))
+         assertContentEquals(listOf("0: discard", "2: transmit"),
+             ApfJniUtils.disassembleApf(program).map { it.trim() })
 
-        gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV)
+        gen = ApfV6Generator()
         val largeByteArray = ByteArray(256) { 0x01 }
         gen.addData(largeByteArray)
         program = gen.generate()
@@ -237,8 +210,10 @@
         assertContentEquals(byteArrayOf(
                 encodeInstruction(opcode = 14, immLength = 2, register = 1), 0x01, 0x00) +
                 largeByteArray, program)
+        assertContentEquals(listOf("0: data        256," + "01".repeat(256) ),
+            ApfJniUtils.disassembleApf(program).map { it.trim() })
 
-        gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV)
+        gen = ApfV6Generator()
         gen.addWriteU8(0x01)
         gen.addWriteU16(0x0102)
         gen.addWriteU32(0x01020304)
@@ -260,20 +235,20 @@
                 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"
+        assertContentEquals(listOf(
+            "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))
+        ApfJniUtils.disassembleApf(program).map { it.trim() })
 
-        gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV)
+        gen = ApfV6Generator()
         gen.addWriteU8(R0)
         gen.addWriteU16(R0)
         gen.addWriteU32(R0)
@@ -289,16 +264,15 @@
                 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: ewrite1 r0",
-//                "       2: ewrite2 r0",
-//                "       4: ewrite4 r0",
-//                "       6: ewrite1 r1",
-//                "       8: ewrite2 r1",
-//                "      10: ewrite4 r1"), ApfJniUtils.disassembleApf(program))
+        assertContentEquals(listOf(
+                "0: ewrite1     r0",
+                "2: ewrite2     r0",
+                "4: ewrite4     r0",
+                "6: ewrite1     r1",
+                "8: ewrite2     r1",
+                "10: ewrite4     r1"), ApfJniUtils.disassembleApf(program).map { it.trim() })
 
-        gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV)
+        gen = ApfV6Generator()
         gen.addDataCopy(0, 10)
         gen.addDataCopy(1, 5)
         gen.addPacketCopy(1000, 255)
@@ -309,12 +283,12 @@
                 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))
+        assertContentEquals(listOf(
+                "0: dcopy       0, 10",
+                "2: dcopy       1, 5",
+                "5: pcopy       1000, 255"), ApfJniUtils.disassembleApf(program).map { it.trim() })
 
-        gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV)
+        gen = ApfV6Generator()
         gen.addPacketCopyFromR0LenR1()
         gen.addPacketCopyFromR0(5)
         gen.addDataCopyFromR0LenR1()
@@ -331,7 +305,7 @@
 //                "       0: dcopy [r1+0], 5",
 //                "       4: pcopy [r0+1000], 255"), ApfJniUtils.disassembleApf(program))
 
-        gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV)
+        gen = ApfV6Generator()
         gen.addJumpIfBytesAtR0Equal(byteArrayOf('a'.code.toByte()), ApfV4Generator.DROP_LABEL)
         program = gen.generate()
         assertContentEquals(
@@ -339,7 +313,7 @@
                         1, 1, 'a'.code.toByte()), program)
 
         val qnames = byteArrayOf(1, 'A'.code.toByte(), 1, 'B'.code.toByte(), 0, 0)
-        gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV)
+        gen = ApfV6Generator()
         gen.addJumpIfPktAtR0DoesNotContainDnsQ(qnames, 0x0c, ApfV4Generator.DROP_LABEL)
         gen.addJumpIfPktAtR0ContainDnsQ(qnames, 0x0c, ApfV4Generator.DROP_LABEL)
         program = gen.generate()
@@ -349,7 +323,7 @@
                 encodeInstruction(21, 1, 1), 43, 1, 0x0c.toByte(),
         ) + qnames, program)
 
-        gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV)
+        gen = ApfV6Generator()
         gen.addJumpIfPktAtR0DoesNotContainDnsA(qnames, ApfV4Generator.DROP_LABEL)
         gen.addJumpIfPktAtR0ContainDnsA(qnames, ApfV4Generator.DROP_LABEL)
         program = gen.generate()
diff --git a/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt b/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt
index e071280..fbf311b 100644
--- a/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt
+++ b/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt
@@ -55,6 +55,7 @@
 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_IGNORE_ORGANIC_NUD_FAILURE_VERSION
 import com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION
 import com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_ROUTER_MAC_CHANGE_FAILURE_ONLY_AFTER_ROAM_VERSION
 import com.android.testutils.makeNewNeighMessage
@@ -71,7 +72,9 @@
 import kotlin.test.fail
 import org.junit.After
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.TestName
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.any
@@ -185,6 +188,7 @@
 @RunWith(AndroidJUnit4::class)
 @SmallTest
 class IpReachabilityMonitorTest {
+    @get:Rule val mTestName = TestName()
     private val callback = mock(IpReachabilityMonitor.Callback::class.java)
     private val dependencies = mock(IpReachabilityMonitor.Dependencies::class.java)
     private val log = mock(SharedLog::class.java)
@@ -271,6 +275,19 @@
         doReturn(true).`when`(dependencies).isFeatureNotChickenedOut(any(),
             eq(IP_REACHABILITY_ROUTER_MAC_CHANGE_FAILURE_ONLY_AFTER_ROAM_VERSION))
 
+        val ignoreOrganicNudFailureTestList = listOf(
+                "testLoseProvisioning_ignoreOrganicIpv4DnsLost",
+                "testLoseProvisioning_ignoreOrganicIpv6DnsLost",
+                "testLoseProvisioning_ignoreOrganicIpv4GatewayLost",
+                "testLoseProvisioning_ignoreOrganicIpv6GatewayLost")
+        // The experiment flag: IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION is read at
+        // the IpReachabilityMonitor constructor, so we have to set the value before initializing
+        // an IpReachabilityMonitor instance.
+        if (ignoreOrganicNudFailureTestList.contains(mTestName.methodName)) {
+            doReturn(true).`when`(dependencies).isFeatureEnabled(any(),
+                    eq(IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION))
+        }
+
         val monitorFuture = CompletableFuture<IpReachabilityMonitor>()
         // IpReachabilityMonitor needs to be started from the handler thread
         handler.post {
@@ -312,6 +329,15 @@
         lostNeighbor: InetAddress,
         eventType: NudEventType
     ) {
+        runLoseProvisioningTest(newLp, lostNeighbor, eventType, true /* expectedNotifyLost */)
+    }
+
+    private fun runLoseProvisioningTest(
+        newLp: LinkProperties,
+        lostNeighbor: InetAddress,
+        eventType: NudEventType,
+        expectedNotifyLost: Boolean
+    ) {
         reachabilityMonitor.updateLinkProperties(newLp)
 
         neighborMonitor.enqueuePacket(makeNewNeighMessage(TEST_IPV4_GATEWAY, NUD_STALE))
@@ -320,8 +346,14 @@
         neighborMonitor.enqueuePacket(makeNewNeighMessage(TEST_IPV6_DNS, NUD_STALE))
 
         neighborMonitor.enqueuePacket(makeNewNeighMessage(lostNeighbor, NUD_FAILED))
-        verify(callback, timeout(TEST_TIMEOUT_MS)).notifyLost(eq(lostNeighbor), anyString(),
-                eq(eventType))
+        handlerThread.waitForIdle(TEST_TIMEOUT_MS)
+
+        if (expectedNotifyLost) {
+            verify(callback, timeout(TEST_TIMEOUT_MS)).notifyLost(eq(lostNeighbor), anyString(),
+                    eq(eventType))
+        } else {
+             verify(callback, never()).notifyLost(eq(lostNeighbor), anyString(), any())
+        }
     }
 
     private fun verifyNudFailureMetrics(
@@ -395,6 +427,30 @@
                 NUD_ORGANIC_FAILED_CRITICAL)
     }
 
+    @Test
+    fun testLoseProvisioning_ignoreOrganicIpv4DnsLost() {
+        runLoseProvisioningTest(TEST_LINK_PROPERTIES, TEST_IPV4_DNS, NUD_ORGANIC_FAILED_CRITICAL,
+                false /* expectedNotifyLost */)
+    }
+
+    @Test
+    fun testLoseProvisioning_ignoreOrganicIpv6DnsLost() {
+        runLoseProvisioningTest(TEST_LINK_PROPERTIES, TEST_IPV6_DNS, NUD_ORGANIC_FAILED_CRITICAL,
+                false /* expectedNotifyLost */)
+    }
+
+    @Test
+    fun testLoseProvisioning_ignoreOrganicIpv4GatewayLost() {
+        runLoseProvisioningTest(TEST_LINK_PROPERTIES, TEST_IPV4_GATEWAY,
+                NUD_ORGANIC_FAILED_CRITICAL, false /* expectedNotifyLost */)
+    }
+
+    @Test
+    fun testLoseProvisioning_ignoreOrganicIpv6GatewayLost() {
+        runLoseProvisioningTest(TEST_LINK_PROPERTIES, TEST_IPV6_GATEWAY,
+                NUD_ORGANIC_FAILED_CRITICAL, false /* expectedNotifyLost */)
+    }
+
     private fun runNudProbeFailureMetricsTest(
         lp: LinkProperties,
         lostNeighbor: InetAddress,
diff --git a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
index 43bee55..d3647f5 100644
--- a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
+++ b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
@@ -154,8 +154,8 @@
 import android.util.ArrayMap;
 
 import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.SharedLog;
 import com.android.networkstack.NetworkStackNotifier;
@@ -180,6 +180,8 @@
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
+import com.android.testutils.FunctionalUtils.ThrowingConsumer;
 import com.android.testutils.HandlerUtils;
 
 import com.google.protobuf.nano.MessageNano;
@@ -194,6 +196,7 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.InOrder;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 import org.mockito.Spy;
 import org.mockito.invocation.InvocationOnMock;
@@ -223,12 +226,15 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
+import java.util.function.Supplier;
 
 import javax.net.ssl.SSLHandshakeException;
 
-@RunWith(AndroidJUnit4.class)
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
 @SuppressLint("NewApi")  // Uses hidden APIs, which the linter would identify as missing APIs.
 public class NetworkMonitorTest {
@@ -321,6 +327,7 @@
     private static final NetworkAgentConfigShim TEST_AGENT_CONFIG =
             NetworkAgentConfigShimImpl.newInstance(null);
     private static final LinkProperties TEST_LINK_PROPERTIES = new LinkProperties();
+    private static final int THREAD_QUIT_MAX_RETRY_COUNT = 3;
 
     // Cannot have a static member for the LinkProperties with captive portal API information, as
     // the initializer would crash on Q (the members in LinkProperties were introduced in R).
@@ -564,6 +571,11 @@
 
     private FakeDns mFakeDns;
 
+    @GuardedBy("mThreadsToBeCleared")
+    private final ArrayList<Thread> mThreadsToBeCleared = new ArrayList<>();
+    @GuardedBy("mExecutorServiceToBeCleared")
+    private final ArrayList<ExecutorService> mExecutorServiceToBeCleared = new ArrayList<>();
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -578,6 +590,18 @@
                 .getSetting(any(), eq(Settings.Global.CAPTIVE_PORTAL_HTTP_URL), any());
         doReturn(TEST_HTTPS_URL).when(mDependencies)
                 .getSetting(any(), eq(Settings.Global.CAPTIVE_PORTAL_HTTPS_URL), any());
+        doAnswer((invocation) -> {
+            synchronized (mThreadsToBeCleared) {
+                mThreadsToBeCleared.add(invocation.getArgument(0));
+            }
+            return null;
+        }).when(mDependencies).onThreadCreated(any());
+        doAnswer((invocation) -> {
+            synchronized (mExecutorServiceToBeCleared) {
+                mExecutorServiceToBeCleared.add(invocation.getArgument(0));
+            }
+            return null;
+        }).when(mDependencies).onExecutorServiceCreated(any());
 
         doReturn(mCleartextDnsNetwork).when(mNetwork).getPrivateDnsBypassingCopy();
 
@@ -680,8 +704,52 @@
         mRegisteredReceivers = new HashSet<>();
     }
 
+    private static <T> void quitThreadsThat(Supplier<List<T>> supplier, ThrowingConsumer terminator)
+            throws Exception {
+        // Run it multiple times since new threads might be generated in a thread
+        // that is about to be terminated, e.g. each thread that runs
+        // isCaptivePortal could generate 2 more probing threads.
+        for (int retryCount = 0; retryCount < THREAD_QUIT_MAX_RETRY_COUNT; retryCount++) {
+            final List<T> resourcesToBeCleared = supplier.get();
+            if (resourcesToBeCleared.isEmpty()) return;
+            for (final T resource : resourcesToBeCleared) {
+                terminator.accept(resource);
+            }
+        }
+
+        assertEquals(Collections.emptyList(), supplier.get());
+    }
+
+    private void quitExecutorServices() throws Exception {
+        quitThreadsThat(() -> {
+            synchronized (mExecutorServiceToBeCleared) {
+                final ArrayList<ExecutorService> ret = new ArrayList<>(mExecutorServiceToBeCleared);
+                mExecutorServiceToBeCleared.clear();
+                return ret;
+            }
+        }, (it) -> {
+            final ExecutorService ecs = (ExecutorService) it;
+            ecs.awaitTermination(HANDLER_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        });
+    }
+
+    private void quitThreads() throws Exception {
+        quitThreadsThat(() -> {
+            synchronized (mThreadsToBeCleared) {
+                final ArrayList<Thread> ret = new ArrayList<>(mThreadsToBeCleared);
+                mThreadsToBeCleared.clear();
+                return ret;
+            }
+        }, (it) -> {
+            final Thread th = (Thread) it;
+            th.interrupt();
+            th.join(HANDLER_TIMEOUT_MS);
+            if (th.isAlive()) fail("Threads did not terminate within timeout.");
+        });
+    }
+
     @After
-    public void tearDown() {
+    public void tearDown() throws Exception {
         mFakeDns.clearAll();
         // Make a local copy of mCreatedNetworkMonitors because during the iteration below,
         // WrappedNetworkMonitor#onQuitting will delete elements from it on the handler threads.
@@ -695,6 +763,10 @@
                 0, mCreatedNetworkMonitors.size());
         assertEquals("BroadcastReceiver still registered after disconnect",
                 0, mRegisteredReceivers.size());
+        quitThreads();
+        quitExecutorServices();
+        // Clear mocks to prevent from stubs holding instances and cause memory leaks.
+        Mockito.framework().clearInlineMocks();
     }
 
     private void initHttpConnection(HttpURLConnection connection) {