Merge "Use ArraySet instead of HashSet in IpReachabilityMonitor." into main
diff --git a/res/values/config.xml b/res/values/config.xml
index aed375c..16364d6 100644
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -108,12 +108,14 @@
 
     <!-- Whether the APF Filter in the device should filter out IEEE 802.3 Frames
          Those frames are identified by the field Eth-type having values
-         less than 0x600 -->
+         less than 0x600.
+         This configuration has been deprecated and is not functional in Android V+. -->
     <bool name="config_apfDrop802_3Frames">true</bool>
 
     <!-- An array of Denylisted EtherType, packets with EtherTypes within this array
-         will be dropped
-         TODO: are these proper values? -->
+         will be dropped.
+         TODO: are these proper values?
+         This configuration has been deprecated and is not functional in Android V+. -->
     <integer-array name="config_apfEthTypeDenyList">
         <item>0x88A2</item>
         <item>0x88A4</item>
diff --git a/res/values/overlayable.xml b/res/values/overlayable.xml
index 9c2172a..08a2778 100644
--- a/res/values/overlayable.xml
+++ b/res/values/overlayable.xml
@@ -79,10 +79,12 @@
             <item type="integer" name="config_evaluating_bandwidth_max_retry_timer_ms"/>
 
             <!-- Whether the APF Filter in the device should filter out IEEE 802.3 Frames
-            Those frames are identified by the field Eth-type having values less than 0x600 -->
+            Those frames are identified by the field Eth-type having values less than 0x600.
+            This configuration has been deprecated and is not functional in Android V+ -->
             <item type="bool" name="config_apfDrop802_3Frames"/>
             <!-- An array of Denylisted EtherType, packets with EtherTypes within this array
-            will be dropped -->
+            will be dropped.
+            This configuration has been deprecated and is not functional in Android V+ -->
             <item type="array" name="config_apfEthTypeDenyList"/>
             <!-- Icon used for connected captive portal notifications on wifi networks,
             notifying that internet access is now available or that a venue info page is
diff --git a/src/android/net/apf/AndroidPacketFilter.java b/src/android/net/apf/AndroidPacketFilter.java
index 1a34ec6..2a7165b 100644
--- a/src/android/net/apf/AndroidPacketFilter.java
+++ b/src/android/net/apf/AndroidPacketFilter.java
@@ -15,6 +15,7 @@
  */
 package android.net.apf;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.LinkProperties;
 import android.net.NattKeepalivePacketDataParcelable;
@@ -98,4 +99,12 @@
 
     /** Return hex string of current APF snapshot for testing purposes. */
     @Nullable String getDataSnapshotHexString();
+
+    /**
+     * Determines whether the APF interpreter advertises support for the data buffer access
+     * opcodes LDDW (LoaD Data Word) and STDW (STore Data Word).
+     */
+    default boolean hasDataAccess(@NonNull ApfCapabilities capabilities) {
+        return capabilities.apfVersionSupported > 2;
+    }
 }
diff --git a/src/android/net/apf/ApfConstants.java b/src/android/net/apf/ApfConstants.java
index a4b38a3..fe2cfd8 100644
--- a/src/android/net/apf/ApfConstants.java
+++ b/src/android/net/apf/ApfConstants.java
@@ -53,13 +53,18 @@
     // The IPv6 all nodes address ff02::1
     public static final byte[] IPV6_ALL_NODES_ADDRESS =
             { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
+    // The IPv6 unspecified address ::
+    public static final byte[] IPV6_UNSPECIFIED_ADDRESS =
+            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
     // The IPv6 solicited nodes multicast address prefix ff02::1:ffXX:X/104
     public static final byte[] IPV6_SOLICITED_NODES_PREFIX =
             { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, (byte) 0xff};
 
     public static final int ICMP6_TYPE_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN;
     public static final int ICMP6_CODE_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN + 1;
+    public static final int ICMP6_CHECKSUM_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN + 2;
     public static final int ICMP6_NS_TARGET_IP_OFFSET = ICMP6_TYPE_OFFSET + 8;
+    public static final int ICMP6_NS_OPTION_TYPE_OFFSET = ICMP6_NS_TARGET_IP_OFFSET + 16;
 
     public static final int IPPROTO_HOPOPTS = 0;
 
@@ -90,6 +95,11 @@
     // TODO: Select a proper max length
     public static final int APF_MAX_ETH_TYPE_BLACK_LIST_LEN = 20;
 
+    // The ethernet solicited nodes multicast address prefix 33:33:FF:xx:xx:xx
+    public static final byte[] ETH_SOLICITED_NODES_PREFIX =
+            {(byte) 0x33, (byte) 0x33, (byte) 0xff};
+    public static final byte[] ETH_MULTICAST_IPV6_ALL_NODES_MAC_ADDRESS =
+            { (byte) 0x33, (byte) 0x33, 0, 0, 0, 1};
     public static final byte[] ETH_MULTICAST_MDNS_V4_MAC_ADDRESS =
             {(byte) 0x01, (byte) 0x00, (byte) 0x5e, (byte) 0x00, (byte) 0x00, (byte) 0xfb};
     public static final byte[] ETH_MULTICAST_MDNS_V6_MAC_ADDRESS =
diff --git a/src/android/net/apf/ApfFilter.java b/src/android/net/apf/ApfFilter.java
index 8f221af..c705928 100644
--- a/src/android/net/apf/ApfFilter.java
+++ b/src/android/net/apf/ApfFilter.java
@@ -77,6 +77,7 @@
 import static android.system.OsConstants.ETH_P_ARP;
 import static android.system.OsConstants.ETH_P_IP;
 import static android.system.OsConstants.ETH_P_IPV6;
+import static android.system.OsConstants.IFA_F_TENTATIVE;
 import static android.system.OsConstants.IPPROTO_ICMPV6;
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
@@ -116,6 +117,7 @@
 import android.text.format.DateUtils;
 import android.util.ArraySet;
 import android.util.Log;
+import android.util.Pair;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.GuardedBy;
@@ -336,9 +338,13 @@
     @GuardedBy("this")
     private int mIPv4PrefixLength;
 
-    // Our IPv6 addresses
+    // Our IPv6 non-tentative addresses
     @GuardedBy("this")
-    private Set<Inet6Address> mIPv6Addresses = new ArraySet<>();
+    private Set<Inet6Address> mIPv6NonTentativeAddresses = new ArraySet<>();
+
+    // Our tentative IPv6 addresses
+    @GuardedBy("this")
+    private Set<Inet6Address> mIPv6TentativeAddresses = new ArraySet<>();
 
     // Whether CLAT is enabled.
     @GuardedBy("this")
@@ -495,6 +501,17 @@
 
             return addresses;
         }
+
+        /**
+         * Loads the existing ND traffic class for the specific interface from the file
+         * /proc/sys/net/ipv6/conf/{ifname}/ndisc_tclass.
+         *
+         * If the file does not exist or the interface is not found,
+         * the function returns 0..255, 0 as default ND traffic class.
+         */
+        public int getNdTrafficClass(@NonNull String ifname) {
+            return ProcfsParsingUtils.getNdTrafficClass(ifname);
+        }
     }
 
     public synchronized void setDataSnapshot(byte[] data) {
@@ -554,7 +571,7 @@
                 // Clear the APF memory to reset all counters upon connecting to the first AP
                 // in an SSID. This is limited to APFv4 devices because this large write triggers
                 // a crash on some older devices (b/78905546).
-                if (mIsRunning && mApfCapabilities.hasDataAccess()) {
+                if (mIsRunning && hasDataAccess(mApfCapabilities)) {
                     byte[] zeroes = new byte[mApfCapabilities.maximumApfProgramSize];
                     if (!mIpClientCallback.installPacketFilter(zeroes)) {
                         sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
@@ -1782,13 +1799,24 @@
     }
 
     @GuardedBy("this")
-    private List<byte[]> getUnicastIpv6Addresses() {
+    private List<byte[]> getIpv6Addresses(
+            boolean includeNonTentative, boolean includeTentative, boolean includeAnycast) {
         final List<byte[]> addresses = new ArrayList<>();
-        for (Inet6Address addr : mIPv6Addresses) {
-            addresses.add(addr.getAddress());
+        if (includeNonTentative) {
+            for (Inet6Address addr : mIPv6NonTentativeAddresses) {
+                addresses.add(addr.getAddress());
+            }
         }
 
-        addresses.addAll(mDependencies.getAnycast6Addresses(mInterfaceParams.name));
+        if (includeTentative) {
+            for (Inet6Address addr : mIPv6TentativeAddresses) {
+                addresses.add(addr.getAddress());
+            }
+        }
+
+        if (includeAnycast) {
+            addresses.addAll(mDependencies.getAnycast6Addresses(mInterfaceParams.name));
+        }
         return addresses;
     }
 
@@ -1804,7 +1832,10 @@
     @GuardedBy("this")
     private void generateNsFilterLocked(ApfV6Generator v6Gen)
             throws IllegalInstructionException {
-        final List<byte[]> allIPv6Addrs = getUnicastIpv6Addresses();
+        final List<byte[]> allIPv6Addrs = getIpv6Addresses(
+                true /* includeNonTentative */,
+                true /* includeTentative */,
+                true /* includeAnycast */);
         if (allIPv6Addrs.isEmpty()) {
             // There is no IPv6 link local address.
             v6Gen.addCountAndDrop(DROPPED_IPV6_NS_NO_ADDRESS);
@@ -1857,6 +1888,8 @@
         //   (8 bytes ICMP6 header + 16 bytes target address + 8 bytes option) -> pass
         v6Gen.addLoad16(R0, IPV6_PAYLOAD_LEN_OFFSET)
                 .addCountAndPassIfR0GreaterThan(32, PASSED_IPV6_NS_MULTIPLE_OPTIONS);
+
+        v6Gen.addCountAndPass(Counter.PASSED_IPV6_ICMP);
     }
 
     /**
@@ -2119,7 +2152,7 @@
             gen = new ApfV4Generator(mApfCapabilities.apfVersionSupported);
         }
 
-        if (mApfCapabilities.hasDataAccess()) {
+        if (hasDataAccess(mApfCapabilities)) {
             if (gen instanceof ApfV4Generator) {
                 // Increment TOTAL_PACKETS.
                 // Only needed in APFv4.
@@ -2241,7 +2274,7 @@
         final byte[] program;
         int programMinLft = Integer.MAX_VALUE;
         int maximumApfProgramSize = mApfCapabilities.maximumApfProgramSize;
-        if (mApfCapabilities.hasDataAccess()) {
+        if (hasDataAccess(mApfCapabilities)) {
             // Reserve space for the counters.
             maximumApfProgramSize -= Counter.totalSize();
         }
@@ -2509,19 +2542,28 @@
         return ipv4Address;
     }
 
-    /** Retrieve the IPv6 LinkAddress list, otherwise return empty list. */
-    private static Set<Inet6Address> retrieveIPv6LinkAddress(LinkProperties lp) {
-        final Set<Inet6Address> ipv6Addresses = new ArraySet<>();
-
+    /** Retrieve the pair of IPv6 Inet6Address set, otherwise return pair with two empty set.
+     *  The first element is a set containing tentative IPv6 addresses,
+     *  the second element is a set containing non-tentative IPv6 addresses
+     *  */
+    private static Pair<Set<Inet6Address>, Set<Inet6Address>>
+            retrieveIPv6LinkAddress(LinkProperties lp) {
+        final Set<Inet6Address> tentativeAddrs = new ArraySet<>();
+        final Set<Inet6Address> nonTentativeAddrs = new ArraySet<>();
         for (LinkAddress address : lp.getLinkAddresses()) {
             if (!(address.getAddress() instanceof Inet6Address)) {
                 continue;
             }
 
-            ipv6Addresses.add((Inet6Address) address.getAddress());
+            if ((address.getFlags() & IFA_F_TENTATIVE) == IFA_F_TENTATIVE) {
+                tentativeAddrs.add((Inet6Address) address.getAddress());
+            } else {
+                nonTentativeAddrs.add((Inet6Address) address.getAddress());
+            }
         }
 
-        return ipv6Addresses;
+
+        return new Pair<>(tentativeAddrs, nonTentativeAddrs);
     }
 
     public synchronized void setLinkProperties(LinkProperties lp) {
@@ -2529,17 +2571,20 @@
         final LinkAddress ipv4Address = retrieveIPv4LinkAddress(lp);
         final byte[] addr = (ipv4Address != null) ? ipv4Address.getAddress().getAddress() : null;
         final int prefix = (ipv4Address != null) ? ipv4Address.getPrefixLength() : 0;
-        final Set<Inet6Address> ipv6Addresses = retrieveIPv6LinkAddress(lp);
+        final Pair<Set<Inet6Address>, Set<Inet6Address>>
+                ipv6Addresses = retrieveIPv6LinkAddress(lp);
 
         if ((prefix == mIPv4PrefixLength)
                 && Arrays.equals(addr, mIPv4Address)
-                && ipv6Addresses.equals(mIPv6Addresses)
+                && ipv6Addresses.first.equals(mIPv6TentativeAddresses)
+                && ipv6Addresses.second.equals(mIPv6NonTentativeAddresses)
         ) {
             return;
         }
         mIPv4Address = addr;
         mIPv4PrefixLength = prefix;
-        mIPv6Addresses = ipv6Addresses;
+        mIPv6TentativeAddresses = ipv6Addresses.first;
+        mIPv6NonTentativeAddresses = ipv6Addresses.second;
 
         installNewProgramLocked();
     }
@@ -2618,7 +2663,7 @@
             pw.println("IPv4 address: " + InetAddress.getByAddress(mIPv4Address).getHostAddress());
             pw.println("IPv6 addresses: ");
             pw.increaseIndent();
-            for (Inet6Address addr: mIPv6Addresses) {
+            for (Inet6Address addr: mIPv6NonTentativeAddresses) {
                 pw.println(addr.getHostAddress());
             }
             pw.decreaseIndent();
@@ -2691,7 +2736,7 @@
 
         pw.println("APF packet counters: ");
         pw.increaseIndent();
-        if (!mApfCapabilities.hasDataAccess()) {
+        if (!hasDataAccess(mApfCapabilities)) {
             pw.println("APF counters not supported");
         } else if (mDataSnapshot == null) {
             pw.println("No last snapshot.");
diff --git a/src/android/net/apf/LegacyApfFilter.java b/src/android/net/apf/LegacyApfFilter.java
index caa3b25..99aab80 100644
--- a/src/android/net/apf/LegacyApfFilter.java
+++ b/src/android/net/apf/LegacyApfFilter.java
@@ -126,7 +126,7 @@
      * When APFv4 is supported, loads R1 with the offset of the specified counter.
      */
     private void maybeSetupCounter(ApfV4Generator gen, Counter c) {
-        if (mApfCapabilities.hasDataAccess()) {
+        if (hasDataAccess(mApfCapabilities)) {
             gen.addLoadImmediate(R1, c.offset());
         }
     }
@@ -408,7 +408,7 @@
         mSessionStartMs = mClock.elapsedRealtime();
         mMinMetricsSessionDurationMs = config.minMetricsSessionDurationMs;
 
-        if (mApfCapabilities.hasDataAccess()) {
+        if (hasDataAccess(mApfCapabilities)) {
             mCountAndPassLabel = "countAndPass";
             mCountAndDropLabel = "countAndDrop";
         } else {
@@ -492,7 +492,7 @@
                 // Clear the APF memory to reset all counters upon connecting to the first AP
                 // in an SSID. This is limited to APFv4 devices because this large write triggers
                 // a crash on some older devices (b/78905546).
-                if (mIsRunning && mApfCapabilities.hasDataAccess()) {
+                if (mIsRunning && hasDataAccess(mApfCapabilities)) {
                     byte[] zeroes = new byte[mApfCapabilities.maximumApfProgramSize];
                     if (!mIpClientCallback.installPacketFilter(zeroes)) {
                         sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
@@ -1728,7 +1728,7 @@
         // This is guaranteed to succeed because of the check in maybeCreate.
         ApfV4Generator gen = new ApfV4Generator(mApfCapabilities.apfVersionSupported);
 
-        if (mApfCapabilities.hasDataAccess()) {
+        if (hasDataAccess(mApfCapabilities)) {
             // Increment TOTAL_PACKETS
             maybeSetupCounter(gen, Counter.TOTAL_PACKETS);
             gen.addLoadData(R0, 0);  // load counter
@@ -1831,7 +1831,7 @@
     private void emitEpilogue(ApfV4Generator gen) throws IllegalInstructionException {
         // If APFv4 is unsupported, no epilogue is necessary: if execution reached this far, it
         // will just fall-through to the PASS label.
-        if (!mApfCapabilities.hasDataAccess()) return;
+        if (!hasDataAccess(mApfCapabilities)) return;
 
         // Execution will reach the bottom of the program if none of the filters match,
         // which will pass the packet to the application processor.
@@ -1867,7 +1867,7 @@
         final byte[] program;
         long programMinLifetime = Long.MAX_VALUE;
         long maximumApfProgramSize = mApfCapabilities.maximumApfProgramSize;
-        if (mApfCapabilities.hasDataAccess()) {
+        if (hasDataAccess(mApfCapabilities)) {
             // Reserve space for the counters.
             maximumApfProgramSize -= Counter.totalSize();
         }
@@ -2323,7 +2323,7 @@
 
         pw.println("APF packet counters: ");
         pw.increaseIndent();
-        if (!mApfCapabilities.hasDataAccess()) {
+        if (!hasDataAccess(mApfCapabilities)) {
             pw.println("APF counters not supported");
         } else if (mDataSnapshot == null) {
             pw.println("No last snapshot.");
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java
index a69505c..304b038 100644
--- a/src/android/net/ip/IpClient.java
+++ b/src/android/net/ip/IpClient.java
@@ -1355,15 +1355,13 @@
         // Thread-unsafe access to mApfFilter but just used for debugging.
         final AndroidPacketFilter apfFilter = mApfFilter;
         final android.net.shared.ProvisioningConfiguration provisioningConfig = mConfiguration;
-        final ApfCapabilities apfCapabilities = (provisioningConfig != null)
-                ? provisioningConfig.mApfCapabilities : null;
+        final ApfCapabilities apfCapabilities = mCurrentApfCapabilities;
 
         IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
         pw.println(mTag + " APF dump:");
         pw.increaseIndent();
-        if (apfFilter != null && apfCapabilities != null
-                && apfCapabilities.apfVersionSupported > 0) {
-            if (apfCapabilities.hasDataAccess()) {
+        if (apfFilter != null) {
+            if (apfCapabilities != null && apfFilter.hasDataAccess(apfCapabilities)) {
                 // Request a new snapshot, then wait for it.
                 mApfDataSnapshotComplete.close();
                 mCallback.startReadPacketFilter("dumpsys");
@@ -2525,11 +2523,19 @@
     }
 
     @Nullable
-    private AndroidPacketFilter maybeCreateApfFilter(final ApfCapabilities apfCapabilities) {
+    private AndroidPacketFilter maybeCreateApfFilter(final ApfCapabilities apfCaps) {
         ApfFilter.ApfConfiguration apfConfig = new ApfFilter.ApfConfiguration();
-        apfConfig.apfCapabilities = apfCapabilities;
-        if (apfCapabilities != null && !SdkLevel.isAtLeastV()
-                && apfCapabilities.apfVersionSupported <= 4) {
+        apfConfig.apfCapabilities = apfCaps;
+        if (apfCaps != null && !SdkLevel.isAtLeastS()) {
+            // Due to potential OEM modifications in Android R, reconfigure
+            // apfVersionSupported using apfCapabilities.hasDataAccess() to ensure safe data
+            // region access within ApfFilter.
+            int apfVersionSupported = apfCaps.hasDataAccess() ? 3 : 2;
+            apfConfig.apfCapabilities = new ApfCapabilities(apfVersionSupported,
+                    apfCaps.maximumApfProgramSize, apfCaps.apfPacketFormat);
+        }
+        if (apfConfig.apfCapabilities != null && !SdkLevel.isAtLeastV()
+                && apfConfig.apfCapabilities.apfVersionSupported <= 4) {
             apfConfig.installableProgramSizeClamp = 1024;
         }
         apfConfig.multicastFilter = mMulticastFiltering;
diff --git a/tests/unit/src/android/net/apf/ApfNewTest.kt b/tests/unit/src/android/net/apf/ApfNewTest.kt
index 73c9d79..d271b2e 100644
--- a/tests/unit/src/android/net/apf/ApfNewTest.kt
+++ b/tests/unit/src/android/net/apf/ApfNewTest.kt
@@ -18,6 +18,7 @@
 import android.content.Context
 import android.net.LinkAddress
 import android.net.LinkProperties
+import android.net.MacAddress
 import android.net.apf.ApfCounterTracker.Counter
 import android.net.apf.ApfCounterTracker.Counter.APF_PROGRAM_ID
 import android.net.apf.ApfCounterTracker.Counter.APF_VERSION
@@ -41,7 +42,6 @@
 import android.net.apf.ApfTestUtils.DROP
 import android.net.apf.ApfTestUtils.MIN_PKT_SIZE
 import android.net.apf.ApfTestUtils.PASS
-import android.net.apf.ApfTestUtils.TestApfFilter
 import android.net.apf.ApfTestUtils.assertDrop
 import android.net.apf.ApfTestUtils.assertPass
 import android.net.apf.ApfTestUtils.assertVerdict
@@ -54,10 +54,13 @@
 import android.net.apf.BaseApfGenerator.PASS_LABEL
 import android.net.apf.BaseApfGenerator.Register.R0
 import android.net.apf.BaseApfGenerator.Register.R1
+import android.net.ip.IpClient.IpClientCallbacksWrapper
 import android.os.Build
 import android.system.OsConstants.ARPHRD_ETHER
+import android.system.OsConstants.IFA_F_TENTATIVE
 import androidx.test.filters.SmallTest
 import com.android.net.module.util.HexDump
+import com.android.net.module.util.InterfaceParams
 import com.android.net.module.util.NetworkStackConstants.ARP_ETHER_IPV4_LEN
 import com.android.net.module.util.NetworkStackConstants.ARP_REPLY
 import com.android.net.module.util.NetworkStackConstants.ARP_REQUEST
@@ -80,9 +83,12 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.any
 import org.mockito.Mock
 import org.mockito.Mockito
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 
@@ -97,22 +103,29 @@
 @SmallTest
 class ApfNewTest {
 
-    @get:Rule
-    val ignoreRule = DevSdkIgnoreRule()
+    @get:Rule val ignoreRule = DevSdkIgnoreRule()
 
-    @Mock
-    private lateinit var context: Context
+    @Mock private lateinit var context: Context
 
-    @Mock
-    private lateinit var metrics: NetworkQuirkMetrics
+    @Mock private lateinit var metrics: NetworkQuirkMetrics
 
-    @Mock
-    private lateinit var dependencies: Dependencies
+    @Mock private lateinit var dependencies: Dependencies
+
+    @Mock private lateinit var ipClientCallback: IpClientCallbacksWrapper
 
     private val defaultMaximumApfProgramSize = 2048
 
-    private val testPacket = byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8,
-                                         9, 10, 11, 12, 13, 14, 15, 16)
+    private val loInterfaceParams = InterfaceParams.getByName("lo")
+
+    private val ifParams =
+        InterfaceParams(
+            "lo",
+            loInterfaceParams.index,
+            MacAddress.fromBytes(byteArrayOf(2, 3, 4, 5, 6, 7)),
+            loInterfaceParams.defaultMtu
+        )
+
+    private val testPacket = byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)
     private val hostIpv4Address = byteArrayOf(10, 0, 0, 1)
     private val senderIpv4Address = byteArrayOf(10, 0, 0, 2)
     private val arpBroadcastMacAddress = intArrayOf(0xff, 0xff, 0xff, 0xff, 0xff, 0xff)
@@ -127,6 +140,14 @@
         intArrayOf(0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x01, 0, 0, 0x1b, 0x44, 0x55, 0x66, 0x77)
             .map{ it.toByte() }.toByteArray()
     )
+    private val hostIpv6TentativeAddresses = listOf(
+        // 2001::200:1a:1234:5678
+        intArrayOf(0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x02, 0, 0, 0x1a, 0x12, 0x34, 0x56, 0x78)
+            .map{ it.toByte() }.toByteArray(),
+        // 2001::100:1b:1234:5678
+        intArrayOf(0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x01, 0, 0, 0x1b, 0x12, 0x34, 0x56, 0x78)
+            .map{ it.toByte() }.toByteArray()
+    )
     private val hostAnycast6Addresses = listOf(
         // 2001::100:1b:aabb:ccdd
         intArrayOf(0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x01, 0, 0, 0x1b, 0xaa, 0xbb, 0xcc, 0xdd)
@@ -1284,16 +1305,18 @@
     }
 
     private fun doTestEtherTypeAllowListFilter(apfVersion: Int) {
-        val ipClientCallback = ApfTestUtils.MockIpClientCallback()
-        val apfFilter = TestApfFilter(
+        val programCaptor = ArgumentCaptor.forClass(ByteArray::class.java)
+        val apfFilter =
+            ApfFilter(
                 context,
                 getDefaultConfig(apfVersion),
+                ifParams,
                 ipClientCallback,
                 metrics,
                 dependencies
-        )
-
-        val program = ipClientCallback.assertProgramUpdateAndGet()
+            )
+        verify(ipClientCallback, times(2)).installPacketFilter(programCaptor.capture())
+        val program = programCaptor.allValues.last()
 
         // Using scapy to generate IPv4 mDNS packet:
         //   eth = Ether(src="E8:9F:80:66:60:BB", dst="01:00:5E:00:00:FB")
@@ -1724,16 +1747,19 @@
 
     @Test
     fun testIPv4PacketFilterOnV6OnlyNetwork() {
-        val ipClientCallback = ApfTestUtils.MockIpClientCallback()
-        val apfFilter = TestApfFilter(
+        val apfFilter =
+            ApfFilter(
                 context,
                 getDefaultConfig(),
+                ifParams,
                 ipClientCallback,
                 metrics,
                 dependencies
         )
         apfFilter.updateClatInterfaceState(true)
-        val program = ipClientCallback.assertProgramUpdateAndGet()
+        val programCaptor = ArgumentCaptor.forClass(ByteArray::class.java)
+        verify(ipClientCallback, times(3)).installPacketFilter(programCaptor.capture())
+        val program = programCaptor.allValues.last()
 
         // Using scapy to generate IPv4 mDNS packet:
         //   eth = Ether(src="E8:9F:80:66:60:BB", dst="01:00:5E:00:00:FB")
@@ -1806,20 +1832,23 @@
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Test
     fun testArpTransmit() {
-        val ipClientCallback = ApfTestUtils.MockIpClientCallback()
-        val apfFilter = TestApfFilter(
+        val apfFilter =
+            ApfFilter(
                 context,
                 getDefaultConfig(),
+                ifParams,
                 ipClientCallback,
                 metrics,
                 dependencies
         )
+        verify(ipClientCallback, times(2)).installPacketFilter(any())
         val linkAddress = LinkAddress(InetAddress.getByAddress(hostIpv4Address), 24)
         val lp = LinkProperties()
         lp.addLinkAddress(linkAddress)
-        ipClientCallback.resetApfProgramWait()
         apfFilter.setLinkProperties(lp)
-        val program = ipClientCallback.assertProgramUpdateAndGet()
+        val programCaptor = ArgumentCaptor.forClass(ByteArray::class.java)
+        verify(ipClientCallback, times(3)).installPacketFilter(programCaptor.capture())
+        val program = programCaptor.value
         val receivedArpPacketBuf = ArpPacket.buildArpPacket(
                 arpBroadcastMacAddress,
                 senderMacAddress,
@@ -1854,18 +1883,20 @@
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     fun testNsFilterNoIPv6() {
         `when`(dependencies.getAnycast6Addresses(any())).thenReturn(listOf())
-        val ipClientCallback = ApfTestUtils.MockIpClientCallback()
-        val apfFilter = TestApfFilter(
+        val apfFilter =
+            ApfFilter(
                 context,
                 getDefaultConfig(),
+                ifParams,
                 ipClientCallback,
                 metrics,
                 dependencies
         )
 
         // validate NS packet check when there is no IPv6 address
-
-        var program = ipClientCallback.assertProgramUpdateAndGet()
+        val programCaptor = ArgumentCaptor.forClass(ByteArray::class.java)
+        verify(ipClientCallback, times(2)).installPacketFilter(programCaptor.capture())
+        val program = programCaptor.allValues.last()
         // Using scapy to generate IPv6 NS packet:
         // eth = Ether(src="00:01:02:03:04:05", dst="01:02:03:04:05:06")
         // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
@@ -1888,26 +1919,30 @@
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     fun testNsFilter() {
-        val ipClientCallback = ApfTestUtils.MockIpClientCallback()
-        val apfFilter = TestApfFilter(
-            context,
-            getDefaultConfig(),
-            ipClientCallback,
-            metrics,
-            dependencies
+        val apfFilter =
+            ApfFilter(
+                context,
+                getDefaultConfig(),
+                ifParams,
+                ipClientCallback,
+                metrics,
+                dependencies
         )
+        verify(ipClientCallback, times(2)).installPacketFilter(any())
 
         // validate Ethernet dst address check
 
         val lp = LinkProperties()
-        ipClientCallback.resetApfProgramWait()
         for (addr in hostIpv6Addresses) {
             lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
         }
 
         apfFilter.setLinkProperties(lp)
+        verify(ipClientCallback, times(3)).installPacketFilter(any())
         apfFilter.updateClatInterfaceState(true)
-        val program = ipClientCallback.assertProgramUpdateAndGet()
+        val programCaptor = ArgumentCaptor.forClass(ByteArray::class.java)
+        verify(ipClientCallback, times(4)).installPacketFilter(programCaptor.capture())
+        val program = programCaptor.value
 
         // Using scapy to generate IPv6 NS packet:
         // eth = Ether(src="00:01:02:03:04:05", dst="00:05:04:03:02:01")
@@ -2148,42 +2183,52 @@
 
     @Test
     fun testApfProgramUpdate() {
-        val ipClientCallback = ApfTestUtils.MockIpClientCallback()
-        val apfFilter = TestApfFilter(
-            context,
-            getDefaultConfig(),
-            ipClientCallback,
-            metrics,
-            dependencies
+        val apfFilter =
+            ApfFilter(
+                context,
+                getDefaultConfig(),
+                ifParams,
+                ipClientCallback,
+                metrics,
+                dependencies
         )
 
-        val lp = LinkProperties()
-
+        verify(ipClientCallback, times(2)).installPacketFilter(any())
         // add IPv4 address, expect to have apf program update
-        ipClientCallback.resetApfProgramWait()
+        val lp = LinkProperties()
         val linkAddress = LinkAddress(InetAddress.getByAddress(hostIpv4Address), 24)
         lp.addLinkAddress(linkAddress)
         apfFilter.setLinkProperties(lp)
-        ipClientCallback.assertProgramUpdateAndGet()
+        verify(ipClientCallback, times(3)).installPacketFilter(any())
 
         // add the same IPv4 address, expect to have no apf program update
-        ipClientCallback.resetApfProgramWait()
         apfFilter.setLinkProperties(lp)
-        ipClientCallback.assertNoProgramUpdate()
+        verify(ipClientCallback, times(3)).installPacketFilter(any())
 
         // add IPv6 addresses, expect to have apf program update
-        ipClientCallback.resetApfProgramWait()
         for (addr in hostIpv6Addresses) {
             lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
         }
 
         apfFilter.setLinkProperties(lp)
-        ipClientCallback.assertProgramUpdateAndGet()
+        verify(ipClientCallback, times(4)).installPacketFilter(any())
 
         // add the same IPv6 addresses, expect to have no apf program update
-        ipClientCallback.resetApfProgramWait()
         apfFilter.setLinkProperties(lp)
-        ipClientCallback.assertNoProgramUpdate()
+        verify(ipClientCallback, times(4)).installPacketFilter(any())
+
+        // add more tentative IPv6 addresses, expect to have apf program update
+        for (addr in hostIpv6TentativeAddresses) {
+            lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64, IFA_F_TENTATIVE, 0))
+        }
+
+        apfFilter.setLinkProperties(lp)
+        verify(ipClientCallback, times(5)).installPacketFilter(any())
+
+        // add the same IPv6 addresses, expect to have no apf program update
+        apfFilter.setLinkProperties(lp)
+        verify(ipClientCallback, times(5)).installPacketFilter(any())
+        apfFilter.shutdown()
     }
 
     private fun verifyProgramRun(
diff --git a/tests/unit/src/android/net/apf/ApfTest.java b/tests/unit/src/android/net/apf/ApfTest.java
index 33c89a7..05e2e39 100644
--- a/tests/unit/src/android/net/apf/ApfTest.java
+++ b/tests/unit/src/android/net/apf/ApfTest.java
@@ -73,7 +73,6 @@
 import android.net.apf.ApfFilter.ApfConfiguration;
 import android.net.apf.ApfTestUtils.MockIpClientCallback;
 import android.net.apf.ApfTestUtils.TestApfFilter;
-import android.net.apf.ApfTestUtils.TestLegacyApfFilter;
 import android.net.apf.BaseApfGenerator.IllegalInstructionException;
 import android.net.metrics.IpConnectivityLog;
 import android.os.Build;
@@ -3135,33 +3134,17 @@
     }
 
     private TestAndroidPacketFilter makeTestApfFilter(ApfConfiguration config,
-            MockIpClientCallback ipClientCallback, boolean isLegacy) throws Exception {
-        final TestAndroidPacketFilter apfFilter;
-        if (isLegacy) {
-            apfFilter = new TestLegacyApfFilter(mContext, config, ipClientCallback,
-                    mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
-        } else {
-            apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mNetworkQuirkMetrics,
+            MockIpClientCallback ipClientCallback) throws Exception {
+        return new TestApfFilter(mContext, config, ipClientCallback, mNetworkQuirkMetrics,
                     mDependencies, mClock);
-        }
-        return apfFilter;
     }
 
-    // LegacyApfFilter ignores zero lifetime RAs (doesn't update program) but ApfFilter won't.
-    private void verifyUpdateProgramForZeroLifetimeRa(MockIpClientCallback ipClientCallback,
-            boolean isLegacy) {
-        if (isLegacy) {
-            ipClientCallback.assertNoProgramUpdate();
-        } else {
-            ipClientCallback.assertProgramUpdateAndGet();
-        }
-    }
 
-    private void verifyInstallPacketFilterFailure(boolean isLegacy) throws Exception {
+    @Test
+    public void testInstallPacketFilterFailure() throws Exception {
         final MockIpClientCallback ipClientCallback = new MockIpClientCallback(false);
         final ApfConfiguration config = getDefaultConfig();
-        final TestAndroidPacketFilter apfFilter =
-                makeTestApfFilter(config, ipClientCallback, isLegacy);
+        final TestAndroidPacketFilter apfFilter = makeTestApfFilter(config, ipClientCallback);
         verify(mNetworkQuirkMetrics).setEvent(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
         verify(mNetworkQuirkMetrics).statsWrite();
         reset(mNetworkQuirkMetrics);
@@ -3172,23 +3155,14 @@
         verify(mNetworkQuirkMetrics).statsWrite();
     }
 
-    @Test
-    public void testInstallPacketFilterFailure() throws Exception {
-        verifyInstallPacketFilterFailure(false /* isLegacy */);
-    }
 
     @Test
-    public void testInstallPacketFilterFailure_LegacyApfFilter() throws Exception {
-        verifyInstallPacketFilterFailure(true /* isLegacy */);
-    }
-
-    private void verifyApfProgramOverSize(boolean isLegacy) throws Exception {
+    public void testApfProgramOverSize() throws Exception {
         final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
         final ApfConfiguration config = getDefaultConfig();
         final ApfCapabilities capabilities = new ApfCapabilities(2, 512, ARPHRD_ETHER);
         config.apfCapabilities = capabilities;
-        final TestAndroidPacketFilter apfFilter =
-                makeTestApfFilter(config, ipClientCallback, isLegacy);
+        final TestAndroidPacketFilter apfFilter = makeTestApfFilter(config, ipClientCallback);
         byte[] program = ipClientCallback.assertProgramUpdateAndGet();
         final byte[] ra = buildLargeRa();
         apfFilter.pretendPacketReceived(ra);
@@ -3199,27 +3173,12 @@
     }
 
     @Test
-    public void testApfProgramOverSize() throws Exception {
-        verifyApfProgramOverSize(false /* isLegacy */);
-    }
-
-    @Test
-    public void testApfProgramOverSize_LegacyApfFilter() throws Exception {
-        verifyApfProgramOverSize(true /* isLegacy */);
-    }
-
-    private void verifyGenerateApfProgramException(boolean isLegacy) throws Exception {
+    public void testGenerateApfProgramException() throws Exception {
         final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
         final ApfConfiguration config = getDefaultConfig();
         final TestAndroidPacketFilter apfFilter;
-        if (isLegacy) {
-            apfFilter = new TestLegacyApfFilter(mContext, config, ipClientCallback,
-                    mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies,
-                    true /* throwsExceptionWhenGeneratesProgram */);
-        } else {
-            apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mNetworkQuirkMetrics,
-                    mDependencies, true /* throwsExceptionWhenGeneratesProgram */);
-        }
+        apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mNetworkQuirkMetrics,
+                mDependencies, true /* throwsExceptionWhenGeneratesProgram */);
         synchronized (apfFilter) {
             apfFilter.installNewProgramLocked();
         }
@@ -3228,16 +3187,7 @@
     }
 
     @Test
-    public void testGenerateApfProgramException() throws Exception {
-        verifyGenerateApfProgramException(false /* isLegacy */);
-    }
-
-    @Test
-    public void testGenerateApfProgramException_LegacyApfFilter() throws Exception {
-        verifyGenerateApfProgramException(true /* isLegacy */);
-    }
-
-    private void verifyApfSessionInfoMetrics(boolean isLegacy) throws Exception {
+    public void testApfSessionInfoMetrics() throws Exception {
         final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
         final ApfConfiguration config = getDefaultConfig();
         final ApfCapabilities capabilities = new ApfCapabilities(4, 4096, ARPHRD_ETHER);
@@ -3245,8 +3195,7 @@
         final long startTimeMs = 12345;
         final long durationTimeMs = config.minMetricsSessionDurationMs;
         doReturn(startTimeMs).when(mClock).elapsedRealtime();
-        final TestAndroidPacketFilter apfFilter =
-                makeTestApfFilter(config, ipClientCallback, isLegacy);
+        final TestAndroidPacketFilter apfFilter = makeTestApfFilter(config, ipClientCallback);
         int maxProgramSize = 0;
         int numProgramUpdated = 0;
         byte[] program = ipClientCallback.assertProgramUpdateAndGet();
@@ -3318,23 +3267,13 @@
     }
 
     @Test
-    public void testApfSessionInfoMetrics() throws Exception {
-        verifyApfSessionInfoMetrics(false /* isLegacy */);
-    }
-
-    @Test
-    public void testApfSessionInfoMetrics_LegacyApfFilter() throws Exception {
-        verifyApfSessionInfoMetrics(true /* isLegacy */);
-    }
-
-    private void verifyIpClientRaInfoMetrics(boolean isLegacy) throws Exception {
+    public void testIpClientRaInfoMetrics() throws Exception {
         final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
         final ApfConfiguration config = getDefaultConfig();
         final long startTimeMs = 12345;
         final long durationTimeMs = config.minMetricsSessionDurationMs;
         doReturn(startTimeMs).when(mClock).elapsedRealtime();
-        final TestAndroidPacketFilter apfFilter =
-                makeTestApfFilter(config, ipClientCallback, isLegacy);
+        final TestAndroidPacketFilter apfFilter = makeTestApfFilter(config, ipClientCallback);
         byte[] program = ipClientCallback.assertProgramUpdateAndGet();
 
         final int routerLifetime = 1000;
@@ -3379,27 +3318,20 @@
         apfFilter.pretendPacketReceived(raInvalid.build());
         ipClientCallback.assertNoProgramUpdate();
         apfFilter.pretendPacketReceived(raZeroRouterLifetime.build());
-        verifyUpdateProgramForZeroLifetimeRa(ipClientCallback, isLegacy);
+        ipClientCallback.assertProgramUpdateAndGet();
         apfFilter.pretendPacketReceived(raZeroPioValidLifetime.build());
-        verifyUpdateProgramForZeroLifetimeRa(ipClientCallback, isLegacy);
+        ipClientCallback.assertProgramUpdateAndGet();
         apfFilter.pretendPacketReceived(raZeroRdnssLifetime.build());
-        verifyUpdateProgramForZeroLifetimeRa(ipClientCallback, isLegacy);
+        ipClientCallback.assertProgramUpdateAndGet();
         apfFilter.pretendPacketReceived(raZeroRioRouteLifetime.build());
-        verifyUpdateProgramForZeroLifetimeRa(ipClientCallback, isLegacy);
+        ipClientCallback.assertProgramUpdateAndGet();
 
         // Write metrics data to statsd pipeline when shutdown.
         doReturn(startTimeMs + durationTimeMs).when(mClock).elapsedRealtime();
         apfFilter.shutdown();
 
         // Verify each metric fields in IpClientRaInfoMetrics.
-        if (isLegacy) {
-            // LegacyApfFilter will purge expired RAs before adding new RA. Every time a new zero
-            // lifetime RA is received, zero lifetime RAs except the newly added one will be
-            // cleared, so the number of distinct RAs is 3 (ra1, ra2 and the newly added RA).
-            verify(mIpClientRaInfoMetrics).setMaxNumberOfDistinctRas(3);
-        } else {
-            verify(mIpClientRaInfoMetrics).setMaxNumberOfDistinctRas(6);
-        }
+        verify(mIpClientRaInfoMetrics).setMaxNumberOfDistinctRas(6);
         verify(mIpClientRaInfoMetrics).setNumberOfZeroLifetimeRas(4);
         verify(mIpClientRaInfoMetrics).setNumberOfParsingErrorRas(1);
         verify(mIpClientRaInfoMetrics).setLowestRouterLifetimeSeconds(routerLifetime);
@@ -3409,16 +3341,6 @@
         verify(mIpClientRaInfoMetrics).statsWrite();
     }
 
-    @Test
-    public void testIpClientRaInfoMetrics() throws Exception {
-        verifyIpClientRaInfoMetrics(false /* isLegacy */);
-    }
-
-    @Test
-    public void testIpClientRaInfoMetrics_LegacyApfFilter() throws Exception {
-        verifyIpClientRaInfoMetrics(true /* isLegacy */);
-    }
-
     private void verifyNoMetricsWrittenForShortDuration(boolean isLegacy) throws Exception {
         final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
         final ApfConfiguration config = getDefaultConfig();
@@ -3427,8 +3349,7 @@
 
         // Verify no metrics data written to statsd for duration less than durationTimeMs.
         doReturn(startTimeMs).when(mClock).elapsedRealtime();
-        final TestAndroidPacketFilter apfFilter =
-                makeTestApfFilter(config, ipClientCallback, isLegacy);
+        final TestAndroidPacketFilter apfFilter = makeTestApfFilter(config, ipClientCallback);
         doReturn(startTimeMs + durationTimeMs - 1).when(mClock).elapsedRealtime();
         apfFilter.shutdown();
         verify(mApfSessionInfoMetrics, never()).statsWrite();
diff --git a/tests/unit/src/android/net/apf/LegacyApfTest.java b/tests/unit/src/android/net/apf/LegacyApfTest.java
new file mode 100644
index 0000000..f51ecdd
--- /dev/null
+++ b/tests/unit/src/android/net/apf/LegacyApfTest.java
@@ -0,0 +1,2267 @@
+/*
+ * Copyright (C) 2012 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.ApfJniUtils.dropsAllPackets;
+import static android.net.apf.ApfTestUtils.DROP;
+import static android.net.apf.ApfTestUtils.PASS;
+import static android.os.PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED;
+import static android.os.PowerManager.ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED;
+import static android.system.OsConstants.ARPHRD_ETHER;
+import static android.system.OsConstants.ETH_P_ARP;
+import static android.system.OsConstants.ETH_P_IP;
+import static android.system.OsConstants.ETH_P_IPV6;
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+import static com.android.net.module.util.HexDump.hexStringToByteArray;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.NattKeepalivePacketDataParcelable;
+import android.net.TcpKeepalivePacketDataParcelable;
+import android.net.apf.ApfCounterTracker.Counter;
+import android.net.apf.ApfFilter.ApfConfiguration;
+import android.net.apf.ApfTestUtils.MockIpClientCallback;
+import android.net.apf.ApfTestUtils.TestApfFilter;
+import android.net.apf.ApfTestUtils.TestLegacyApfFilter;
+import android.net.metrics.IpConnectivityLog;
+import android.os.Build;
+import android.os.PowerManager;
+import android.stats.connectivity.NetworkQuirkEvent;
+import android.system.ErrnoException;
+import android.text.format.DateUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.HexDump;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.NetworkStackConstants;
+import com.android.networkstack.metrics.ApfSessionInfoMetrics;
+import com.android.networkstack.metrics.IpClientRaInfoMetrics;
+import com.android.networkstack.metrics.NetworkQuirkMetrics;
+import com.android.server.networkstack.tests.R;
+import com.android.testutils.ConcurrentUtils;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import libcore.io.Streams;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+
+/**
+ * Tests for APF program generator and interpreter.
+ *
+ * The test cases will be executed by both APFv4 and APFv6 interpreter.
+ */
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+public class LegacyApfTest {
+    private static final int APF_VERSION_2 = 2;
+
+    @Rule
+    public DevSdkIgnoreRule mDevSdkIgnoreRule = new DevSdkIgnoreRule();
+    // Indicates which apf interpreter to run.
+    @Parameterized.Parameter()
+    public int mApfVersion;
+
+    @Parameterized.Parameters
+    public static Iterable<? extends Object> data() {
+        return Arrays.asList(4, 6);
+    }
+
+    @Mock private Context mContext;
+    @Mock
+    private ApfFilter.Dependencies mDependencies;
+    @Mock private PowerManager mPowerManager;
+    @Mock private IpConnectivityLog mIpConnectivityLog;
+    @Mock private NetworkQuirkMetrics mNetworkQuirkMetrics;
+    @Mock private ApfSessionInfoMetrics mApfSessionInfoMetrics;
+    @Mock private IpClientRaInfoMetrics mIpClientRaInfoMetrics;
+    @Mock private ApfFilter.Clock mClock;
+    @GuardedBy("mApfFilterCreated")
+    private final ArrayList<AndroidPacketFilter> mApfFilterCreated = new ArrayList<>();
+    @GuardedBy("mThreadsToBeCleared")
+    private final ArrayList<Thread> mThreadsToBeCleared = new ArrayList<>();
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        doReturn(mPowerManager).when(mContext).getSystemService(PowerManager.class);
+        doReturn(mApfSessionInfoMetrics).when(mDependencies).getApfSessionInfoMetrics();
+        doReturn(mIpClientRaInfoMetrics).when(mDependencies).getIpClientRaInfoMetrics();
+        doAnswer((invocation) -> {
+            synchronized (mApfFilterCreated) {
+                mApfFilterCreated.add(invocation.getArgument(0));
+            }
+            return null;
+        }).when(mDependencies).onApfFilterCreated(any());
+        doAnswer((invocation) -> {
+            synchronized (mThreadsToBeCleared) {
+                mThreadsToBeCleared.add(invocation.getArgument(0));
+            }
+            return null;
+        }).when(mDependencies).onThreadCreated(any());
+    }
+
+    private void quitThreads() throws Exception {
+        ConcurrentUtils.quitThreads(
+                THREAD_QUIT_MAX_RETRY_COUNT,
+                false /* interrupt */,
+                HANDLER_TIMEOUT_MS,
+                () -> {
+                    synchronized (mThreadsToBeCleared) {
+                        final ArrayList<Thread> ret = new ArrayList<>(mThreadsToBeCleared);
+                        mThreadsToBeCleared.clear();
+                        return ret;
+                    }
+                });
+    }
+
+    private void shutdownApfFilters() throws Exception {
+        ConcurrentUtils.quitResources(THREAD_QUIT_MAX_RETRY_COUNT, () -> {
+            synchronized (mApfFilterCreated) {
+                final ArrayList<AndroidPacketFilter> ret =
+                        new ArrayList<>(mApfFilterCreated);
+                mApfFilterCreated.clear();
+                return ret;
+            }
+        }, (apf) -> {
+            apf.shutdown();
+        });
+        synchronized (mApfFilterCreated) {
+            assertEquals("ApfFilters did not fully shutdown.",
+                    0, mApfFilterCreated.size());
+        }
+        // It's necessary to wait until all ReceiveThreads have finished running because
+        // clearInlineMocks clears all Mock objects, including some privilege frameworks
+        // required by logStats, at the end of ReceiveThread#run.
+        quitThreads();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        shutdownApfFilters();
+        // Clear mocks to prevent from stubs holding instances and cause memory leaks.
+        Mockito.framework().clearInlineMocks();
+    }
+
+    private static final String TAG = "ApfTest";
+    // Expected return codes from APF interpreter.
+    private static final ApfCapabilities MOCK_APF_CAPABILITIES =
+            new ApfCapabilities(2, 4096, ARPHRD_ETHER);
+
+    private static final boolean DROP_MULTICAST = true;
+    private static final boolean ALLOW_MULTICAST = false;
+
+    private static final boolean DROP_802_3_FRAMES = true;
+    private static final boolean ALLOW_802_3_FRAMES = false;
+
+    private static final int MIN_RDNSS_LIFETIME_SEC = 0;
+    private static final int MIN_METRICS_SESSION_DURATIONS_MS = 300_000;
+
+    private static final int HANDLER_TIMEOUT_MS = 1000;
+    private static final int THREAD_QUIT_MAX_RETRY_COUNT = 3;
+
+    // Constants for opcode encoding
+    private static final byte LI_OP   = (byte)(13 << 3);
+    private static final byte LDDW_OP = (byte)(22 << 3);
+    private static final byte STDW_OP = (byte)(23 << 3);
+    private static final byte SIZE0   = (byte)(0 << 1);
+    private static final byte SIZE8   = (byte)(1 << 1);
+    private static final byte SIZE16  = (byte)(2 << 1);
+    private static final byte SIZE32  = (byte)(3 << 1);
+    private static final byte R1_REG = 1;
+
+    private static ApfConfiguration getDefaultConfig() {
+        ApfFilter.ApfConfiguration config = new ApfConfiguration();
+        config.apfCapabilities = MOCK_APF_CAPABILITIES;
+        config.multicastFilter = ALLOW_MULTICAST;
+        config.ieee802_3Filter = ALLOW_802_3_FRAMES;
+        config.ethTypeBlackList = new int[0];
+        config.minRdnssLifetimeSec = MIN_RDNSS_LIFETIME_SEC;
+        config.minRdnssLifetimeSec = 67;
+        config.minMetricsSessionDurationMs = MIN_METRICS_SESSION_DURATIONS_MS;
+        return config;
+    }
+
+    private void assertPass(ApfV4Generator gen) throws ApfV4Generator.IllegalInstructionException {
+        ApfTestUtils.assertPass(mApfVersion, gen);
+    }
+
+    private void assertDrop(ApfV4Generator gen) throws ApfV4Generator.IllegalInstructionException {
+        ApfTestUtils.assertDrop(mApfVersion, gen);
+    }
+
+    private void assertPass(byte[] program, byte[] packet) {
+        ApfTestUtils.assertPass(mApfVersion, program, packet);
+    }
+
+    private void assertDrop(byte[] program, byte[] packet) {
+        ApfTestUtils.assertDrop(mApfVersion, program, packet);
+    }
+
+    private void assertPass(byte[] program, byte[] packet, int filterAge) {
+        ApfTestUtils.assertPass(mApfVersion, program, packet, filterAge);
+    }
+
+    private void assertDrop(byte[] program, byte[] packet, int filterAge) {
+        ApfTestUtils.assertDrop(mApfVersion, program, packet, filterAge);
+    }
+
+    private void assertPass(ApfV4Generator gen, byte[] packet, int filterAge)
+            throws ApfV4Generator.IllegalInstructionException {
+        ApfTestUtils.assertPass(mApfVersion, gen, packet, filterAge);
+    }
+
+    private void assertDrop(ApfV4Generator gen, byte[] packet, int filterAge)
+            throws ApfV4Generator.IllegalInstructionException {
+        ApfTestUtils.assertDrop(mApfVersion, gen, packet, filterAge);
+    }
+
+    private void assertDataMemoryContents(int expected, byte[] program, byte[] packet,
+            byte[] data, byte[] expectedData) throws Exception {
+        ApfTestUtils.assertDataMemoryContents(mApfVersion, expected, program, packet, data,
+                expectedData, false /* ignoreInterpreterVersion */);
+    }
+
+    private void assertDataMemoryContentsIgnoreVersion(int expected, byte[] program,
+            byte[] packet, byte[] data, byte[] expectedData) throws Exception {
+        ApfTestUtils.assertDataMemoryContents(mApfVersion, expected, program, packet, data,
+                expectedData, true /* ignoreInterpreterVersion */);
+    }
+
+    private void assertVerdict(String msg, int expected, byte[] program,
+            byte[] packet, int filterAge) {
+        ApfTestUtils.assertVerdict(mApfVersion, msg, expected, program, packet, filterAge);
+    }
+
+    private void assertVerdict(int expected, byte[] program, byte[] packet) {
+        ApfTestUtils.assertVerdict(mApfVersion, expected, program, packet);
+    }
+
+    /**
+     * Generate APF program, run pcap file though APF filter, then check all the packets in the file
+     * should be dropped.
+     */
+    @Test
+    public void testApfFilterPcapFile() throws Exception {
+        final byte[] MOCK_PCAP_IPV4_ADDR = {(byte) 172, 16, 7, (byte) 151};
+        String pcapFilename = stageFile(R.raw.apfPcap);
+        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        LinkAddress link = new LinkAddress(InetAddress.getByAddress(MOCK_PCAP_IPV4_ADDR), 16);
+        LinkProperties lp = new LinkProperties();
+        lp.addLinkAddress(link);
+
+        ApfConfiguration config = getDefaultConfig();
+        ApfCapabilities MOCK_APF_PCAP_CAPABILITIES = new ApfCapabilities(4, 1700, ARPHRD_ETHER);
+        config.apfCapabilities = MOCK_APF_PCAP_CAPABILITIES;
+        config.multicastFilter = DROP_MULTICAST;
+        config.ieee802_3Filter = DROP_802_3_FRAMES;
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics, mDependencies);
+        apfFilter.setLinkProperties(lp);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        byte[] data = new byte[Counter.totalSize()];
+        final boolean result;
+
+        result = dropsAllPackets(mApfVersion, program, data, pcapFilename);
+        Log.i(TAG, "testApfFilterPcapFile(): Data counters: " + HexDump.toHexString(data, false));
+
+        assertTrue("Failed to drop all packets by filter. \nAPF counters:" +
+            HexDump.toHexString(data, false), result);
+    }
+
+    private static final int ETH_HEADER_LEN               = 14;
+    private static final int ETH_DEST_ADDR_OFFSET         = 0;
+    private static final int ETH_ETHERTYPE_OFFSET         = 12;
+    private static final byte[] ETH_BROADCAST_MAC_ADDRESS =
+            {(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff };
+    private static final byte[] ETH_MULTICAST_MDNS_v4_MAC_ADDRESS =
+            {(byte) 0x01, (byte) 0x00, (byte) 0x5e, (byte) 0x00, (byte) 0x00, (byte) 0xfb};
+    private static final byte[] ETH_MULTICAST_MDNS_V6_MAC_ADDRESS =
+            {(byte) 0x33, (byte) 0x33, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xfb};
+
+    private static final int IP_HEADER_OFFSET = ETH_HEADER_LEN;
+
+    private static final int IPV4_HEADER_LEN          = 20;
+    private static final int IPV4_TOTAL_LENGTH_OFFSET = IP_HEADER_OFFSET + 2;
+    private static final int IPV4_PROTOCOL_OFFSET     = IP_HEADER_OFFSET + 9;
+    private static final int IPV4_SRC_ADDR_OFFSET     = IP_HEADER_OFFSET + 12;
+    private static final int IPV4_DEST_ADDR_OFFSET    = IP_HEADER_OFFSET + 16;
+
+    private static final int IPV4_TCP_HEADER_LEN           = 20;
+    private static final int IPV4_TCP_HEADER_OFFSET        = IP_HEADER_OFFSET + IPV4_HEADER_LEN;
+    private static final int IPV4_TCP_SRC_PORT_OFFSET      = IPV4_TCP_HEADER_OFFSET + 0;
+    private static final int IPV4_TCP_DEST_PORT_OFFSET     = IPV4_TCP_HEADER_OFFSET + 2;
+    private static final int IPV4_TCP_SEQ_NUM_OFFSET       = IPV4_TCP_HEADER_OFFSET + 4;
+    private static final int IPV4_TCP_ACK_NUM_OFFSET       = IPV4_TCP_HEADER_OFFSET + 8;
+    private static final int IPV4_TCP_HEADER_LENGTH_OFFSET = IPV4_TCP_HEADER_OFFSET + 12;
+    private static final int IPV4_TCP_HEADER_FLAG_OFFSET   = IPV4_TCP_HEADER_OFFSET + 13;
+
+    private static final int IPV4_UDP_HEADER_OFFSET    = IP_HEADER_OFFSET + IPV4_HEADER_LEN;
+    private static final int IPV4_UDP_SRC_PORT_OFFSET  = IPV4_UDP_HEADER_OFFSET + 0;
+    private static final int IPV4_UDP_DEST_PORT_OFFSET = IPV4_UDP_HEADER_OFFSET + 2;
+    private static final int IPV4_UDP_LENGTH_OFFSET    = IPV4_UDP_HEADER_OFFSET + 4;
+    private static final int IPV4_UDP_PAYLOAD_OFFSET   = IPV4_UDP_HEADER_OFFSET + 8;
+    private static final byte[] IPV4_BROADCAST_ADDRESS =
+            {(byte) 255, (byte) 255, (byte) 255, (byte) 255};
+
+    private static final int IPV6_HEADER_LEN             = 40;
+    private static final int IPV6_PAYLOAD_LENGTH_OFFSET  = IP_HEADER_OFFSET + 4;
+    private static final int IPV6_NEXT_HEADER_OFFSET     = IP_HEADER_OFFSET + 6;
+    private static final int IPV6_SRC_ADDR_OFFSET        = IP_HEADER_OFFSET + 8;
+    private static final int IPV6_DEST_ADDR_OFFSET       = IP_HEADER_OFFSET + 24;
+    private static final int IPV6_PAYLOAD_OFFSET = IP_HEADER_OFFSET + IPV6_HEADER_LEN;
+    private static final int IPV6_TCP_SRC_PORT_OFFSET    = IPV6_PAYLOAD_OFFSET + 0;
+    private static final int IPV6_TCP_DEST_PORT_OFFSET   = IPV6_PAYLOAD_OFFSET + 2;
+    private static final int IPV6_TCP_SEQ_NUM_OFFSET     = IPV6_PAYLOAD_OFFSET + 4;
+    private static final int IPV6_TCP_ACK_NUM_OFFSET     = IPV6_PAYLOAD_OFFSET + 8;
+    // The IPv6 all nodes address ff02::1
+    private static final byte[] IPV6_ALL_NODES_ADDRESS   =
+            { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
+    private static final byte[] IPV6_ALL_ROUTERS_ADDRESS =
+            { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 };
+    private static final byte[] IPV6_SOLICITED_NODE_MULTICAST_ADDRESS = {
+            (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
+            (byte) 0xff, (byte) 0xab, (byte) 0xcd, (byte) 0xef,
+    };
+
+    private static final int ICMP6_TYPE_OFFSET           = IP_HEADER_OFFSET + IPV6_HEADER_LEN;
+    private static final int ICMP6_ROUTER_SOLICITATION   = 133;
+    private static final int ICMP6_ROUTER_ADVERTISEMENT  = 134;
+    private static final int ICMP6_NEIGHBOR_SOLICITATION = 135;
+    private static final int ICMP6_NEIGHBOR_ANNOUNCEMENT = 136;
+
+    private static final int ICMP6_RA_HEADER_LEN = 16;
+    private static final int ICMP6_RA_CHECKSUM_OFFSET =
+            IP_HEADER_OFFSET + IPV6_HEADER_LEN + 2;
+    private static final int ICMP6_RA_ROUTER_LIFETIME_OFFSET =
+            IP_HEADER_OFFSET + IPV6_HEADER_LEN + 6;
+    private static final int ICMP6_RA_REACHABLE_TIME_OFFSET =
+            IP_HEADER_OFFSET + IPV6_HEADER_LEN + 8;
+    private static final int ICMP6_RA_RETRANSMISSION_TIMER_OFFSET =
+            IP_HEADER_OFFSET + IPV6_HEADER_LEN + 12;
+    private static final int ICMP6_RA_OPTION_OFFSET =
+            IP_HEADER_OFFSET + IPV6_HEADER_LEN + ICMP6_RA_HEADER_LEN;
+
+    private static final int ICMP6_PREFIX_OPTION_TYPE                      = 3;
+    private static final int ICMP6_PREFIX_OPTION_LEN                       = 32;
+    private static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET     = 4;
+    private static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET = 8;
+
+    // From RFC6106: Recursive DNS Server option
+    private static final int ICMP6_RDNSS_OPTION_TYPE = 25;
+    // From RFC6106: DNS Search List option
+    private static final int ICMP6_DNSSL_OPTION_TYPE = 31;
+
+    // From RFC4191: Route Information option
+    private static final int ICMP6_ROUTE_INFO_OPTION_TYPE = 24;
+    // Above three options all have the same format:
+    private static final int ICMP6_4_BYTE_OPTION_LEN      = 8;
+    private static final int ICMP6_4_BYTE_LIFETIME_OFFSET = 4;
+    private static final int ICMP6_4_BYTE_LIFETIME_LEN    = 4;
+
+    private static final int UDP_HEADER_LEN              = 8;
+    private static final int UDP_DESTINATION_PORT_OFFSET = ETH_HEADER_LEN + 22;
+
+    private static final int DHCP_CLIENT_PORT       = 68;
+    private static final int DHCP_CLIENT_MAC_OFFSET = ETH_HEADER_LEN + UDP_HEADER_LEN + 48;
+
+    private static final int ARP_HEADER_OFFSET          = ETH_HEADER_LEN;
+    private static final byte[] ARP_IPV4_REQUEST_HEADER = {
+            0, 1, // Hardware type: Ethernet (1)
+            8, 0, // Protocol type: IP (0x0800)
+            6,    // Hardware size: 6
+            4,    // Protocol size: 4
+            0, 1  // Opcode: request (1)
+    };
+    private static final byte[] ARP_IPV4_REPLY_HEADER = {
+            0, 1, // Hardware type: Ethernet (1)
+            8, 0, // Protocol type: IP (0x0800)
+            6,    // Hardware size: 6
+            4,    // Protocol size: 4
+            0, 2  // Opcode: reply (2)
+    };
+    private static final int ARP_SOURCE_IP_ADDRESS_OFFSET = ARP_HEADER_OFFSET + 14;
+    private static final int ARP_TARGET_IP_ADDRESS_OFFSET = ARP_HEADER_OFFSET + 24;
+
+    private static final byte[] MOCK_IPV4_ADDR           = {10, 0, 0, 1};
+    private static final byte[] MOCK_BROADCAST_IPV4_ADDR = {10, 0, 31, (byte) 255}; // prefix = 19
+    private static final byte[] MOCK_MULTICAST_IPV4_ADDR = {(byte) 224, 0, 0, 1};
+    private static final byte[] ANOTHER_IPV4_ADDR        = {10, 0, 0, 2};
+    private static final byte[] IPV4_SOURCE_ADDR         = {10, 0, 0, 3};
+    private static final byte[] ANOTHER_IPV4_SOURCE_ADDR = {(byte) 192, 0, 2, 1};
+    private static final byte[] BUG_PROBE_SOURCE_ADDR1   = {0, 0, 1, 2};
+    private static final byte[] BUG_PROBE_SOURCE_ADDR2   = {3, 4, 0, 0};
+    private static final byte[] IPV4_ANY_HOST_ADDR       = {0, 0, 0, 0};
+    private static final byte[] IPV4_MDNS_MULTICAST_ADDR = {(byte) 224, 0, 0, (byte) 251};
+    private static final byte[] IPV6_MDNS_MULTICAST_ADDR =
+            {(byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (byte) 0xfb};
+    private static final int IPV6_UDP_DEST_PORT_OFFSET = IPV6_PAYLOAD_OFFSET + 2;
+    private static final int MDNS_UDP_PORT = 5353;
+
+    private static void setIpv4VersionFields(ByteBuffer packet) {
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short) ETH_P_IP);
+        packet.put(IP_HEADER_OFFSET, (byte) 0x45);
+    }
+
+    private static void setIpv6VersionFields(ByteBuffer packet) {
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short) ETH_P_IPV6);
+        packet.put(IP_HEADER_OFFSET, (byte) 0x60);
+    }
+
+    private static ByteBuffer makeIpv4Packet(int proto) {
+        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
+        setIpv4VersionFields(packet);
+        packet.put(IPV4_PROTOCOL_OFFSET, (byte) proto);
+        return packet;
+    }
+
+    private static ByteBuffer makeIpv6Packet(int nextHeader) {
+        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
+        setIpv6VersionFields(packet);
+        packet.put(IPV6_NEXT_HEADER_OFFSET, (byte) nextHeader);
+        return packet;
+    }
+
+    @Test
+    public void testApfFilterIPv4() throws Exception {
+        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        LinkAddress link = new LinkAddress(InetAddress.getByAddress(MOCK_IPV4_ADDR), 19);
+        LinkProperties lp = new LinkProperties();
+        lp.addLinkAddress(link);
+
+        ApfConfiguration config = getDefaultConfig();
+        config.multicastFilter = DROP_MULTICAST;
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics, mDependencies);
+        apfFilter.setLinkProperties(lp);
+
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
+        if (SdkLevel.isAtLeastV()) {
+            // Verify empty packet of 100 zero bytes is dropped
+            assertDrop(program, packet.array());
+        } else {
+            // Verify empty packet of 100 zero bytes is passed
+            assertPass(program, packet.array());
+        }
+
+        // Verify unicast IPv4 packet is passed
+        put(packet, ETH_DEST_ADDR_OFFSET, TestApfFilter.MOCK_MAC_ADDR);
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP);
+        put(packet, IPV4_DEST_ADDR_OFFSET, MOCK_IPV4_ADDR);
+        assertPass(program, packet.array());
+
+        // Verify L2 unicast to IPv4 broadcast addresses is dropped (b/30231088)
+        put(packet, IPV4_DEST_ADDR_OFFSET, IPV4_BROADCAST_ADDRESS);
+        assertDrop(program, packet.array());
+        put(packet, IPV4_DEST_ADDR_OFFSET, MOCK_BROADCAST_IPV4_ADDR);
+        assertDrop(program, packet.array());
+
+        // Verify multicast/broadcast IPv4, not DHCP to us, is dropped
+        put(packet, ETH_DEST_ADDR_OFFSET, ETH_BROADCAST_MAC_ADDRESS);
+        assertDrop(program, packet.array());
+        packet.put(IP_HEADER_OFFSET, (byte) 0x45);
+        assertDrop(program, packet.array());
+        packet.put(IPV4_PROTOCOL_OFFSET, (byte)IPPROTO_UDP);
+        assertDrop(program, packet.array());
+        packet.putShort(UDP_DESTINATION_PORT_OFFSET, (short)DHCP_CLIENT_PORT);
+        assertDrop(program, packet.array());
+        put(packet, IPV4_DEST_ADDR_OFFSET, MOCK_MULTICAST_IPV4_ADDR);
+        assertDrop(program, packet.array());
+        put(packet, IPV4_DEST_ADDR_OFFSET, MOCK_BROADCAST_IPV4_ADDR);
+        assertDrop(program, packet.array());
+        put(packet, IPV4_DEST_ADDR_OFFSET, IPV4_BROADCAST_ADDRESS);
+        assertDrop(program, packet.array());
+
+        // Verify broadcast IPv4 DHCP to us is passed
+        put(packet, DHCP_CLIENT_MAC_OFFSET, TestApfFilter.MOCK_MAC_ADDR);
+        assertPass(program, packet.array());
+
+        // Verify unicast IPv4 DHCP to us is passed
+        put(packet, ETH_DEST_ADDR_OFFSET, TestApfFilter.MOCK_MAC_ADDR);
+        assertPass(program, packet.array());
+    }
+
+    @Test
+    public void testApfFilterIPv6() throws Exception {
+        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        ApfConfiguration config = getDefaultConfig();
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics, mDependencies);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // Verify empty IPv6 packet is passed
+        ByteBuffer packet = makeIpv6Packet(IPPROTO_UDP);
+        assertPass(program, packet.array());
+
+        // Verify empty ICMPv6 packet is passed
+        packet.put(IPV6_NEXT_HEADER_OFFSET, (byte)IPPROTO_ICMPV6);
+        assertPass(program, packet.array());
+
+        // Verify empty ICMPv6 NA packet is passed
+        packet.put(ICMP6_TYPE_OFFSET, (byte)ICMP6_NEIGHBOR_ANNOUNCEMENT);
+        assertPass(program, packet.array());
+
+        // Verify ICMPv6 NA to ff02::1 is dropped
+        put(packet, IPV6_DEST_ADDR_OFFSET, IPV6_ALL_NODES_ADDRESS);
+        assertDrop(program, packet.array());
+
+        // Verify ICMPv6 NA to ff02::2 is dropped
+        put(packet, IPV6_DEST_ADDR_OFFSET, IPV6_ALL_ROUTERS_ADDRESS);
+        assertDrop(program, packet.array());
+
+        // Verify ICMPv6 NA to Solicited-Node Multicast is passed
+        put(packet, IPV6_DEST_ADDR_OFFSET, IPV6_SOLICITED_NODE_MULTICAST_ADDRESS);
+        assertPass(program, packet.array());
+
+        // Verify ICMPv6 RS to any is dropped
+        packet.put(ICMP6_TYPE_OFFSET, (byte)ICMP6_ROUTER_SOLICITATION);
+        assertDrop(program, packet.array());
+        put(packet, IPV6_DEST_ADDR_OFFSET, IPV6_ALL_ROUTERS_ADDRESS);
+        assertDrop(program, packet.array());
+    }
+
+    @Test
+    public void testApfFilterMulticast() throws Exception {
+        final byte[] unicastIpv4Addr   = {(byte)192,0,2,63};
+        final byte[] broadcastIpv4Addr = {(byte)192,0,2,(byte)255};
+        final byte[] multicastIpv4Addr = {(byte)224,0,0,1};
+        final byte[] multicastIpv6Addr = {(byte)0xff,2,0,0,0,0,0,0,0,0,0,0,0,0,0,(byte)0xfb};
+
+        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        LinkAddress link = new LinkAddress(InetAddress.getByAddress(unicastIpv4Addr), 24);
+        LinkProperties lp = new LinkProperties();
+        lp.addLinkAddress(link);
+
+        ApfConfiguration config = getDefaultConfig();
+        config.ieee802_3Filter = DROP_802_3_FRAMES;
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics, mDependencies);
+        apfFilter.setLinkProperties(lp);
+
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // Construct IPv4 and IPv6 multicast packets.
+        ByteBuffer mcastv4packet = makeIpv4Packet(IPPROTO_UDP);
+        put(mcastv4packet, IPV4_DEST_ADDR_OFFSET, multicastIpv4Addr);
+
+        ByteBuffer mcastv6packet = makeIpv6Packet(IPPROTO_UDP);
+        put(mcastv6packet, IPV6_DEST_ADDR_OFFSET, multicastIpv6Addr);
+
+        // Construct IPv4 broadcast packet.
+        ByteBuffer bcastv4packet1 = makeIpv4Packet(IPPROTO_UDP);
+        bcastv4packet1.put(ETH_BROADCAST_MAC_ADDRESS);
+        bcastv4packet1.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP);
+        put(bcastv4packet1, IPV4_DEST_ADDR_OFFSET, multicastIpv4Addr);
+
+        ByteBuffer bcastv4packet2 = makeIpv4Packet(IPPROTO_UDP);
+        bcastv4packet2.put(ETH_BROADCAST_MAC_ADDRESS);
+        bcastv4packet2.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP);
+        put(bcastv4packet2, IPV4_DEST_ADDR_OFFSET, IPV4_BROADCAST_ADDRESS);
+
+        // Construct IPv4 broadcast with L2 unicast address packet (b/30231088).
+        ByteBuffer bcastv4unicastl2packet = makeIpv4Packet(IPPROTO_UDP);
+        bcastv4unicastl2packet.put(TestApfFilter.MOCK_MAC_ADDR);
+        bcastv4unicastl2packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP);
+        put(bcastv4unicastl2packet, IPV4_DEST_ADDR_OFFSET, broadcastIpv4Addr);
+
+        // Verify initially disabled multicast filter is off
+        assertPass(program, mcastv4packet.array());
+        assertPass(program, mcastv6packet.array());
+        assertPass(program, bcastv4packet1.array());
+        assertPass(program, bcastv4packet2.array());
+        assertPass(program, bcastv4unicastl2packet.array());
+
+        // Turn on multicast filter and verify it works
+        ipClientCallback.resetApfProgramWait();
+        apfFilter.setMulticastFilter(true);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        assertDrop(program, mcastv4packet.array());
+        assertDrop(program, mcastv6packet.array());
+        assertDrop(program, bcastv4packet1.array());
+        assertDrop(program, bcastv4packet2.array());
+        assertDrop(program, bcastv4unicastl2packet.array());
+
+        // Turn off multicast filter and verify it's off
+        ipClientCallback.resetApfProgramWait();
+        apfFilter.setMulticastFilter(false);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        assertPass(program, mcastv4packet.array());
+        assertPass(program, mcastv6packet.array());
+        assertPass(program, bcastv4packet1.array());
+        assertPass(program, bcastv4packet2.array());
+        assertPass(program, bcastv4unicastl2packet.array());
+
+        // Verify it can be initialized to on
+        ipClientCallback.resetApfProgramWait();
+        config.multicastFilter = DROP_MULTICAST;
+        config.ieee802_3Filter = DROP_802_3_FRAMES;
+        apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mNetworkQuirkMetrics,
+                mDependencies);
+        apfFilter.setLinkProperties(lp);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        assertDrop(program, mcastv4packet.array());
+        assertDrop(program, mcastv6packet.array());
+        assertDrop(program, bcastv4packet1.array());
+        assertDrop(program, bcastv4unicastl2packet.array());
+
+        // Verify that ICMPv6 multicast is not dropped.
+        mcastv6packet.put(IPV6_NEXT_HEADER_OFFSET, (byte)IPPROTO_ICMPV6);
+        assertPass(program, mcastv6packet.array());
+    }
+
+    @Test
+    public void testApfFilterMulticastPingWhileDozing() throws Exception {
+        doTestApfFilterMulticastPingWhileDozing(false /* isLightDozing */);
+    }
+
+    @Test
+    @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testApfFilterMulticastPingWhileLightDozing() throws Exception {
+        doTestApfFilterMulticastPingWhileDozing(true /* isLightDozing */);
+    }
+
+    @Test
+    @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+    public void testShouldHandleLightDozeKillSwitch() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        final ApfConfiguration configuration = getDefaultConfig();
+        configuration.shouldHandleLightDoze = false;
+        final ApfFilter apfFilter = TestApfFilter.createTestApfFilter(mContext, ipClientCallback,
+                configuration, mNetworkQuirkMetrics, mDependencies);
+        final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
+                ArgumentCaptor.forClass(BroadcastReceiver.class);
+        verify(mDependencies).addDeviceIdleReceiver(receiverCaptor.capture(), anyBoolean());
+        final BroadcastReceiver receiver = receiverCaptor.getValue();
+        doReturn(true).when(mPowerManager).isDeviceLightIdleMode();
+        receiver.onReceive(mContext, new Intent(ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED));
+        assertFalse(apfFilter.isInDozeMode());
+    }
+
+    private void doTestApfFilterMulticastPingWhileDozing(boolean isLightDozing) throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        final ApfConfiguration configuration = getDefaultConfig();
+        configuration.shouldHandleLightDoze = true;
+        final ApfFilter apfFilter = TestApfFilter.createTestApfFilter(mContext, ipClientCallback,
+                configuration, mNetworkQuirkMetrics, mDependencies);
+        final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
+                ArgumentCaptor.forClass(BroadcastReceiver.class);
+        verify(mDependencies).addDeviceIdleReceiver(receiverCaptor.capture(), anyBoolean());
+        final BroadcastReceiver receiver = receiverCaptor.getValue();
+
+        // Construct a multicast ICMPv6 ECHO request.
+        final byte[] multicastIpv6Addr = {(byte)0xff,2,0,0,0,0,0,0,0,0,0,0,0,0,0,(byte)0xfb};
+        final ByteBuffer packet = makeIpv6Packet(IPPROTO_ICMPV6);
+        packet.put(ICMP6_TYPE_OFFSET, (byte)ICMPV6_ECHO_REQUEST_TYPE);
+        put(packet, IPV6_DEST_ADDR_OFFSET, multicastIpv6Addr);
+
+        // Normally, we let multicast pings alone...
+        assertPass(ipClientCallback.assertProgramUpdateAndGet(), packet.array());
+
+        if (isLightDozing) {
+            doReturn(true).when(mPowerManager).isDeviceLightIdleMode();
+            receiver.onReceive(mContext, new Intent(ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED));
+        } else {
+            doReturn(true).when(mPowerManager).isDeviceIdleMode();
+            receiver.onReceive(mContext, new Intent(ACTION_DEVICE_IDLE_MODE_CHANGED));
+        }
+        // ...and even while dozing...
+        assertPass(ipClientCallback.assertProgramUpdateAndGet(), packet.array());
+
+        // ...but when the multicast filter is also enabled, drop the multicast pings to save power.
+        apfFilter.setMulticastFilter(true);
+        assertDrop(ipClientCallback.assertProgramUpdateAndGet(), packet.array());
+
+        // However, we should still let through all other ICMPv6 types.
+        ByteBuffer raPacket = ByteBuffer.wrap(packet.array().clone());
+        setIpv6VersionFields(packet);
+        packet.put(IPV6_NEXT_HEADER_OFFSET, (byte) IPPROTO_ICMPV6);
+        raPacket.put(ICMP6_TYPE_OFFSET, (byte) NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT);
+        assertPass(ipClientCallback.assertProgramUpdateAndGet(), raPacket.array());
+
+        // Now wake up from doze mode to ensure that we no longer drop the packets.
+        // (The multicast filter is still enabled at this point).
+        if (isLightDozing) {
+            doReturn(false).when(mPowerManager).isDeviceLightIdleMode();
+            receiver.onReceive(mContext, new Intent(ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED));
+        } else {
+            doReturn(false).when(mPowerManager).isDeviceIdleMode();
+            receiver.onReceive(mContext, new Intent(ACTION_DEVICE_IDLE_MODE_CHANGED));
+        }
+        assertPass(ipClientCallback.assertProgramUpdateAndGet(), packet.array());
+    }
+
+    @Test
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void testApfFilter802_3() throws Exception {
+        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        ApfConfiguration config = getDefaultConfig();
+        ApfFilter apfFilter = TestApfFilter.createTestApfFilter(mContext, ipClientCallback, config,
+                mNetworkQuirkMetrics, mDependencies);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // Verify empty packet of 100 zero bytes is passed
+        // Note that eth-type = 0 makes it an IEEE802.3 frame
+        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
+        assertPass(program, packet.array());
+
+        // Verify empty packet with IPv4 is passed
+        setIpv4VersionFields(packet);
+        assertPass(program, packet.array());
+
+        // Verify empty IPv6 packet is passed
+        setIpv6VersionFields(packet);
+        assertPass(program, packet.array());
+
+        // Now turn on the filter
+        ipClientCallback.resetApfProgramWait();
+        config.ieee802_3Filter = DROP_802_3_FRAMES;
+        apfFilter = TestApfFilter.createTestApfFilter(mContext, ipClientCallback, config,
+                mNetworkQuirkMetrics, mDependencies);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // Verify that IEEE802.3 frame is dropped
+        // In this case ethtype is used for payload length
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)(100 - 14));
+        assertDrop(program, packet.array());
+
+        // Verify that IPv4 (as example of Ethernet II) frame will pass
+        setIpv4VersionFields(packet);
+        assertPass(program, packet.array());
+
+        // Verify that IPv6 (as example of Ethernet II) frame will pass
+        setIpv6VersionFields(packet);
+        assertPass(program, packet.array());
+    }
+
+    @Test
+    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void testApfFilterEthTypeBL() throws Exception {
+        final int[] emptyBlackList = {};
+        final int[] ipv4BlackList = {ETH_P_IP};
+        final int[] ipv4Ipv6BlackList = {ETH_P_IP, ETH_P_IPV6};
+
+        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        ApfConfiguration config = getDefaultConfig();
+        ApfFilter apfFilter = TestApfFilter.createTestApfFilter(mContext, ipClientCallback, config,
+                mNetworkQuirkMetrics, mDependencies);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // Verify empty packet of 100 zero bytes is passed
+        // Note that eth-type = 0 makes it an IEEE802.3 frame
+        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
+        assertPass(program, packet.array());
+
+        // Verify empty packet with IPv4 is passed
+        setIpv4VersionFields(packet);
+        assertPass(program, packet.array());
+
+        // Verify empty IPv6 packet is passed
+        setIpv6VersionFields(packet);
+        assertPass(program, packet.array());
+
+        // Now add IPv4 to the black list
+        ipClientCallback.resetApfProgramWait();
+        config.ethTypeBlackList = ipv4BlackList;
+        apfFilter = TestApfFilter.createTestApfFilter(mContext, ipClientCallback, config,
+                mNetworkQuirkMetrics, mDependencies);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // Verify that IPv4 frame will be dropped
+        setIpv4VersionFields(packet);
+        assertDrop(program, packet.array());
+
+        // Verify that IPv6 frame will pass
+        setIpv6VersionFields(packet);
+        assertPass(program, packet.array());
+
+        // Now let us have both IPv4 and IPv6 in the black list
+        ipClientCallback.resetApfProgramWait();
+        config.ethTypeBlackList = ipv4Ipv6BlackList;
+        apfFilter = TestApfFilter.createTestApfFilter(mContext, ipClientCallback, config,
+                mNetworkQuirkMetrics, mDependencies);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // Verify that IPv4 frame will be dropped
+        setIpv4VersionFields(packet);
+        assertDrop(program, packet.array());
+
+        // Verify that IPv6 frame will be dropped
+        setIpv6VersionFields(packet);
+        assertDrop(program, packet.array());
+    }
+
+    private byte[] getProgram(MockIpClientCallback cb, ApfFilter filter, LinkProperties lp) {
+        cb.resetApfProgramWait();
+        filter.setLinkProperties(lp);
+        return cb.assertProgramUpdateAndGet();
+    }
+
+    private void verifyArpFilter(byte[] program, int filterResult) {
+        // Verify ARP request packet
+        assertPass(program, arpRequestBroadcast(MOCK_IPV4_ADDR));
+        assertVerdict(filterResult, program, arpRequestBroadcast(ANOTHER_IPV4_ADDR));
+        assertVerdict(filterResult, program, arpRequestBroadcast(IPV4_ANY_HOST_ADDR));
+
+        // Verify ARP reply packets from different source ip
+        assertDrop(program, arpReply(IPV4_ANY_HOST_ADDR, IPV4_ANY_HOST_ADDR));
+        assertPass(program, arpReply(ANOTHER_IPV4_SOURCE_ADDR, IPV4_ANY_HOST_ADDR));
+        assertPass(program, arpReply(BUG_PROBE_SOURCE_ADDR1, IPV4_ANY_HOST_ADDR));
+        assertPass(program, arpReply(BUG_PROBE_SOURCE_ADDR2, IPV4_ANY_HOST_ADDR));
+
+        // Verify unicast ARP reply packet is always accepted.
+        assertPass(program, arpReply(IPV4_SOURCE_ADDR, MOCK_IPV4_ADDR));
+        assertPass(program, arpReply(IPV4_SOURCE_ADDR, ANOTHER_IPV4_ADDR));
+        assertPass(program, arpReply(IPV4_SOURCE_ADDR, IPV4_ANY_HOST_ADDR));
+
+        // Verify GARP reply packets are always filtered
+        assertDrop(program, garpReply());
+    }
+
+    @Test
+    public void testApfFilterArp() throws Exception {
+        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        ApfConfiguration config = getDefaultConfig();
+        config.multicastFilter = DROP_MULTICAST;
+        config.ieee802_3Filter = DROP_802_3_FRAMES;
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics, mDependencies);
+
+        // Verify initially ARP request filter is off, and GARP filter is on.
+        verifyArpFilter(ipClientCallback.assertProgramUpdateAndGet(), PASS);
+
+        // Inform ApfFilter of our address and verify ARP filtering is on
+        LinkAddress linkAddress = new LinkAddress(InetAddress.getByAddress(MOCK_IPV4_ADDR), 24);
+        LinkProperties lp = new LinkProperties();
+        assertTrue(lp.addLinkAddress(linkAddress));
+        verifyArpFilter(getProgram(ipClientCallback, apfFilter, lp), DROP);
+
+        // Inform ApfFilter of loss of IP and verify ARP filtering is off
+        verifyArpFilter(getProgram(ipClientCallback, apfFilter, new LinkProperties()), PASS);
+    }
+
+    private static byte[] arpReply(byte[] sip, byte[] tip) {
+        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_ARP);
+        put(packet, ARP_HEADER_OFFSET, ARP_IPV4_REPLY_HEADER);
+        put(packet, ARP_SOURCE_IP_ADDRESS_OFFSET, sip);
+        put(packet, ARP_TARGET_IP_ADDRESS_OFFSET, tip);
+        return packet.array();
+    }
+
+    private static byte[] arpRequestBroadcast(byte[] tip) {
+        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_ARP);
+        put(packet, ETH_DEST_ADDR_OFFSET, ETH_BROADCAST_MAC_ADDRESS);
+        put(packet, ARP_HEADER_OFFSET, ARP_IPV4_REQUEST_HEADER);
+        put(packet, ARP_TARGET_IP_ADDRESS_OFFSET, tip);
+        return packet.array();
+    }
+
+    private static byte[] garpReply() {
+        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
+        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_ARP);
+        put(packet, ETH_DEST_ADDR_OFFSET, ETH_BROADCAST_MAC_ADDRESS);
+        put(packet, ARP_HEADER_OFFSET, ARP_IPV4_REPLY_HEADER);
+        put(packet, ARP_TARGET_IP_ADDRESS_OFFSET, IPV4_ANY_HOST_ADDR);
+        return packet.array();
+    }
+
+    private static final byte[] IPV4_KEEPALIVE_SRC_ADDR = {10, 0, 0, 5};
+    private static final byte[] IPV4_KEEPALIVE_DST_ADDR = {10, 0, 0, 6};
+    private static final byte[] IPV4_ANOTHER_ADDR = {10, 0 , 0, 7};
+    private static final byte[] IPV6_KEEPALIVE_SRC_ADDR =
+            {(byte) 0x24, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (byte) 0xfa, (byte) 0xf1};
+    private static final byte[] IPV6_KEEPALIVE_DST_ADDR =
+            {(byte) 0x24, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (byte) 0xfa, (byte) 0xf2};
+    private static final byte[] IPV6_ANOTHER_ADDR =
+            {(byte) 0x24, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (byte) 0xfa, (byte) 0xf5};
+
+    @Test
+    public void testApfFilterKeepaliveAck() throws Exception {
+        final MockIpClientCallback cb = new MockIpClientCallback();
+        final ApfConfiguration config = getDefaultConfig();
+        config.multicastFilter = DROP_MULTICAST;
+        config.ieee802_3Filter = DROP_802_3_FRAMES;
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb,
+                mNetworkQuirkMetrics, mDependencies);
+        byte[] program;
+        final int srcPort = 12345;
+        final int dstPort = 54321;
+        final int seqNum = 2123456789;
+        final int ackNum = 1234567890;
+        final int anotherSrcPort = 23456;
+        final int anotherDstPort = 65432;
+        final int anotherSeqNum = 2123456780;
+        final int anotherAckNum = 1123456789;
+        final int slot1 = 1;
+        final int slot2 = 2;
+        final int window = 14480;
+        final int windowScale = 4;
+
+        // src: 10.0.0.5, port: 12345
+        // dst: 10.0.0.6, port: 54321
+        InetAddress srcAddr = InetAddress.getByAddress(IPV4_KEEPALIVE_SRC_ADDR);
+        InetAddress dstAddr = InetAddress.getByAddress(IPV4_KEEPALIVE_DST_ADDR);
+
+        final TcpKeepalivePacketDataParcelable parcel = new TcpKeepalivePacketDataParcelable();
+        parcel.srcAddress = srcAddr.getAddress();
+        parcel.srcPort = srcPort;
+        parcel.dstAddress = dstAddr.getAddress();
+        parcel.dstPort = dstPort;
+        parcel.seq = seqNum;
+        parcel.ack = ackNum;
+
+        apfFilter.addTcpKeepalivePacketFilter(slot1, parcel);
+        program = cb.assertProgramUpdateAndGet();
+
+        // Verify IPv4 keepalive ack packet is dropped
+        // src: 10.0.0.6, port: 54321
+        // dst: 10.0.0.5, port: 12345
+        assertDrop(program,
+                ipv4TcpPacket(IPV4_KEEPALIVE_DST_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
+                        dstPort, srcPort, ackNum, seqNum + 1, 0 /* dataLength */));
+        // Verify IPv4 non-keepalive ack packet from the same source address is passed
+        assertPass(program,
+                ipv4TcpPacket(IPV4_KEEPALIVE_DST_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
+                        dstPort, srcPort, ackNum + 100, seqNum, 0 /* dataLength */));
+        assertPass(program,
+                ipv4TcpPacket(IPV4_KEEPALIVE_DST_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
+                        dstPort, srcPort, ackNum, seqNum + 1, 10 /* dataLength */));
+        // Verify IPv4 packet from another address is passed
+        assertPass(program,
+                ipv4TcpPacket(IPV4_ANOTHER_ADDR, IPV4_KEEPALIVE_SRC_ADDR, anotherSrcPort,
+                        anotherDstPort, anotherSeqNum, anotherAckNum, 0 /* dataLength */));
+
+        // Remove IPv4 keepalive filter
+        apfFilter.removeKeepalivePacketFilter(slot1);
+
+        try {
+            // src: 2404:0:0:0:0:0:faf1, port: 12345
+            // dst: 2404:0:0:0:0:0:faf2, port: 54321
+            srcAddr = InetAddress.getByAddress(IPV6_KEEPALIVE_SRC_ADDR);
+            dstAddr = InetAddress.getByAddress(IPV6_KEEPALIVE_DST_ADDR);
+
+            final TcpKeepalivePacketDataParcelable ipv6Parcel =
+                    new TcpKeepalivePacketDataParcelable();
+            ipv6Parcel.srcAddress = srcAddr.getAddress();
+            ipv6Parcel.srcPort = srcPort;
+            ipv6Parcel.dstAddress = dstAddr.getAddress();
+            ipv6Parcel.dstPort = dstPort;
+            ipv6Parcel.seq = seqNum;
+            ipv6Parcel.ack = ackNum;
+
+            apfFilter.addTcpKeepalivePacketFilter(slot1, ipv6Parcel);
+            program = cb.assertProgramUpdateAndGet();
+
+            // Verify IPv6 keepalive ack packet is dropped
+            // src: 2404:0:0:0:0:0:faf2, port: 54321
+            // dst: 2404:0:0:0:0:0:faf1, port: 12345
+            assertDrop(program,
+                    ipv6TcpPacket(IPV6_KEEPALIVE_DST_ADDR, IPV6_KEEPALIVE_SRC_ADDR,
+                            dstPort, srcPort, ackNum, seqNum + 1));
+            // Verify IPv6 non-keepalive ack packet from the same source address is passed
+            assertPass(program,
+                    ipv6TcpPacket(IPV6_KEEPALIVE_DST_ADDR, IPV6_KEEPALIVE_SRC_ADDR,
+                            dstPort, srcPort, ackNum + 100, seqNum));
+            // Verify IPv6 packet from another address is passed
+            assertPass(program,
+                    ipv6TcpPacket(IPV6_ANOTHER_ADDR, IPV6_KEEPALIVE_SRC_ADDR, anotherSrcPort,
+                            anotherDstPort, anotherSeqNum, anotherAckNum));
+
+            // Remove IPv6 keepalive filter
+            apfFilter.removeKeepalivePacketFilter(slot1);
+
+            // Verify multiple filters
+            apfFilter.addTcpKeepalivePacketFilter(slot1, parcel);
+            apfFilter.addTcpKeepalivePacketFilter(slot2, ipv6Parcel);
+            program = cb.assertProgramUpdateAndGet();
+
+            // Verify IPv4 keepalive ack packet is dropped
+            // src: 10.0.0.6, port: 54321
+            // dst: 10.0.0.5, port: 12345
+            assertDrop(program,
+                    ipv4TcpPacket(IPV4_KEEPALIVE_DST_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
+                            dstPort, srcPort, ackNum, seqNum + 1, 0 /* dataLength */));
+            // Verify IPv4 non-keepalive ack packet from the same source address is passed
+            assertPass(program,
+                    ipv4TcpPacket(IPV4_KEEPALIVE_DST_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
+                            dstPort, srcPort, ackNum + 100, seqNum, 0 /* dataLength */));
+            // Verify IPv4 packet from another address is passed
+            assertPass(program,
+                    ipv4TcpPacket(IPV4_ANOTHER_ADDR, IPV4_KEEPALIVE_SRC_ADDR, anotherSrcPort,
+                            anotherDstPort, anotherSeqNum, anotherAckNum, 0 /* dataLength */));
+
+            // Verify IPv6 keepalive ack packet is dropped
+            // src: 2404:0:0:0:0:0:faf2, port: 54321
+            // dst: 2404:0:0:0:0:0:faf1, port: 12345
+            assertDrop(program,
+                    ipv6TcpPacket(IPV6_KEEPALIVE_DST_ADDR, IPV6_KEEPALIVE_SRC_ADDR,
+                            dstPort, srcPort, ackNum, seqNum + 1));
+            // Verify IPv6 non-keepalive ack packet from the same source address is passed
+            assertPass(program,
+                    ipv6TcpPacket(IPV6_KEEPALIVE_DST_ADDR, IPV6_KEEPALIVE_SRC_ADDR,
+                            dstPort, srcPort, ackNum + 100, seqNum));
+            // Verify IPv6 packet from another address is passed
+            assertPass(program,
+                    ipv6TcpPacket(IPV6_ANOTHER_ADDR, IPV6_KEEPALIVE_SRC_ADDR, anotherSrcPort,
+                            anotherDstPort, anotherSeqNum, anotherAckNum));
+
+            // Remove keepalive filters
+            apfFilter.removeKeepalivePacketFilter(slot1);
+            apfFilter.removeKeepalivePacketFilter(slot2);
+        } catch (UnsupportedOperationException e) {
+            // TODO: support V6 packets
+        }
+
+        program = cb.assertProgramUpdateAndGet();
+
+        // Verify IPv4, IPv6 packets are passed
+        assertPass(program,
+                ipv4TcpPacket(IPV4_KEEPALIVE_DST_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
+                        dstPort, srcPort, ackNum, seqNum + 1, 0 /* dataLength */));
+        assertPass(program,
+                ipv6TcpPacket(IPV6_KEEPALIVE_DST_ADDR, IPV6_KEEPALIVE_SRC_ADDR,
+                        dstPort, srcPort, ackNum, seqNum + 1));
+        assertPass(program,
+                ipv4TcpPacket(IPV4_ANOTHER_ADDR, IPV4_KEEPALIVE_SRC_ADDR, srcPort,
+                        dstPort, anotherSeqNum, anotherAckNum, 0 /* dataLength */));
+        assertPass(program,
+                ipv6TcpPacket(IPV6_ANOTHER_ADDR, IPV6_KEEPALIVE_SRC_ADDR, srcPort,
+                        dstPort, anotherSeqNum, anotherAckNum));
+    }
+
+    private static byte[] ipv4TcpPacket(byte[] sip, byte[] dip, int sport,
+            int dport, int seq, int ack, int dataLength) {
+        final int totalLength = dataLength + IPV4_HEADER_LEN + IPV4_TCP_HEADER_LEN;
+
+        ByteBuffer packet = ByteBuffer.wrap(new byte[totalLength + ETH_HEADER_LEN]);
+
+        // Ethertype and IPv4 header
+        setIpv4VersionFields(packet);
+        packet.putShort(IPV4_TOTAL_LENGTH_OFFSET, (short) totalLength);
+        packet.put(IPV4_PROTOCOL_OFFSET, (byte) IPPROTO_TCP);
+        put(packet, IPV4_SRC_ADDR_OFFSET, sip);
+        put(packet, IPV4_DEST_ADDR_OFFSET, dip);
+        packet.putShort(IPV4_TCP_SRC_PORT_OFFSET, (short) sport);
+        packet.putShort(IPV4_TCP_DEST_PORT_OFFSET, (short) dport);
+        packet.putInt(IPV4_TCP_SEQ_NUM_OFFSET, seq);
+        packet.putInt(IPV4_TCP_ACK_NUM_OFFSET, ack);
+
+        // TCP header length 5(20 bytes), reserved 3 bits, NS=0
+        packet.put(IPV4_TCP_HEADER_LENGTH_OFFSET, (byte) 0x50);
+        // TCP flags: ACK set
+        packet.put(IPV4_TCP_HEADER_FLAG_OFFSET, (byte) 0x10);
+        return packet.array();
+    }
+
+    private static byte[] ipv6TcpPacket(byte[] sip, byte[] tip, int sport,
+            int dport, int seq, int ack) {
+        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
+        setIpv6VersionFields(packet);
+        packet.put(IPV6_NEXT_HEADER_OFFSET, (byte) IPPROTO_TCP);
+        put(packet, IPV6_SRC_ADDR_OFFSET, sip);
+        put(packet, IPV6_DEST_ADDR_OFFSET, tip);
+        packet.putShort(IPV6_TCP_SRC_PORT_OFFSET, (short) sport);
+        packet.putShort(IPV6_TCP_DEST_PORT_OFFSET, (short) dport);
+        packet.putInt(IPV6_TCP_SEQ_NUM_OFFSET, seq);
+        packet.putInt(IPV6_TCP_ACK_NUM_OFFSET, ack);
+        return packet.array();
+    }
+
+    @Test
+    public void testApfFilterNattKeepalivePacket() throws Exception {
+        final MockIpClientCallback cb = new MockIpClientCallback();
+        final ApfConfiguration config = getDefaultConfig();
+        config.multicastFilter = DROP_MULTICAST;
+        config.ieee802_3Filter = DROP_802_3_FRAMES;
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb,
+                mNetworkQuirkMetrics, mDependencies);
+        byte[] program;
+        final int srcPort = 1024;
+        final int dstPort = 4500;
+        final int slot1 = 1;
+        // NAT-T keepalive
+        final byte[] kaPayload = {(byte) 0xff};
+        final byte[] nonKaPayload = {(byte) 0xfe};
+
+        // src: 10.0.0.5, port: 1024
+        // dst: 10.0.0.6, port: 4500
+        InetAddress srcAddr = InetAddress.getByAddress(IPV4_KEEPALIVE_SRC_ADDR);
+        InetAddress dstAddr = InetAddress.getByAddress(IPV4_KEEPALIVE_DST_ADDR);
+
+        final NattKeepalivePacketDataParcelable parcel = new NattKeepalivePacketDataParcelable();
+        parcel.srcAddress = srcAddr.getAddress();
+        parcel.srcPort = srcPort;
+        parcel.dstAddress = dstAddr.getAddress();
+        parcel.dstPort = dstPort;
+
+        apfFilter.addNattKeepalivePacketFilter(slot1, parcel);
+        program = cb.assertProgramUpdateAndGet();
+
+        // Verify IPv4 keepalive packet is dropped
+        // src: 10.0.0.6, port: 4500
+        // dst: 10.0.0.5, port: 1024
+        byte[] pkt = ipv4UdpPacket(IPV4_KEEPALIVE_DST_ADDR,
+                    IPV4_KEEPALIVE_SRC_ADDR, dstPort, srcPort, 1 /* dataLength */);
+        System.arraycopy(kaPayload, 0, pkt, IPV4_UDP_PAYLOAD_OFFSET, kaPayload.length);
+        assertDrop(program, pkt);
+
+        // Verify a packet with payload length 1 byte but it is not 0xff will pass the filter.
+        System.arraycopy(nonKaPayload, 0, pkt, IPV4_UDP_PAYLOAD_OFFSET, nonKaPayload.length);
+        assertPass(program, pkt);
+
+        // Verify IPv4 non-keepalive response packet from the same source address is passed
+        assertPass(program,
+                ipv4UdpPacket(IPV4_KEEPALIVE_DST_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
+                        dstPort, srcPort, 10 /* dataLength */));
+
+        // Verify IPv4 non-keepalive response packet from other source address is passed
+        assertPass(program,
+                ipv4UdpPacket(IPV4_ANOTHER_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
+                        dstPort, srcPort, 10 /* dataLength */));
+
+        apfFilter.removeKeepalivePacketFilter(slot1);
+    }
+
+    private static byte[] ipv4UdpPacket(byte[] sip, byte[] dip, int sport,
+            int dport, int dataLength) {
+        final int totalLength = dataLength + IPV4_HEADER_LEN + UDP_HEADER_LEN;
+        final int udpLength = UDP_HEADER_LEN + dataLength;
+        ByteBuffer packet = ByteBuffer.wrap(new byte[totalLength + ETH_HEADER_LEN]);
+
+        // Ethertype and IPv4 header
+        setIpv4VersionFields(packet);
+        packet.putShort(IPV4_TOTAL_LENGTH_OFFSET, (short) totalLength);
+        packet.put(IPV4_PROTOCOL_OFFSET, (byte) IPPROTO_UDP);
+        put(packet, IPV4_SRC_ADDR_OFFSET, sip);
+        put(packet, IPV4_DEST_ADDR_OFFSET, dip);
+        packet.putShort(IPV4_UDP_SRC_PORT_OFFSET, (short) sport);
+        packet.putShort(IPV4_UDP_DEST_PORT_OFFSET, (short) dport);
+        packet.putShort(IPV4_UDP_LENGTH_OFFSET, (short) udpLength);
+
+        return packet.array();
+    }
+
+    private static class RaPacketBuilder {
+        final ByteArrayOutputStream mPacket = new ByteArrayOutputStream();
+        int mFlowLabel = 0x12345;
+        int mReachableTime = 30_000;
+        int mRetransmissionTimer = 1000;
+
+        public RaPacketBuilder(int routerLft) throws Exception {
+            InetAddress src = InetAddress.getByName("fe80::1234:abcd");
+            ByteBuffer buffer = ByteBuffer.allocate(ICMP6_RA_OPTION_OFFSET);
+
+            buffer.putShort(ETH_ETHERTYPE_OFFSET, (short) ETH_P_IPV6);
+            buffer.position(ETH_HEADER_LEN);
+
+            // skip version, tclass, flowlabel; set in build()
+            buffer.position(buffer.position() + 4);
+
+            buffer.putShort((short) 0);                     // Payload length; updated later
+            buffer.put((byte) IPPROTO_ICMPV6);              // Next header
+            buffer.put((byte) 0xff);                        // Hop limit
+            buffer.put(src.getAddress());                   // Source address
+            buffer.put(IPV6_ALL_NODES_ADDRESS);             // Destination address
+
+            buffer.put((byte) ICMP6_ROUTER_ADVERTISEMENT);  // Type
+            buffer.put((byte) 0);                           // Code (0)
+            buffer.putShort((short) 0);                     // Checksum (ignored)
+            buffer.put((byte) 64);                          // Hop limit
+            buffer.put((byte) 0);                           // M/O, reserved
+            buffer.putShort((short) routerLft);             // Router lifetime
+            // skip reachable time; set in build()
+            // skip retransmission timer; set in build();
+
+            mPacket.write(buffer.array(), 0, buffer.capacity());
+        }
+
+        public RaPacketBuilder setFlowLabel(int flowLabel) {
+            mFlowLabel = flowLabel;
+            return this;
+        }
+
+        public RaPacketBuilder setReachableTime(int reachable) {
+            mReachableTime = reachable;
+            return this;
+        }
+
+        public RaPacketBuilder setRetransmissionTimer(int retrans) {
+            mRetransmissionTimer = retrans;
+            return this;
+        }
+
+        public RaPacketBuilder addPioOption(int valid, int preferred, String prefixString)
+                throws Exception {
+            ByteBuffer buffer = ByteBuffer.allocate(ICMP6_PREFIX_OPTION_LEN);
+
+            IpPrefix prefix = new IpPrefix(prefixString);
+            buffer.put((byte) ICMP6_PREFIX_OPTION_TYPE);  // Type
+            buffer.put((byte) 4);                         // Length in 8-byte units
+            buffer.put((byte) prefix.getPrefixLength());  // Prefix length
+            buffer.put((byte) 0b11000000);                // L = 1, A = 1
+            buffer.putInt(valid);
+            buffer.putInt(preferred);
+            buffer.putInt(0);                             // Reserved
+            buffer.put(prefix.getRawAddress());
+
+            mPacket.write(buffer.array(), 0, buffer.capacity());
+            return this;
+        }
+
+        public RaPacketBuilder addRioOption(int lifetime, String prefixString) throws Exception {
+            IpPrefix prefix = new IpPrefix(prefixString);
+
+            int optionLength;
+            if (prefix.getPrefixLength() == 0) {
+                optionLength = 1;
+            } else if (prefix.getPrefixLength() <= 64) {
+                optionLength = 2;
+            } else {
+                optionLength = 3;
+            }
+
+            ByteBuffer buffer = ByteBuffer.allocate(optionLength * 8);
+
+            buffer.put((byte) ICMP6_ROUTE_INFO_OPTION_TYPE);  // Type
+            buffer.put((byte) optionLength);                  // Length in 8-byte units
+            buffer.put((byte) prefix.getPrefixLength());      // Prefix length
+            buffer.put((byte) 0b00011000);                    // Pref = high
+            buffer.putInt(lifetime);                          // Lifetime
+
+            byte[] prefixBytes = prefix.getRawAddress();
+            buffer.put(prefixBytes, 0, (optionLength - 1) * 8);
+
+            mPacket.write(buffer.array(), 0, buffer.capacity());
+            return this;
+        }
+
+        public RaPacketBuilder addDnsslOption(int lifetime, String... domains) {
+            ByteArrayOutputStream dnssl = new ByteArrayOutputStream();
+            for (String domain : domains) {
+                for (String label : domain.split("\\.")) {
+                    final byte[] bytes = label.getBytes(StandardCharsets.UTF_8);
+                    dnssl.write((byte) bytes.length);
+                    dnssl.write(bytes, 0, bytes.length);
+                }
+                dnssl.write((byte) 0);
+            }
+
+            // Extend with 0s to make it 8-byte aligned.
+            while (dnssl.size() % 8 != 0) {
+                dnssl.write((byte) 0);
+            }
+
+            final int length = ICMP6_4_BYTE_OPTION_LEN + dnssl.size();
+            ByteBuffer buffer = ByteBuffer.allocate(length);
+
+            buffer.put((byte) ICMP6_DNSSL_OPTION_TYPE);  // Type
+            buffer.put((byte) (length / 8));             // Length
+            // skip past reserved bytes
+            buffer.position(buffer.position() + 2);
+            buffer.putInt(lifetime);                     // Lifetime
+            buffer.put(dnssl.toByteArray());             // Domain names
+
+            mPacket.write(buffer.array(), 0, buffer.capacity());
+            return this;
+        }
+
+        public RaPacketBuilder addRdnssOption(int lifetime, String... servers) throws Exception {
+            int optionLength = 1 + 2 * servers.length;   // In 8-byte units
+            ByteBuffer buffer = ByteBuffer.allocate(optionLength * 8);
+
+            buffer.put((byte) ICMP6_RDNSS_OPTION_TYPE);  // Type
+            buffer.put((byte) optionLength);             // Length
+            buffer.putShort((short) 0);                  // Reserved
+            buffer.putInt(lifetime);                     // Lifetime
+            for (String server : servers) {
+                buffer.put(InetAddress.getByName(server).getAddress());
+            }
+
+            mPacket.write(buffer.array(), 0, buffer.capacity());
+            return this;
+        }
+
+        public RaPacketBuilder addZeroLengthOption() throws Exception {
+            ByteBuffer buffer = ByteBuffer.allocate(ICMP6_4_BYTE_OPTION_LEN);
+            buffer.put((byte) ICMP6_PREFIX_OPTION_TYPE);
+            buffer.put((byte) 0);
+
+            mPacket.write(buffer.array(), 0, buffer.capacity());
+            return this;
+        }
+
+        public byte[] build() {
+            ByteBuffer buffer = ByteBuffer.wrap(mPacket.toByteArray());
+            // IPv6, traffic class = 0, flow label = mFlowLabel
+            buffer.putInt(IP_HEADER_OFFSET, 0x60000000 | (0xFFFFF & mFlowLabel));
+            buffer.putShort(IPV6_PAYLOAD_LENGTH_OFFSET, (short) buffer.capacity());
+
+            buffer.position(ICMP6_RA_REACHABLE_TIME_OFFSET);
+            buffer.putInt(mReachableTime);
+            buffer.putInt(mRetransmissionTimer);
+
+            return buffer.array();
+        }
+    }
+
+    private byte[] buildLargeRa() throws Exception {
+        RaPacketBuilder builder = new RaPacketBuilder(1800 /* router lft */);
+
+        builder.addRioOption(1200, "64:ff9b::/96");
+        builder.addRdnssOption(7200, "2001:db8:1::1", "2001:db8:1::2");
+        builder.addRioOption(2100, "2000::/3");
+        builder.addRioOption(2400, "::/0");
+        builder.addPioOption(600, 300, "2001:db8:a::/64");
+        builder.addRioOption(1500, "2001:db8:c:d::/64");
+        builder.addPioOption(86400, 43200, "fd95:d1e:12::/64");
+
+        return builder.build();
+    }
+
+    // Verify that the last program pushed to the IpClient.Callback properly filters the
+    // given packet for the given lifetime.
+    private void verifyRaLifetime(byte[] program, ByteBuffer packet, int lifetime) {
+        verifyRaLifetime(program, packet, lifetime, 0);
+    }
+
+    // Verify that the last program pushed to the IpClient.Callback properly filters the
+    // given packet for the given lifetime and programInstallTime. programInstallTime is
+    // the time difference between when RA is last seen and the program is installed.
+    private void verifyRaLifetime(byte[] program, ByteBuffer packet, int lifetime,
+            int programInstallTime) {
+        final int FRACTION_OF_LIFETIME = 6;
+        final int ageLimit = lifetime / FRACTION_OF_LIFETIME - programInstallTime;
+
+        // Verify new program should drop RA for 1/6th its lifetime and pass afterwards.
+        assertDrop(program, packet.array());
+        assertDrop(program, packet.array(), ageLimit);
+        assertPass(program, packet.array(), ageLimit + 1);
+        assertPass(program, packet.array(), lifetime);
+        // Verify RA checksum is ignored
+        final short originalChecksum = packet.getShort(ICMP6_RA_CHECKSUM_OFFSET);
+        packet.putShort(ICMP6_RA_CHECKSUM_OFFSET, (short)12345);
+        assertDrop(program, packet.array());
+        packet.putShort(ICMP6_RA_CHECKSUM_OFFSET, (short)-12345);
+        assertDrop(program, packet.array());
+        packet.putShort(ICMP6_RA_CHECKSUM_OFFSET, originalChecksum);
+
+        // Verify other changes to RA (e.g., a change in the source address) make it not match.
+        final int offset = IPV6_SRC_ADDR_OFFSET + 5;
+        final byte originalByte = packet.get(offset);
+        packet.put(offset, (byte) (~originalByte));
+        assertPass(program, packet.array());
+        packet.put(offset, originalByte);
+        assertDrop(program, packet.array());
+    }
+
+    // Test that when ApfFilter is shown the given packet, it generates a program to filter it
+    // for the given lifetime.
+    private void verifyRaLifetime(TestApfFilter apfFilter, MockIpClientCallback ipClientCallback,
+            ByteBuffer packet, int lifetime) throws IOException, ErrnoException {
+        // Verify new program generated if ApfFilter witnesses RA
+        apfFilter.pretendPacketReceived(packet.array());
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        verifyRaLifetime(program, packet, lifetime);
+    }
+
+    private void assertInvalidRa(TestApfFilter apfFilter, MockIpClientCallback ipClientCallback,
+            ByteBuffer packet) throws IOException, ErrnoException {
+        apfFilter.pretendPacketReceived(packet.array());
+        ipClientCallback.assertNoProgramUpdate();
+    }
+
+    @Test
+    public void testApfFilterRa() throws Exception {
+        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        ApfConfiguration config = getDefaultConfig();
+        config.multicastFilter = DROP_MULTICAST;
+        config.ieee802_3Filter = DROP_802_3_FRAMES;
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics, mDependencies);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        final int ROUTER_LIFETIME = 1000;
+        final int PREFIX_VALID_LIFETIME = 200;
+        final int PREFIX_PREFERRED_LIFETIME = 100;
+        final int RDNSS_LIFETIME  = 300;
+        final int ROUTE_LIFETIME  = 400;
+        // Note that lifetime of 2000 will be ignored in favor of shorter route lifetime of 1000.
+        final int DNSSL_LIFETIME  = 2000;
+
+        // Verify RA is passed the first time
+        RaPacketBuilder ra = new RaPacketBuilder(ROUTER_LIFETIME);
+        ByteBuffer basePacket = ByteBuffer.wrap(ra.build());
+        assertPass(program, basePacket.array());
+
+        verifyRaLifetime(apfFilter, ipClientCallback, basePacket, ROUTER_LIFETIME);
+
+        ra = new RaPacketBuilder(ROUTER_LIFETIME);
+        // Check that changes are ignored in every byte of the flow label.
+        ra.setFlowLabel(0x56789);
+        ByteBuffer newFlowLabelPacket = ByteBuffer.wrap(ra.build());
+
+        // Ensure zero-length options cause the packet to be silently skipped.
+        // Do this before we test other packets. http://b/29586253
+        ra = new RaPacketBuilder(ROUTER_LIFETIME);
+        ra.addZeroLengthOption();
+        ByteBuffer zeroLengthOptionPacket = ByteBuffer.wrap(ra.build());
+        assertInvalidRa(apfFilter, ipClientCallback, zeroLengthOptionPacket);
+
+        // Generate several RAs with different options and lifetimes, and verify when
+        // ApfFilter is shown these packets, it generates programs to filter them for the
+        // appropriate lifetime.
+        ra = new RaPacketBuilder(ROUTER_LIFETIME);
+        ra.addPioOption(PREFIX_VALID_LIFETIME, PREFIX_PREFERRED_LIFETIME, "2001:db8::/64");
+        ByteBuffer prefixOptionPacket = ByteBuffer.wrap(ra.build());
+        verifyRaLifetime(
+                apfFilter, ipClientCallback, prefixOptionPacket, PREFIX_PREFERRED_LIFETIME);
+
+        ra = new RaPacketBuilder(ROUTER_LIFETIME);
+        ra.addRdnssOption(RDNSS_LIFETIME, "2001:4860:4860::8888", "2001:4860:4860::8844");
+        ByteBuffer rdnssOptionPacket = ByteBuffer.wrap(ra.build());
+        verifyRaLifetime(apfFilter, ipClientCallback, rdnssOptionPacket, RDNSS_LIFETIME);
+
+        final int lowLifetime = 60;
+        ra = new RaPacketBuilder(ROUTER_LIFETIME);
+        ra.addRdnssOption(lowLifetime, "2620:fe::9");
+        ByteBuffer lowLifetimeRdnssOptionPacket = ByteBuffer.wrap(ra.build());
+        verifyRaLifetime(apfFilter, ipClientCallback, lowLifetimeRdnssOptionPacket,
+                ROUTER_LIFETIME);
+
+        ra = new RaPacketBuilder(ROUTER_LIFETIME);
+        ra.addRioOption(ROUTE_LIFETIME, "64:ff9b::/96");
+        ByteBuffer routeInfoOptionPacket = ByteBuffer.wrap(ra.build());
+        verifyRaLifetime(apfFilter, ipClientCallback, routeInfoOptionPacket, ROUTE_LIFETIME);
+
+        // Check that RIOs differing only in the first 4 bytes are different.
+        ra = new RaPacketBuilder(ROUTER_LIFETIME);
+        ra.addRioOption(ROUTE_LIFETIME, "64:ff9b::/64");
+        // Packet should be passed because it is different.
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        assertPass(program, ra.build());
+
+        ra = new RaPacketBuilder(ROUTER_LIFETIME);
+        ra.addDnsslOption(DNSSL_LIFETIME, "test.example.com", "one.more.example.com");
+        ByteBuffer dnsslOptionPacket = ByteBuffer.wrap(ra.build());
+        verifyRaLifetime(apfFilter, ipClientCallback, dnsslOptionPacket, ROUTER_LIFETIME);
+
+        ByteBuffer largeRaPacket = ByteBuffer.wrap(buildLargeRa());
+        verifyRaLifetime(apfFilter, ipClientCallback, largeRaPacket, 300);
+
+        // Verify that current program filters all the RAs (note: ApfFilter.MAX_RAS == 10).
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        verifyRaLifetime(program, basePacket, ROUTER_LIFETIME);
+        verifyRaLifetime(program, newFlowLabelPacket, ROUTER_LIFETIME);
+        verifyRaLifetime(program, prefixOptionPacket, PREFIX_PREFERRED_LIFETIME);
+        verifyRaLifetime(program, rdnssOptionPacket, RDNSS_LIFETIME);
+        verifyRaLifetime(program, lowLifetimeRdnssOptionPacket, ROUTER_LIFETIME);
+        verifyRaLifetime(program, routeInfoOptionPacket, ROUTE_LIFETIME);
+        verifyRaLifetime(program, dnsslOptionPacket, ROUTER_LIFETIME);
+        verifyRaLifetime(program, largeRaPacket, 300);
+    }
+
+    @Test
+    public void testRaWithDifferentReachableTimeAndRetransTimer() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        final ApfConfiguration config = getDefaultConfig();
+        config.multicastFilter = DROP_MULTICAST;
+        config.ieee802_3Filter = DROP_802_3_FRAMES;
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics, mDependencies);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        final int RA_REACHABLE_TIME = 1800;
+        final int RA_RETRANSMISSION_TIMER = 1234;
+
+        // Create an Ra packet without options
+        // Reachable time = 1800, retransmission timer = 1234
+        RaPacketBuilder ra = new RaPacketBuilder(1800 /* router lft */);
+        ra.setReachableTime(RA_REACHABLE_TIME);
+        ra.setRetransmissionTimer(RA_RETRANSMISSION_TIMER);
+        byte[] raPacket = ra.build();
+        // First RA passes filter
+        assertPass(program, raPacket);
+
+        // Assume apf is shown the given RA, it generates program to filter it.
+        apfFilter.pretendPacketReceived(raPacket);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        assertDrop(program, raPacket);
+
+        // A packet with different reachable time should be passed.
+        // Reachable time = 2300, retransmission timer = 1234
+        ra.setReachableTime(RA_REACHABLE_TIME + 500);
+        raPacket = ra.build();
+        assertPass(program, raPacket);
+
+        // A packet with different retransmission timer should be passed.
+        // Reachable time = 1800, retransmission timer = 2234
+        ra.setReachableTime(RA_REACHABLE_TIME);
+        ra.setRetransmissionTimer(RA_RETRANSMISSION_TIMER + 1000);
+        raPacket = ra.build();
+        assertPass(program, raPacket);
+    }
+
+    // The ByteBuffer is always created by ByteBuffer#wrap in the helper functions
+    @SuppressWarnings("ByteBufferBackingArray")
+    @Test
+    public void testRaWithProgramInstalledSomeTimeAfterLastSeen() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        final ApfConfiguration config = getDefaultConfig();
+        config.multicastFilter = DROP_MULTICAST;
+        config.ieee802_3Filter = DROP_802_3_FRAMES;
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics, mDependencies);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        final int routerLifetime = 1000;
+        final int timePassedSeconds = 12;
+
+        // Verify that when the program is generated and installed some time after RA is last seen
+        // it should be installed with the correct remaining lifetime.
+        ByteBuffer basePacket = ByteBuffer.wrap(new RaPacketBuilder(routerLifetime).build());
+        verifyRaLifetime(apfFilter, ipClientCallback, basePacket, routerLifetime);
+        apfFilter.increaseCurrentTimeSeconds(timePassedSeconds);
+        synchronized (apfFilter) {
+            apfFilter.installNewProgramLocked();
+        }
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        verifyRaLifetime(program, basePacket, routerLifetime, timePassedSeconds);
+
+        // Packet should be passed if the program is installed after 1/6 * lifetime from last seen
+        apfFilter.increaseCurrentTimeSeconds((int) (routerLifetime / 6) - timePassedSeconds - 1);
+        synchronized (apfFilter) {
+            apfFilter.installNewProgramLocked();
+        }
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        assertDrop(program, basePacket.array());
+        apfFilter.increaseCurrentTimeSeconds(1);
+        synchronized (apfFilter) {
+            apfFilter.installNewProgramLocked();
+        }
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        assertPass(program, basePacket.array());
+    }
+
+    /**
+     * Stage a file for testing, i.e. make it native accessible. Given a resource ID,
+     * copy that resource into the app's data directory and return the path to it.
+     */
+    private String stageFile(int rawId) throws Exception {
+        File file = new File(InstrumentationRegistry.getContext().getFilesDir(), "staged_file");
+        new File(file.getParent()).mkdirs();
+        InputStream in = null;
+        OutputStream out = null;
+        try {
+            in = InstrumentationRegistry.getContext().getResources().openRawResource(rawId);
+            out = new FileOutputStream(file);
+            Streams.copy(in, out);
+        } finally {
+            if (in != null) in.close();
+            if (out != null) out.close();
+        }
+        return file.getAbsolutePath();
+    }
+
+    private static void put(ByteBuffer buffer, int position, byte[] bytes) {
+        final int original = buffer.position();
+        buffer.position(position);
+        buffer.put(bytes);
+        buffer.position(original);
+    }
+
+    @Test
+    public void testRaParsing() throws Exception {
+        final int maxRandomPacketSize = 512;
+        final Random r = new Random();
+        MockIpClientCallback cb = new MockIpClientCallback();
+        ApfConfiguration config = getDefaultConfig();
+        config.multicastFilter = DROP_MULTICAST;
+        config.ieee802_3Filter = DROP_802_3_FRAMES;
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb, mNetworkQuirkMetrics,
+                mDependencies);
+        for (int i = 0; i < 1000; i++) {
+            byte[] packet = new byte[r.nextInt(maxRandomPacketSize + 1)];
+            r.nextBytes(packet);
+            try {
+                apfFilter.new Ra(packet, packet.length);
+            } catch (ApfFilter.InvalidRaException e) {
+            } catch (Exception e) {
+                throw new Exception("bad packet: " + HexDump.toHexString(packet), e);
+            }
+        }
+    }
+
+    @Test
+    public void testRaProcessing() throws Exception {
+        final int maxRandomPacketSize = 512;
+        final Random r = new Random();
+        MockIpClientCallback cb = new MockIpClientCallback();
+        ApfConfiguration config = getDefaultConfig();
+        config.multicastFilter = DROP_MULTICAST;
+        config.ieee802_3Filter = DROP_802_3_FRAMES;
+        TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb, mNetworkQuirkMetrics,
+                mDependencies);
+        for (int i = 0; i < 1000; i++) {
+            byte[] packet = new byte[r.nextInt(maxRandomPacketSize + 1)];
+            r.nextBytes(packet);
+            try {
+                apfFilter.processRa(packet, packet.length);
+            } catch (Exception e) {
+                throw new Exception("bad packet: " + HexDump.toHexString(packet), e);
+            }
+        }
+    }
+
+    @Test
+    public void testMatchedRaUpdatesLifetime() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, getDefaultConfig(),
+                ipClientCallback, mNetworkQuirkMetrics, mDependencies);
+
+        // Create an RA and build an APF program
+        byte[] ra = new RaPacketBuilder(1800 /* router lifetime */).build();
+        apfFilter.pretendPacketReceived(ra);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // lifetime dropped significantly, assert pass
+        ra = new RaPacketBuilder(200 /* router lifetime */).build();
+        assertPass(program, ra);
+
+        // update program with the new RA
+        apfFilter.pretendPacketReceived(ra);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // assert program was updated and new lifetimes were taken into account.
+        assertDrop(program, ra);
+    }
+
+    @Test
+    public void testProcessRaWithInfiniteLifeTimeWithoutCrash() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        // configure accept_ra_min_lft
+        final ApfConfiguration config = getDefaultConfig();
+        config.acceptRaMinLft = 180;
+        TestApfFilter apfFilter;
+        // Template packet:
+        // Frame 1: 150 bytes on wire (1200 bits), 150 bytes captured (1200 bits)
+        // Ethernet II, Src: Netgear_23:67:2c (28:c6:8e:23:67:2c), Dst: IPv6mcast_01 (33:33:00:00:00:01)
+        // Internet Protocol Version 6, Src: fe80::2ac6:8eff:fe23:672c, Dst: ff02::1
+        // Internet Control Message Protocol v6
+        //   Type: Router Advertisement (134)
+        //   Code: 0
+        //   Checksum: 0x0acd [correct]
+        //   Checksum Status: Good
+        //   Cur hop limit: 64
+        //   Flags: 0xc0, Managed address configuration, Other configuration, Prf (Default Router Preference): Medium
+        //   Router lifetime (s): 7000
+        //   Reachable time (ms): 0
+        //   Retrans timer (ms): 0
+        //   ICMPv6 Option (Source link-layer address : 28:c6:8e:23:67:2c)
+        //     Type: Source link-layer address (1)
+        //     Length: 1 (8 bytes)
+        //     Link-layer address: Netgear_23:67:2c (28:c6:8e:23:67:2c)
+        //     Source Link-layer address: Netgear_23:67:2c (28:c6:8e:23:67:2c)
+        //   ICMPv6 Option (MTU : 1500)
+        //     Type: MTU (5)
+        //     Length: 1 (8 bytes)
+        //     Reserved
+        //     MTU: 1500
+        //   ICMPv6 Option (Prefix information : 2401:fa00:480:f000::/64)
+        //     Type: Prefix information (3)
+        //     Length: 4 (32 bytes)
+        //     Prefix Length: 64
+        //     Flag: 0xc0, On-link flag(L), Autonomous address-configuration flag(A)
+        //     Valid Lifetime: Infinity (4294967295)
+        //     Preferred Lifetime: Infinity (4294967295)
+        //     Reserved
+        //     Prefix: 2401:fa00:480:f000::
+        //   ICMPv6 Option (Recursive DNS Server 2401:fa00:480:f000::1)
+        //     Type: Recursive DNS Server (25)
+        //     Length: 3 (24 bytes)
+        //     Reserved
+        //     Lifetime: 7000
+        //     Recursive DNS Servers: 2401:fa00:480:f000::1
+        //   ICMPv6 Option (Advertisement Interval : 600000)
+        //     Type: Advertisement Interval (7)
+        //     Length: 1 (8 bytes)
+        //     Reserved
+        //     Advertisement Interval: 600000
+        final String packetStringFmt = "33330000000128C68E23672C86DD60054C6B00603AFFFE800000000000002AC68EFFFE23672CFF02000000000000000000000000000186000ACD40C01B580000000000000000010128C68E23672C05010000000005DC030440C0%s000000002401FA000480F00000000000000000001903000000001B582401FA000480F000000000000000000107010000000927C0";
+        final List<String> lifetimes = List.of("FFFFFFFF", "00000000", "00000001", "00001B58");
+        for (String lifetime : lifetimes) {
+            apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mNetworkQuirkMetrics,
+                    mDependencies);
+            final byte[] ra = hexStringToByteArray(
+                    String.format(packetStringFmt, lifetime + lifetime));
+            // feed the RA into APF and generate the filter, the filter shouldn't crash.
+            apfFilter.pretendPacketReceived(ra);
+            ipClientCallback.assertProgramUpdateAndGet();
+        }
+    }
+
+    // Test for go/apf-ra-filter Case 1a.
+    // Old lifetime is 0
+    @Test
+    public void testAcceptRaMinLftCase1a() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        // configure accept_ra_min_lft
+        final ApfConfiguration config = getDefaultConfig();
+        config.acceptRaMinLft = 180;
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics, mDependencies);
+
+        // Create an initial RA and build an APF program
+        byte[] ra = new RaPacketBuilder(1800 /* router lifetime */)
+                .addPioOption(1800 /*valid*/, 0 /*preferred*/, "2001:db8::/64")
+                .build();
+
+        apfFilter.pretendPacketReceived(ra);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // repeated RA is dropped
+        assertDrop(program, ra);
+
+        // PIO preferred lifetime increases
+        ra = new RaPacketBuilder(1800 /* router lifetime */)
+                .addPioOption(1800 /*valid*/, 1 /*preferred*/, "2001:db8::/64")
+                .build();
+        assertPass(program, ra);
+    }
+
+    // Test for go/apf-ra-filter Case 2a.
+    // Old lifetime is > 0
+    @Test
+    public void testAcceptRaMinLftCase2a() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        // configure accept_ra_min_lft
+        final ApfConfiguration config = getDefaultConfig();
+        config.acceptRaMinLft = 180;
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics, mDependencies);
+
+        // Create an initial RA and build an APF program
+        byte[] ra = new RaPacketBuilder(1800 /* router lifetime */)
+                .addPioOption(1800 /*valid*/, 100 /*preferred*/, "2001:db8::/64")
+                .build();
+
+        apfFilter.pretendPacketReceived(ra);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // repeated RA is dropped
+        assertDrop(program, ra);
+
+        // PIO preferred lifetime increases
+        ra = new RaPacketBuilder(1800 /* router lifetime */)
+                .addPioOption(1800 /*valid*/, 101 /*preferred*/, "2001:db8::/64")
+                .build();
+        assertPass(program, ra);
+
+        // PIO preferred lifetime decreases significantly
+        ra = new RaPacketBuilder(1800 /* router lifetime */)
+                .addPioOption(1800 /*valid*/, 33 /*preferred*/, "2001:db8::/64")
+                .build();
+        assertPass(program, ra);
+    }
+
+
+    // Test for go/apf-ra-filter Case 1b.
+    // Old lifetime is 0
+    @Test
+    public void testAcceptRaMinLftCase1b() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        // configure accept_ra_min_lft
+        final ApfConfiguration config = getDefaultConfig();
+        config.acceptRaMinLft = 180;
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics, mDependencies);
+
+        // Create an initial RA and build an APF program
+        byte[] ra = new RaPacketBuilder(0 /* router lifetime */).build();
+
+        apfFilter.pretendPacketReceived(ra);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // repeated RA is dropped
+        assertDrop(program, ra);
+
+        // lifetime increases below accept_ra_min_lft
+        ra = new RaPacketBuilder(179 /* router lifetime */).build();
+        assertDrop(program, ra);
+
+        // lifetime increases to accept_ra_min_lft
+        ra = new RaPacketBuilder(180 /* router lifetime */).build();
+        assertPass(program, ra);
+    }
+
+
+    // Test for go/apf-ra-filter Case 2b.
+    // Old lifetime is < accept_ra_min_lft (but not 0).
+    @Test
+    public void testAcceptRaMinLftCase2b() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        // configure accept_ra_min_lft
+        final ApfConfiguration config = getDefaultConfig();
+        config.acceptRaMinLft = 180;
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics, mDependencies);
+
+        // Create an initial RA and build an APF program
+        byte[] ra = new RaPacketBuilder(100 /* router lifetime */).build();
+
+        apfFilter.pretendPacketReceived(ra);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // repeated RA is dropped
+        assertDrop(program, ra);
+
+        // lifetime increases
+        ra = new RaPacketBuilder(101 /* router lifetime */).build();
+        assertDrop(program, ra);
+
+        // lifetime decreases significantly
+        ra = new RaPacketBuilder(1 /* router lifetime */).build();
+        assertDrop(program, ra);
+
+        // equals accept_ra_min_lft
+        ra = new RaPacketBuilder(180 /* router lifetime */).build();
+        assertPass(program, ra);
+
+        // lifetime is 0
+        ra = new RaPacketBuilder(0 /* router lifetime */).build();
+        assertPass(program, ra);
+    }
+
+    // Test for go/apf-ra-filter Case 3b.
+    // Old lifetime is >= accept_ra_min_lft and <= 3 * accept_ra_min_lft
+    @Test
+    public void testAcceptRaMinLftCase3b() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        // configure accept_ra_min_lft
+        final ApfConfiguration config = getDefaultConfig();
+        config.acceptRaMinLft = 180;
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics, mDependencies);
+
+        // Create an initial RA and build an APF program
+        byte[] ra = new RaPacketBuilder(200 /* router lifetime */).build();
+
+        apfFilter.pretendPacketReceived(ra);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // repeated RA is dropped
+        assertDrop(program, ra);
+
+        // lifetime increases
+        ra = new RaPacketBuilder(201 /* router lifetime */).build();
+        assertPass(program, ra);
+
+        // lifetime is below accept_ra_min_lft (but not 0)
+        ra = new RaPacketBuilder(1 /* router lifetime */).build();
+        assertDrop(program, ra);
+
+        // lifetime is 0
+        ra = new RaPacketBuilder(0 /* router lifetime */).build();
+        assertPass(program, ra);
+    }
+
+    // Test for go/apf-ra-filter Case 4b.
+    // Old lifetime is > 3 * accept_ra_min_lft
+    @Test
+    public void testAcceptRaMinLftCase4b() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        // configure accept_ra_min_lft
+        final ApfConfiguration config = getDefaultConfig();
+        config.acceptRaMinLft = 180;
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics, mDependencies);
+
+        // Create an initial RA and build an APF program
+        byte[] ra = new RaPacketBuilder(1800 /* router lifetime */).build();
+
+        apfFilter.pretendPacketReceived(ra);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // repeated RA is dropped
+        assertDrop(program, ra);
+
+        // lifetime increases
+        ra = new RaPacketBuilder(1801 /* router lifetime */).build();
+        assertPass(program, ra);
+
+        // lifetime is 1/3 of old lft
+        ra = new RaPacketBuilder(600 /* router lifetime */).build();
+        assertDrop(program, ra);
+
+        // lifetime is below 1/3 of old lft
+        ra = new RaPacketBuilder(599 /* router lifetime */).build();
+        assertPass(program, ra);
+
+        // lifetime is below accept_ra_min_lft (but not 0)
+        ra = new RaPacketBuilder(1 /* router lifetime */).build();
+        assertDrop(program, ra);
+
+        // lifetime is 0
+        ra = new RaPacketBuilder(0 /* router lifetime */).build();
+        assertPass(program, ra);
+    }
+
+    @Test
+    public void testRaFilterIsUpdated() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        // configure accept_ra_min_lft
+        final ApfConfiguration config = getDefaultConfig();
+        config.acceptRaMinLft = 180;
+        final TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
+                mNetworkQuirkMetrics, mDependencies);
+
+        // Create an initial RA and build an APF program
+        byte[] ra = new RaPacketBuilder(1800 /* router lifetime */).build();
+        apfFilter.pretendPacketReceived(ra);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        // repeated RA is dropped.
+        assertDrop(program, ra);
+
+        // updated RA is passed, repeated RA is dropped after program update.
+        ra = new RaPacketBuilder(599 /* router lifetime */).build();
+        assertPass(program, ra);
+        apfFilter.pretendPacketReceived(ra);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        assertDrop(program, ra);
+
+        ra = new RaPacketBuilder(180 /* router lifetime */).build();
+        assertPass(program, ra);
+        apfFilter.pretendPacketReceived(ra);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        assertDrop(program, ra);
+
+        ra = new RaPacketBuilder(0 /* router lifetime */).build();
+        assertPass(program, ra);
+        apfFilter.pretendPacketReceived(ra);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        assertDrop(program, ra);
+
+        ra = new RaPacketBuilder(180 /* router lifetime */).build();
+        assertPass(program, ra);
+        apfFilter.pretendPacketReceived(ra);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        assertDrop(program, ra);
+
+        ra = new RaPacketBuilder(599 /* router lifetime */).build();
+        assertPass(program, ra);
+        apfFilter.pretendPacketReceived(ra);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        assertDrop(program, ra);
+
+        ra = new RaPacketBuilder(1800 /* router lifetime */).build();
+        assertPass(program, ra);
+        apfFilter.pretendPacketReceived(ra);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        assertDrop(program, ra);
+    }
+
+    private TestAndroidPacketFilter makeTestApfFilter(ApfConfiguration config,
+            MockIpClientCallback ipClientCallback) throws Exception {
+        return new TestLegacyApfFilter(mContext, config, ipClientCallback, mIpConnectivityLog,
+                mNetworkQuirkMetrics, mDependencies, mClock);
+    }
+
+
+    @Test
+    public void testInstallPacketFilterFailure_LegacyApfFilter() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback(false);
+        final ApfConfiguration config = getDefaultConfig();
+        final TestAndroidPacketFilter apfFilter = makeTestApfFilter(config, ipClientCallback);
+        verify(mNetworkQuirkMetrics).setEvent(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
+        verify(mNetworkQuirkMetrics).statsWrite();
+        reset(mNetworkQuirkMetrics);
+        synchronized (apfFilter) {
+            apfFilter.installNewProgramLocked();
+        }
+        verify(mNetworkQuirkMetrics).setEvent(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
+        verify(mNetworkQuirkMetrics).statsWrite();
+    }
+
+    @Test
+    public void testApfProgramOverSize_LegacyApfFilter() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        final ApfConfiguration config = getDefaultConfig();
+        final ApfCapabilities capabilities = new ApfCapabilities(2, 512, ARPHRD_ETHER);
+        config.apfCapabilities = capabilities;
+        final TestAndroidPacketFilter apfFilter = makeTestApfFilter(config, ipClientCallback);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        final byte[] ra = buildLargeRa();
+        apfFilter.pretendPacketReceived(ra);
+        // The generated program size will be 529, which is larger than 512
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        verify(mNetworkQuirkMetrics).setEvent(NetworkQuirkEvent.QE_APF_OVER_SIZE_FAILURE);
+        verify(mNetworkQuirkMetrics).statsWrite();
+    }
+
+    @Test
+    public void testGenerateApfProgramException_LegacyApfFilter() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        final ApfConfiguration config = getDefaultConfig();
+        final TestAndroidPacketFilter apfFilter;
+        apfFilter = new TestLegacyApfFilter(mContext, config, ipClientCallback, mIpConnectivityLog,
+                mNetworkQuirkMetrics, mDependencies,
+                true /* throwsExceptionWhenGeneratesProgram */);
+        synchronized (apfFilter) {
+            apfFilter.installNewProgramLocked();
+        }
+        verify(mNetworkQuirkMetrics).setEvent(NetworkQuirkEvent.QE_APF_GENERATE_FILTER_EXCEPTION);
+        verify(mNetworkQuirkMetrics).statsWrite();
+    }
+
+    @Test
+    public void testApfSessionInfoMetrics_LegacyApfFilter() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        final ApfConfiguration config = getDefaultConfig();
+        final ApfCapabilities capabilities = new ApfCapabilities(4, 4096, ARPHRD_ETHER);
+        config.apfCapabilities = capabilities;
+        final long startTimeMs = 12345;
+        final long durationTimeMs = config.minMetricsSessionDurationMs;
+        doReturn(startTimeMs).when(mClock).elapsedRealtime();
+        final TestAndroidPacketFilter apfFilter = makeTestApfFilter(config, ipClientCallback);
+        int maxProgramSize = 0;
+        int numProgramUpdated = 0;
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        maxProgramSize = Math.max(maxProgramSize, program.length);
+        numProgramUpdated++;
+
+        final byte[] data = new byte[Counter.totalSize()];
+        final byte[] expectedData = data.clone();
+        final int totalPacketsCounterIdx = Counter.totalSize() + Counter.TOTAL_PACKETS.offset();
+        final int passedIpv6IcmpCounterIdx =
+                Counter.totalSize() + Counter.PASSED_IPV6_ICMP.offset();
+        final int droppedIpv4MulticastIdx =
+                Counter.totalSize() + Counter.DROPPED_IPV4_MULTICAST.offset();
+
+        // Receive an RA packet (passed).
+        final byte[] ra = buildLargeRa();
+        expectedData[totalPacketsCounterIdx + 3] += 1;
+        expectedData[passedIpv6IcmpCounterIdx + 3] += 1;
+        assertDataMemoryContentsIgnoreVersion(PASS, program, ra, data, expectedData);
+        apfFilter.pretendPacketReceived(ra);
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        maxProgramSize = Math.max(maxProgramSize, program.length);
+        numProgramUpdated++;
+
+        apfFilter.setMulticastFilter(true);
+        // setMulticastFilter will trigger program installation.
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        maxProgramSize = Math.max(maxProgramSize, program.length);
+        numProgramUpdated++;
+
+        // Receive IPv4 multicast packet (dropped).
+        final byte[] multicastIpv4Addr = {(byte) 224, 0, 0, 1};
+        ByteBuffer mcastv4packet = makeIpv4Packet(IPPROTO_UDP);
+        put(mcastv4packet, IPV4_DEST_ADDR_OFFSET, multicastIpv4Addr);
+        expectedData[totalPacketsCounterIdx + 3] += 1;
+        expectedData[droppedIpv4MulticastIdx + 3] += 1;
+        assertDataMemoryContentsIgnoreVersion(DROP, program, mcastv4packet.array(), data,
+                expectedData);
+
+        // Set data snapshot and update counters.
+        apfFilter.setDataSnapshot(data);
+
+        // Write metrics data to statsd pipeline when shutdown.
+        doReturn(startTimeMs + durationTimeMs).when(mClock).elapsedRealtime();
+        apfFilter.shutdown();
+        verify(mApfSessionInfoMetrics).setVersion(4);
+        verify(mApfSessionInfoMetrics).setMemorySize(4096);
+
+        // Verify Counters
+        final Map<Counter, Long> expectedCounters = Map.of(Counter.TOTAL_PACKETS, 2L,
+                Counter.PASSED_IPV6_ICMP, 1L, Counter.DROPPED_IPV4_MULTICAST, 1L);
+        final ArgumentCaptor<Counter> counterCaptor = ArgumentCaptor.forClass(Counter.class);
+        final ArgumentCaptor<Long> valueCaptor = ArgumentCaptor.forClass(Long.class);
+        verify(mApfSessionInfoMetrics, times(expectedCounters.size())).addApfCounter(
+                counterCaptor.capture(), valueCaptor.capture());
+        final List<Counter> counters = counterCaptor.getAllValues();
+        final List<Long> values = valueCaptor.getAllValues();
+        final ArrayMap<Counter, Long> capturedCounters = new ArrayMap<>();
+        for (int i = 0; i < counters.size(); i++) {
+            capturedCounters.put(counters.get(i), values.get(i));
+        }
+        assertEquals(expectedCounters, capturedCounters);
+
+        verify(mApfSessionInfoMetrics).setApfSessionDurationSeconds(
+                (int) (durationTimeMs / DateUtils.SECOND_IN_MILLIS));
+        verify(mApfSessionInfoMetrics).setNumOfTimesApfProgramUpdated(numProgramUpdated);
+        verify(mApfSessionInfoMetrics).setMaxProgramSize(maxProgramSize);
+        verify(mApfSessionInfoMetrics).statsWrite();
+    }
+
+    @Test
+    public void testIpClientRaInfoMetrics_LegacyApfFilter() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        final ApfConfiguration config = getDefaultConfig();
+        final long startTimeMs = 12345;
+        final long durationTimeMs = config.minMetricsSessionDurationMs;
+        doReturn(startTimeMs).when(mClock).elapsedRealtime();
+        final TestAndroidPacketFilter apfFilter = makeTestApfFilter(config, ipClientCallback);
+        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+
+        final int routerLifetime = 1000;
+        final int prefixValidLifetime = 200;
+        final int prefixPreferredLifetime = 100;
+        final int rdnssLifetime  = 300;
+        final int routeLifetime  = 400;
+
+        // Construct 2 RAs with partial lifetimes larger than predefined constants
+        final RaPacketBuilder ra1 = new RaPacketBuilder(routerLifetime);
+        ra1.addPioOption(prefixValidLifetime + 123, prefixPreferredLifetime, "2001:db8::/64");
+        ra1.addRdnssOption(rdnssLifetime, "2001:4860:4860::8888", "2001:4860:4860::8844");
+        ra1.addRioOption(routeLifetime + 456, "64:ff9b::/96");
+        final RaPacketBuilder ra2 = new RaPacketBuilder(routerLifetime + 123);
+        ra2.addPioOption(prefixValidLifetime, prefixPreferredLifetime, "2001:db9::/64");
+        ra2.addRdnssOption(rdnssLifetime + 456, "2001:4860:4860::8888", "2001:4860:4860::8844");
+        ra2.addRioOption(routeLifetime, "64:ff9b::/96");
+
+        // Construct an invalid RA packet
+        final RaPacketBuilder raInvalid = new RaPacketBuilder(routerLifetime);
+        raInvalid.addZeroLengthOption();
+
+        // Construct 4 different kinds of zero lifetime RAs
+        final RaPacketBuilder raZeroRouterLifetime = new RaPacketBuilder(0 /* routerLft */);
+        final RaPacketBuilder raZeroPioValidLifetime = new RaPacketBuilder(routerLifetime);
+        raZeroPioValidLifetime.addPioOption(0, prefixPreferredLifetime, "2001:db10::/64");
+        final RaPacketBuilder raZeroRdnssLifetime = new RaPacketBuilder(routerLifetime);
+        raZeroRdnssLifetime.addPioOption(
+                prefixValidLifetime, prefixPreferredLifetime, "2001:db11::/64");
+        raZeroRdnssLifetime.addRdnssOption(0, "2001:4860:4860::8888", "2001:4860:4860::8844");
+        final RaPacketBuilder raZeroRioRouteLifetime = new RaPacketBuilder(routerLifetime);
+        raZeroRioRouteLifetime.addPioOption(
+                prefixValidLifetime, prefixPreferredLifetime, "2001:db12::/64");
+        raZeroRioRouteLifetime.addRioOption(0, "64:ff9b::/96");
+
+        // Inject RA packets. Calling assertProgramUpdateAndGet()/assertNoProgramUpdate() is to make
+        // sure that the RA packet has been processed.
+        apfFilter.pretendPacketReceived(ra1.build());
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        apfFilter.pretendPacketReceived(ra2.build());
+        program = ipClientCallback.assertProgramUpdateAndGet();
+        apfFilter.pretendPacketReceived(raInvalid.build());
+        ipClientCallback.assertNoProgramUpdate();
+        apfFilter.pretendPacketReceived(raZeroRouterLifetime.build());
+        ipClientCallback.assertNoProgramUpdate();
+        apfFilter.pretendPacketReceived(raZeroPioValidLifetime.build());
+        ipClientCallback.assertNoProgramUpdate();
+        apfFilter.pretendPacketReceived(raZeroRdnssLifetime.build());
+        ipClientCallback.assertNoProgramUpdate();
+        apfFilter.pretendPacketReceived(raZeroRioRouteLifetime.build());
+        ipClientCallback.assertNoProgramUpdate();
+
+        // Write metrics data to statsd pipeline when shutdown.
+        doReturn(startTimeMs + durationTimeMs).when(mClock).elapsedRealtime();
+        apfFilter.shutdown();
+
+        // Verify each metric fields in IpClientRaInfoMetrics.
+        // LegacyApfFilter will purge expired RAs before adding new RA. Every time a new zero
+        // lifetime RA is received, zero lifetime RAs except the newly added one will be
+        // cleared, so the number of distinct RAs is 3 (ra1, ra2 and the newly added RA).
+        verify(mIpClientRaInfoMetrics).setMaxNumberOfDistinctRas(3);
+        verify(mIpClientRaInfoMetrics).setNumberOfZeroLifetimeRas(4);
+        verify(mIpClientRaInfoMetrics).setNumberOfParsingErrorRas(1);
+        verify(mIpClientRaInfoMetrics).setLowestRouterLifetimeSeconds(routerLifetime);
+        verify(mIpClientRaInfoMetrics).setLowestPioValidLifetimeSeconds(prefixValidLifetime);
+        verify(mIpClientRaInfoMetrics).setLowestRioRouteLifetimeSeconds(routeLifetime);
+        verify(mIpClientRaInfoMetrics).setLowestRdnssLifetimeSeconds(rdnssLifetime);
+        verify(mIpClientRaInfoMetrics).statsWrite();
+    }
+
+    @Test
+    public void testNoMetricsWrittenForShortDuration_LegacyApfFilter() throws Exception {
+        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
+        final ApfConfiguration config = getDefaultConfig();
+        final long startTimeMs = 12345;
+        final long durationTimeMs = config.minMetricsSessionDurationMs;
+
+        // Verify no metrics data written to statsd for duration less than durationTimeMs.
+        doReturn(startTimeMs).when(mClock).elapsedRealtime();
+        final TestAndroidPacketFilter apfFilter = makeTestApfFilter(config, ipClientCallback);
+        doReturn(startTimeMs + durationTimeMs - 1).when(mClock).elapsedRealtime();
+        apfFilter.shutdown();
+        verify(mApfSessionInfoMetrics, never()).statsWrite();
+        verify(mIpClientRaInfoMetrics, never()).statsWrite();
+
+        // Verify metrics data written to statsd for duration greater than or equal to
+        // durationTimeMs.
+        ApfFilter.Clock clock = mock(ApfFilter.Clock.class);
+        doReturn(startTimeMs).when(clock).elapsedRealtime();
+        final TestAndroidPacketFilter apfFilter2 = new TestApfFilter(mContext, config,
+                ipClientCallback, mNetworkQuirkMetrics, mDependencies, clock);
+        doReturn(startTimeMs + durationTimeMs).when(clock).elapsedRealtime();
+        apfFilter2.shutdown();
+        verify(mApfSessionInfoMetrics).statsWrite();
+        verify(mIpClientRaInfoMetrics).statsWrite();
+    }
+}
diff --git a/tests/unit/src/android/net/ip/IpClientTest.java b/tests/unit/src/android/net/ip/IpClientTest.java
index 8d99b11..00982c7 100644
--- a/tests/unit/src/android/net/ip/IpClientTest.java
+++ b/tests/unit/src/android/net/ip/IpClientTest.java
@@ -83,6 +83,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.netlink.NduseroptMessage;
 import com.android.net.module.util.netlink.RtNetlinkAddressMessage;
@@ -884,7 +885,8 @@
                 any(), configCaptor.capture(), any(), any(), any(), anyBoolean());
         final ApfConfiguration actual = configCaptor.getValue();
         assertNotNull(actual);
-        assertEquals(4, actual.apfCapabilities.apfVersionSupported);
+        int expectedApfVersion = SdkLevel.isAtLeastS() ? 4 : 3;
+        assertEquals(expectedApfVersion, actual.apfCapabilities.apfVersionSupported);
         assertEquals(4096, actual.apfCapabilities.maximumApfProgramSize);
         assertEquals(4, actual.apfCapabilities.apfPacketFormat);