Snap for 12359238 from 9b8fc71c1070e391c0c3f54ef2ca89bed2fe1b4f to android15-tests-release

Change-Id: I3b1a97f3a8e2001ae83628c17be58d5ac19bd87b
diff --git a/Android.bp b/Android.bp
index 3712ba6..1228079 100644
--- a/Android.bp
+++ b/Android.bp
@@ -14,32 +14,6 @@
 // limitations under the License.
 //
 
-// The network stack can be compiled using system_current (non-finalized) SDK, or finalized system_X
-// SDK. There is also a variant that uses system_current SDK and runs in the system process
-// (InProcessNetworkStack). The following structure is used to create the build rules:
-//
-//                          NetworkStackAndroidLibraryDefaults <-- common defaults for android libs
-//                                            /    \
-//           +NetworkStackApiStableShims --> /      \ <-- +NetworkStackApiCurrentShims
-//           +NetworkStackReleaseApiLevel   /        \    +NetworkStackDevApiLevel
-//           +jarjar apishim.api[latest].* /          \
-//            to apishim.*                /            \
-//                                       /              \
-//                                      /                \
-//                                     /                  \               android libs w/ all code
-//                                    / <- +module src/ -> \              (also used in unit tests)
-//                                   /                      \                        |
-//               NetworkStackApiStableLib               NetworkStackApiCurrentLib <--*
-//                          |                                     |
-//                          | <--   +NetworkStackAppDefaults  --> |
-//                          |          (APK build params)         |
-//                          |                                     |
-//                          | <-- +NetworkStackReleaseApiLevel    | <-- +NetworkStackDevApiLevel
-//                          |                                     |
-//                          |                                     |
-//                NetworkStackApiStable          NetworkStack, InProcessNetworkStack, <-- APKs
-//                                                         TestNetworkStack
-
 // Common defaults to define SDK level
 package {
     default_team: "trendy_team_fwk_core_networking",
@@ -56,23 +30,6 @@
     enabled: true,
 }
 
-// This is a placeholder comment to avoid merge conflicts
-// as the above target may have different "enabled" values
-// depending on the branch
-
-java_defaults {
-    name: "NetworkStackDevApiLevel",
-    min_sdk_version: "30",
-    sdk_version: "module_current",
-    libs: [
-        "framework-configinfrastructure",
-        "framework-connectivity",
-        "framework-connectivity-t",
-        "framework-statsd",
-        "framework-wifi",
-    ],
-}
-
 // Common defaults for NetworkStack integration tests, root tests and coverage tests
 // to keep tests always running against the same target sdk version with NetworkStack.
 java_defaults {
@@ -90,6 +47,7 @@
         "framework-connectivity.stubs.module_lib",
         "framework-connectivity-t",
         "framework-statsd",
+        "framework-tethering",
         "framework-wifi",
     ],
 }
@@ -262,7 +220,7 @@
     name: "NetworkStackApiCurrentShims",
     defaults: [
         "NetworkStackShimsDefaults",
-        "NetworkStackDevApiLevel",
+        "NetworkStackReleaseApiLevel",
         "ConnectivityNextEnableDefaults",
     ],
     static_libs: [
@@ -343,7 +301,7 @@
 android_library {
     name: "NetworkStackApiCurrentLib",
     defaults: [
-        "NetworkStackDevApiLevel",
+        "NetworkStackReleaseApiLevel",
         "NetworkStackAndroidLibraryDefaults",
         "ConnectivityNextEnableDefaults",
     ],
@@ -484,7 +442,7 @@
     name: "InProcessNetworkStack",
     defaults: [
         "NetworkStackAppDefaults",
-        "NetworkStackDevApiLevel",
+        "NetworkStackReleaseApiLevel",
         "ConnectivityNextEnableDefaults",
     ],
     static_libs: ["NetworkStackApiCurrentLib"],
@@ -507,7 +465,7 @@
     name: "NetworkStackNextManifestBase",
     defaults: [
         "NetworkStackAppDefaults",
-        "NetworkStackDevApiLevel",
+        "NetworkStackReleaseApiLevel",
         "ConnectivityNextEnableDefaults",
     ],
     static_libs: ["NetworkStackApiCurrentLib"],
@@ -519,7 +477,7 @@
     name: "NetworkStackNext",
     defaults: [
         "NetworkStackAppDefaults",
-        "NetworkStackDevApiLevel",
+        "NetworkStackReleaseApiLevel",
         "ConnectivityNextEnableDefaults",
     ],
     static_libs: ["NetworkStackNextManifestBase"],
diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml
index 22591f0..32d423b 100644
--- a/res/values-el/strings.xml
+++ b/res/values-el/strings.xml
@@ -22,5 +22,5 @@
     <string name="notification_channel_description_network_venue_info" msgid="5131499595382733605">"Εμφανίζονται ειδοποιήσεις για να υποδείξουν ότι το δίκτυο διαθέτει σελίδα πληροφοριών για τον τόπο."</string>
     <string name="connected" msgid="4563643884927480998">"Συνδέθηκε"</string>
     <string name="tap_for_info" msgid="6849746325626883711">"Συνδέθηκε/Πατήστε για προβολή του ιστοτόπου"</string>
-    <string name="application_label" msgid="1322847171305285454">"Διαχείριση δικτύου"</string>
+    <string name="application_label" msgid="1322847171305285454">"Διαχειριστής δικτύου"</string>
 </resources>
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index 451d8c3..b595a9e 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -21,6 +21,6 @@
     <string name="notification_channel_name_network_venue_info" msgid="6526543187249265733">"اطلاعات محل شبکه"</string>
     <string name="notification_channel_description_network_venue_info" msgid="5131499595382733605">"اعلان‌هایی که نمایش داده می‌شوند تا نشان دهند شبکه صفحه اطلاعات محل دارد"</string>
     <string name="connected" msgid="4563643884927480998">"متصل"</string>
-    <string name="tap_for_info" msgid="6849746325626883711">"متصل شد / برای مشاهده وب‌سایت تک‌ضرب بزنید"</string>
+    <string name="tap_for_info" msgid="6849746325626883711">"متصل شد / برای مشاهده وب‌سایت ضربه بزنید"</string>
     <string name="application_label" msgid="1322847171305285454">"مدیر شبکه"</string>
 </resources>
diff --git a/src/android/net/apf/AndroidPacketFilter.java b/src/android/net/apf/AndroidPacketFilter.java
index 8c7ff05..6dd4fad 100644
--- a/src/android/net/apf/AndroidPacketFilter.java
+++ b/src/android/net/apf/AndroidPacketFilter.java
@@ -15,7 +15,6 @@
  */
 package android.net.apf;
 
-import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.LinkProperties;
 import android.net.NattKeepalivePacketDataParcelable;
@@ -104,7 +103,14 @@
      * 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;
+    default boolean hasDataAccess(int apfVersionSupported) {
+        return apfVersionSupported > 2;
+    }
+
+    /**
+     * Whether the ApfFilter supports generating ND offload code.
+     */
+    default boolean supportNdOffload() {
+        return false;
     }
 }
diff --git a/src/android/net/apf/ApfConstants.java b/src/android/net/apf/ApfConstants.java
index fe2cfd8..09be67b 100644
--- a/src/android/net/apf/ApfConstants.java
+++ b/src/android/net/apf/ApfConstants.java
@@ -65,7 +65,40 @@
     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;
+    // From RFC4861:
+    public static final int ICMP6_RA_HEADER_LEN = 16;
+    public static final int ICMP6_RA_CHECKSUM_OFFSET =
+            ETH_HEADER_LEN + IPV6_HEADER_LEN + 2;
+    public static final int ICMP6_RA_CHECKSUM_LEN = 2;
+    public static final int ICMP6_RA_OPTION_OFFSET =
+            ETH_HEADER_LEN + IPV6_HEADER_LEN + ICMP6_RA_HEADER_LEN;
+    public static final int ICMP6_RA_ROUTER_LIFETIME_OFFSET =
+            ETH_HEADER_LEN + IPV6_HEADER_LEN + 6;
+    public static final int ICMP6_RA_ROUTER_LIFETIME_LEN = 2;
+    // Prefix information option.
+    public static final int ICMP6_PREFIX_OPTION_TYPE = 3;
+    public static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET = 4;
+    public static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN = 4;
+    public static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN = 4;
 
+    // From RFC4861: source link-layer address
+    public static final int ICMP6_SOURCE_LL_ADDRESS_OPTION_TYPE = 1;
+    // From RFC4861: mtu size option
+    public static final int ICMP6_MTU_OPTION_TYPE = 5;
+    // From RFC6106: Recursive DNS Server option
+    public static final int ICMP6_RDNSS_OPTION_TYPE = 25;
+    // From RFC6106: DNS Search List option
+    public static final int ICMP6_DNSSL_OPTION_TYPE = 31;
+    // From RFC8910: Captive-Portal option
+    public static final int ICMP6_CAPTIVE_PORTAL_OPTION_TYPE = 37;
+    // From RFC8781: PREF64 option
+    public static final int ICMP6_PREF64_OPTION_TYPE = 38;
+
+    // From RFC4191: Route Information option
+    public static final int ICMP6_ROUTE_INFO_OPTION_TYPE = 24;
+    // Above three options all have the same format:
+    public static final int ICMP6_4_BYTE_LIFETIME_OFFSET = 4;
+    public static final int ICMP6_4_BYTE_LIFETIME_LEN = 4;
     public static final int IPPROTO_HOPOPTS = 0;
 
     // NOTE: this must be added to the IPv4 header length in MemorySlot.IPV4_HEADER_SIZE
diff --git a/src/android/net/apf/ApfCounterTracker.java b/src/android/net/apf/ApfCounterTracker.java
index e86aab1..6c5b566 100644
--- a/src/android/net/apf/ApfCounterTracker.java
+++ b/src/android/net/apf/ApfCounterTracker.java
@@ -50,6 +50,7 @@
         APF_VERSION,
         APF_PROGRAM_ID,
         // TODO: removing PASSED_ARP after remove LegacyApfFilter.java
+        // The counter sequence should keep the same as ApfSessionInfoMetrics.java
         PASSED_ARP,  // see also MIN_PASS_COUNTER below.
         PASSED_ARP_BROADCAST_REPLY,
         // TODO: removing PASSED_ARP_NON_IPV4 after remove LegacyApfFilter.java
@@ -63,8 +64,10 @@
         PASSED_IPV4_UNICAST,
         PASSED_IPV6_ICMP,
         PASSED_IPV6_NON_ICMP,
-        PASSED_IPV6_NS_MULTIPLE_OPTIONS,
+        PASSED_IPV6_NS_DAD,
         PASSED_IPV6_NS_NO_ADDRESS,
+        PASSED_IPV6_NS_NO_SLLA_OPTION,
+        PASSED_IPV6_NS_TENTATIVE,
         PASSED_IPV6_UNICAST_NON_ICMP,
         PASSED_NON_IP_UNICAST,
         PASSED_MDNS,
@@ -83,6 +86,7 @@
         DROPPED_IPV6_NON_ICMP_MULTICAST,
         DROPPED_IPV6_NS_INVALID,
         DROPPED_IPV6_NS_OTHER_HOST,
+        DROPPED_IPV6_NS_REPLIED_NON_DAD,
         DROPPED_802_3_FRAME,
         DROPPED_ETHERTYPE_NOT_ALLOWED,
         DROPPED_IPV4_KEEPALIVE_ACK,
diff --git a/src/android/net/apf/ApfFilter.java b/src/android/net/apf/ApfFilter.java
index 3aee15e..c8e43d0 100644
--- a/src/android/net/apf/ApfFilter.java
+++ b/src/android/net/apf/ApfFilter.java
@@ -36,10 +36,28 @@
 import static android.net.apf.ApfConstants.ETH_TYPE_MAX;
 import static android.net.apf.ApfConstants.ETH_TYPE_MIN;
 import static android.net.apf.ApfConstants.FIXED_ARP_REPLY_HEADER;
+import static android.net.apf.ApfConstants.ICMP6_4_BYTE_LIFETIME_LEN;
+import static android.net.apf.ApfConstants.ICMP6_4_BYTE_LIFETIME_OFFSET;
+import static android.net.apf.ApfConstants.ICMP6_CAPTIVE_PORTAL_OPTION_TYPE;
 import static android.net.apf.ApfConstants.ICMP6_CHECKSUM_OFFSET;
 import static android.net.apf.ApfConstants.ICMP6_CODE_OFFSET;
+import static android.net.apf.ApfConstants.ICMP6_DNSSL_OPTION_TYPE;
+import static android.net.apf.ApfConstants.ICMP6_MTU_OPTION_TYPE;
 import static android.net.apf.ApfConstants.ICMP6_NS_OPTION_TYPE_OFFSET;
 import static android.net.apf.ApfConstants.ICMP6_NS_TARGET_IP_OFFSET;
+import static android.net.apf.ApfConstants.ICMP6_PREF64_OPTION_TYPE;
+import static android.net.apf.ApfConstants.ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN;
+import static android.net.apf.ApfConstants.ICMP6_PREFIX_OPTION_TYPE;
+import static android.net.apf.ApfConstants.ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN;
+import static android.net.apf.ApfConstants.ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET;
+import static android.net.apf.ApfConstants.ICMP6_RA_CHECKSUM_LEN;
+import static android.net.apf.ApfConstants.ICMP6_RA_CHECKSUM_OFFSET;
+import static android.net.apf.ApfConstants.ICMP6_RA_OPTION_OFFSET;
+import static android.net.apf.ApfConstants.ICMP6_RA_ROUTER_LIFETIME_LEN;
+import static android.net.apf.ApfConstants.ICMP6_RA_ROUTER_LIFETIME_OFFSET;
+import static android.net.apf.ApfConstants.ICMP6_RDNSS_OPTION_TYPE;
+import static android.net.apf.ApfConstants.ICMP6_ROUTE_INFO_OPTION_TYPE;
+import static android.net.apf.ApfConstants.ICMP6_SOURCE_LL_ADDRESS_OPTION_TYPE;
 import static android.net.apf.ApfConstants.ICMP6_TYPE_OFFSET;
 import static android.net.apf.ApfConstants.IPPROTO_HOPOPTS;
 import static android.net.apf.ApfConstants.IPV4_ANY_HOST_ADDRESS;
@@ -60,13 +78,18 @@
 import static android.net.apf.ApfConstants.IPV6_PAYLOAD_LEN_OFFSET;
 import static android.net.apf.ApfConstants.IPV6_SOLICITED_NODES_PREFIX;
 import static android.net.apf.ApfConstants.IPV6_SRC_ADDR_OFFSET;
+import static android.net.apf.ApfConstants.IPV6_UNSPECIFIED_ADDRESS;
 import static android.net.apf.ApfConstants.MDNS_PORT;
 import static android.net.apf.ApfConstants.TCP_HEADER_SIZE_OFFSET;
 import static android.net.apf.ApfConstants.TCP_UDP_DESTINATION_PORT_OFFSET;
 import static android.net.apf.ApfConstants.TCP_UDP_SOURCE_PORT_OFFSET;
 import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_INVALID;
 import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_OTHER_HOST;
-import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_MULTIPLE_OPTIONS;
+import static android.net.apf.ApfCounterTracker.Counter.FILTER_AGE_16384THS;
+import static android.net.apf.ApfCounterTracker.Counter.FILTER_AGE_SECONDS;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_DAD;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_NO_SLLA_OPTION;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_TENTATIVE;
 import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_NO_ADDRESS;
 import static android.net.apf.BaseApfGenerator.MemorySlot;
 import static android.net.apf.BaseApfGenerator.Register.R0;
@@ -75,7 +98,6 @@
 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.AF_PACKET;
-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;
@@ -92,6 +114,7 @@
 import static com.android.net.module.util.NetworkStackConstants.ETHER_SRC_ADDR_OFFSET;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NA_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_TLLA;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_TLLA_LEN;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT;
@@ -180,17 +203,18 @@
 
     // Helper class for specifying functional filter parameters.
     public static class ApfConfiguration {
-        public ApfCapabilities apfCapabilities;
+        public int apfVersionSupported;
+        public int apfRamSize;
         public int installableProgramSizeClamp = Integer.MAX_VALUE;
         public boolean multicastFilter;
         public boolean ieee802_3Filter;
         public int[] ethTypeBlackList;
         public int minRdnssLifetimeSec;
         public int acceptRaMinLft;
-        public boolean shouldHandleLightDoze;
         public long minMetricsSessionDurationMs;
         public boolean hasClatInterface;
         public boolean shouldHandleArpOffload;
+        public boolean shouldHandleNdOffload;
     }
 
     /** A wrapper class of {@link SystemClock} to be mocked in unit tests. */
@@ -241,15 +265,18 @@
     private static final boolean DBG = true;
     private static final boolean VDBG = false;
 
-    private final ApfCapabilities mApfCapabilities;
+    private final int mApfRamSize;
+    private final int mMaximumApfProgramSize;
     private final int mInstallableProgramSizeClamp;
     private final IpClientCallbacksWrapper mIpClientCallback;
     private final InterfaceParams mInterfaceParams;
     private final TokenBucket mTokenBucket;
 
     @VisibleForTesting
+    public final int mApfVersionSupported;
+    @VisibleForTesting
     @NonNull
-    public byte[] mHardwareAddress;
+    public final byte[] mHardwareAddress;
     @VisibleForTesting
     public ReceiveThread mReceiveThread;
     @GuardedBy("this")
@@ -261,7 +288,6 @@
     private final boolean mDrop802_3Frames;
     private final int[] mEthTypeBlackList;
 
-    private final Clock mClock;
     private final ApfCounterTracker mApfCounterTracker = new ApfCounterTracker();
     @GuardedBy("this")
     private final long mSessionStartMs;
@@ -287,8 +313,8 @@
     // Tracks the value of /proc/sys/ipv6/conf/$iface/accept_ra_min_lft which affects router, RIO,
     // and PIO valid lifetimes.
     private final int mAcceptRaMinLft;
-    private final boolean mShouldHandleLightDoze;
     private final boolean mShouldHandleArpOffload;
+    private final boolean mShouldHandleNdOffload;
 
     private final NetworkQuirkMetrics mNetworkQuirkMetrics;
     private final IpClientRaInfoMetrics mIpClientRaInfoMetrics;
@@ -305,9 +331,6 @@
         if (!SdkLevel.isAtLeastT()) {
             return false;
         }
-        if (!mShouldHandleLightDoze) {
-            return false;
-        }
         return ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED.equals(intent.getAction());
     }
 
@@ -318,9 +341,6 @@
         if (!SdkLevel.isAtLeastT()) {
             return false;
         }
-        if (!mShouldHandleLightDoze) {
-            return false;
-        }
 
         return powerManager.isDeviceLightIdleMode();
     }
@@ -368,37 +388,39 @@
     public ApfFilter(Context context, ApfConfiguration config, InterfaceParams ifParams,
             IpClientCallbacksWrapper ipClientCallback, NetworkQuirkMetrics networkQuirkMetrics) {
         this(context, config, ifParams, ipClientCallback, networkQuirkMetrics,
-                new Dependencies(context), new Clock());
+                new Dependencies(context));
     }
 
     @VisibleForTesting
     public ApfFilter(Context context, ApfConfiguration config, InterfaceParams ifParams,
             IpClientCallbacksWrapper ipClientCallback, NetworkQuirkMetrics networkQuirkMetrics,
             Dependencies dependencies) {
-        this(context, config, ifParams, ipClientCallback, networkQuirkMetrics, dependencies,
-                new Clock());
-    }
-
-    @VisibleForTesting
-    public ApfFilter(Context context, ApfConfiguration config, InterfaceParams ifParams,
-            IpClientCallbacksWrapper ipClientCallback, NetworkQuirkMetrics networkQuirkMetrics,
-            Dependencies dependencies, Clock clock) {
-        mApfCapabilities = config.apfCapabilities;
+        mApfVersionSupported = config.apfVersionSupported;
+        mApfRamSize = config.apfRamSize;
         mInstallableProgramSizeClamp = config.installableProgramSizeClamp;
+        int maximumApfProgramSize = mApfRamSize;
+        if (hasDataAccess(mApfVersionSupported)) {
+            // Reserve space for the counters.
+            maximumApfProgramSize -= Counter.totalSize();
+        }
+        // Prevent generating (and thus installing) larger programs
+        if (maximumApfProgramSize > mInstallableProgramSizeClamp) {
+            maximumApfProgramSize = mInstallableProgramSizeClamp;
+        }
+        mMaximumApfProgramSize = maximumApfProgramSize;
         mIpClientCallback = ipClientCallback;
         mInterfaceParams = ifParams;
         mMulticastFilter = config.multicastFilter;
         mDrop802_3Frames = config.ieee802_3Filter;
         mMinRdnssLifetimeSec = config.minRdnssLifetimeSec;
         mAcceptRaMinLft = config.acceptRaMinLft;
-        mShouldHandleLightDoze = config.shouldHandleLightDoze;
         mShouldHandleArpOffload = config.shouldHandleArpOffload;
+        mShouldHandleNdOffload = config.shouldHandleNdOffload;
         mDependencies = dependencies;
         mNetworkQuirkMetrics = networkQuirkMetrics;
         mIpClientRaInfoMetrics = dependencies.getIpClientRaInfoMetrics();
         mApfSessionInfoMetrics = dependencies.getApfSessionInfoMetrics();
-        mClock = clock;
-        mSessionStartMs = mClock.elapsedRealtime();
+        mSessionStartMs = dependencies.elapsedRealtime();
         mMinMetricsSessionDurationMs = config.minMetricsSessionDurationMs;
         mHasClat = config.hasClatInterface;
 
@@ -414,15 +436,18 @@
         // 3 seconds.
         mTokenBucket = new TokenBucket(3_000 /* deltaMs */, 20 /* capacity */, 20 /* tokens */);
 
+        mHardwareAddress = mInterfaceParams.macAddr.toByteArray();
         // TODO: ApfFilter should not generate programs until IpClient sends provisioning success.
-        maybeStartFilter();
+        startFilter();
 
         // Listen for doze-mode transition changes to enable/disable the IPv6 multicast filter.
-        mDependencies.addDeviceIdleReceiver(mDeviceIdleReceiver, mShouldHandleLightDoze);
+        mDependencies.addDeviceIdleReceiver(mDeviceIdleReceiver);
 
         mDependencies.onApfFilterCreated(this);
-        // mReceiveThread is created in maybeStartFilter() and halted in shutdown().
-        mDependencies.onThreadCreated(mReceiveThread);
+        // mReceiveThread is created in startFilter() and halted in shutdown().
+        if (mReceiveThread != null) {
+            mDependencies.onThreadCreated(mReceiveThread);
+        }
     }
 
     /**
@@ -435,11 +460,35 @@
             mContext = context;
         }
 
+        /**
+         * Create a socket to read RAs.
+         */
+        @Nullable
+        public FileDescriptor createRaReaderSocket(int ifIndex) {
+            FileDescriptor socket;
+            try {
+                socket = Os.socket(AF_PACKET, SOCK_RAW | SOCK_CLOEXEC, 0);
+                NetworkStackUtils.attachRaFilter(socket);
+                SocketAddress addr = makePacketSocketAddress(ETH_P_IPV6, ifIndex);
+                Os.bind(socket, addr);
+            } catch (SocketException | ErrnoException e) {
+                Log.wtf(TAG, "Error starting filter", e);
+                return null;
+            }
+            return socket;
+        }
+
+        /**
+         * Get elapsedRealtime.
+         */
+        public long elapsedRealtime() {
+            return SystemClock.elapsedRealtime();
+        }
+
         /** Add receiver for detecting doze mode change */
-        public void addDeviceIdleReceiver(@NonNull final BroadcastReceiver receiver,
-                boolean shouldHandleLightDoze) {
+        public void addDeviceIdleReceiver(@NonNull final BroadcastReceiver receiver) {
             final IntentFilter intentFilter = new IntentFilter(ACTION_DEVICE_IDLE_MODE_CHANGED);
-            if (SdkLevel.isAtLeastT() && shouldHandleLightDoze) {
+            if (SdkLevel.isAtLeastT()) {
                 intentFilter.addAction(ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED);
             }
             mContext.registerReceiver(receiver, intentFilter);
@@ -574,40 +623,32 @@
      * filters to ignore useless RAs.
      */
     @VisibleForTesting
-    public void maybeStartFilter() {
-        FileDescriptor socket;
-        try {
-            mHardwareAddress = mInterfaceParams.macAddr.toByteArray();
-            synchronized(this) {
-                // 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 && hasDataAccess(mApfCapabilities)) {
-                    byte[] zeroes = new byte[mApfCapabilities.maximumApfProgramSize];
-                    if (!mIpClientCallback.installPacketFilter(zeroes)) {
-                        sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
-                    }
+    public void startFilter() {
+        synchronized (this) {
+            // Clear the APF memory to reset all counters upon connecting to the first AP
+            // in an SSID. This is limited to APFv3 devices because this large write triggers
+            // a crash on some older devices (b/78905546).
+            if (hasDataAccess(mApfVersionSupported)) {
+                byte[] zeroes = new byte[mApfRamSize];
+                if (!mIpClientCallback.installPacketFilter(zeroes)) {
+                    sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
                 }
-
-                // Install basic filters
-                installNewProgramLocked();
             }
-            socket = Os.socket(AF_PACKET, SOCK_RAW | SOCK_CLOEXEC, 0);
-            NetworkStackUtils.attachRaFilter(socket);
-            SocketAddress addr = makePacketSocketAddress(ETH_P_IPV6, mInterfaceParams.index);
-            Os.bind(socket, addr);
-        } catch(SocketException|ErrnoException e) {
-            Log.e(TAG, "Error starting filter", e);
-            return;
+
+            // Install basic filters
+            installNewProgramLocked();
         }
-        mReceiveThread = new ReceiveThread(socket);
-        mReceiveThread.start();
+        FileDescriptor socket = mDependencies.createRaReaderSocket(mInterfaceParams.index);
+        if (socket != null) {
+            mReceiveThread = new ReceiveThread(socket);
+            mReceiveThread.start();
+        }
     }
 
     // Returns seconds since device boot.
     @VisibleForTesting
     protected int secondsSinceBoot() {
-        return (int) (mClock.elapsedRealtime() / DateUtils.SECOND_IN_MILLIS);
+        return (int) (mDependencies.elapsedRealtime() / DateUtils.SECOND_IN_MILLIS);
     }
 
     public static class InvalidRaException extends Exception {
@@ -669,41 +710,6 @@
     // A class to hold information about an RA.
     @VisibleForTesting
     public class Ra {
-        // From RFC4861:
-        private static final int ICMP6_RA_HEADER_LEN = 16;
-        private static final int ICMP6_RA_CHECKSUM_OFFSET =
-                ETH_HEADER_LEN + IPV6_HEADER_LEN + 2;
-        private static final int ICMP6_RA_CHECKSUM_LEN = 2;
-        private static final int ICMP6_RA_OPTION_OFFSET =
-                ETH_HEADER_LEN + IPV6_HEADER_LEN + ICMP6_RA_HEADER_LEN;
-        private static final int ICMP6_RA_ROUTER_LIFETIME_OFFSET =
-                ETH_HEADER_LEN + IPV6_HEADER_LEN + 6;
-        private static final int ICMP6_RA_ROUTER_LIFETIME_LEN = 2;
-        // Prefix information option.
-        private static final int ICMP6_PREFIX_OPTION_TYPE = 3;
-        private static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET = 4;
-        private static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN = 4;
-        private static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN = 4;
-
-        // From RFC4861: source link-layer address
-        private static final int ICMP6_SOURCE_LL_ADDRESS_OPTION_TYPE = 1;
-        // From RFC4861: mtu size option
-        private static final int ICMP6_MTU_OPTION_TYPE = 5;
-        // 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 RFC8910: Captive-Portal option
-        private static final int ICMP6_CAPTIVE_PORTAL_OPTION_TYPE = 37;
-        // From RFC8781: PREF64 option
-        private static final int ICMP6_PREF64_OPTION_TYPE = 38;
-
-        // 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_LIFETIME_OFFSET = 4;
-        private static final int ICMP6_4_BYTE_LIFETIME_LEN = 4;
-
         // Note: mPacket's position() cannot be assumed to be reset.
         private final ByteBuffer mPacket;
 
@@ -1934,22 +1940,63 @@
         v6Gen.addLoad8(R0, ICMP6_CODE_OFFSET)
                 .addCountAndDropIfR0NotEquals(0, DROPPED_IPV6_NS_INVALID);
 
-        // target address (ICMPv6 NS/NA payload) is not interface addresses -> drop
-        v6Gen.addLoadImmediate(R0, ICMP6_NS_TARGET_IP_OFFSET)
-                .addCountAndDropIfBytesAtR0EqualsNoneOf(allIPv6Addrs, DROPPED_IPV6_NS_OTHER_HOST);
+        // target address (ICMPv6 NS payload)
+        //   1) is one of tentative addresses -> pass
+        //   2) is none of {non-tentative, anycast} addresses -> drop
+        final List<byte[]> tentativeIPv6Addrs = getIpv6Addresses(
+                false, /* includeNonTentative */
+                true, /* includeTentative */
+                false /* includeAnycast */
+        );
+        v6Gen.addLoadImmediate(R0, ICMP6_NS_TARGET_IP_OFFSET);
+        if (!tentativeIPv6Addrs.isEmpty()) {
+            v6Gen.addCountAndPassIfBytesAtR0EqualsAnyOf(
+                    tentativeIPv6Addrs, PASSED_IPV6_NS_TENTATIVE);
+        }
 
-        // Only offload the following cases:
-        //   1) NS packet with no options.
-        //   2) NS packet with only one option: nonce.
-        //   3) NS packet with only one option: SLLA.
-        // For packets containing more than one option,
-        // pass the packet to the CPU for processing.
-        // payload length > 32
-        //   (8 bytes ICMP6 header + 16 bytes target address + 8 bytes option) -> pass
+        final List<byte[]> nonTentativeIpv6Addrs = getIpv6Addresses(
+                true, /* includeNonTentative */
+                false, /* includeTentative */
+                true /* includeAnycast */
+        );
+        if (nonTentativeIpv6Addrs.isEmpty()) {
+            v6Gen.addCountAndDrop(DROPPED_IPV6_NS_OTHER_HOST);
+            return;
+        }
+        v6Gen.addCountAndDropIfBytesAtR0EqualsNoneOf(
+                nonTentativeIpv6Addrs, DROPPED_IPV6_NS_OTHER_HOST);
+
+        // if source ip is unspecified (::), it's DAD request -> pass
+        v6Gen.addLoadImmediate(R0, IPV6_SRC_ADDR_OFFSET)
+                .addCountAndPassIfBytesAtR0Equal(IPV6_UNSPECIFIED_ADDRESS, PASSED_IPV6_NS_DAD);
+
+        // Only offload NUD/Address resolution packets that have SLLA as the their first option.
+        // For option-less NUD packets or NUD/Address resolution packets where
+        // the first option is not SLLA, pass them to the kernel for handling.
+        // if payload len < 32 -> pass
         v6Gen.addLoad16(R0, IPV6_PAYLOAD_LEN_OFFSET)
-                .addCountAndPassIfR0GreaterThan(32, PASSED_IPV6_NS_MULTIPLE_OPTIONS);
+                .addCountAndPassIfR0LessThan(32, PASSED_IPV6_NS_NO_SLLA_OPTION);
 
-        v6Gen.addCountAndPass(Counter.PASSED_IPV6_ICMP);
+        // if the first option is not SLLA -> pass
+        // 0                   1                   2                   3
+        // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+        // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+        // |     Type      |    Length     |Link-Layer Addr  |
+        // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+        v6Gen.addLoad8(R0, ICMP6_NS_OPTION_TYPE_OFFSET)
+                .addCountAndPassIfR0NotEquals(ICMPV6_ND_OPTION_SLLA,
+                        PASSED_IPV6_NS_NO_SLLA_OPTION);
+
+        // Src IPv6 address check:
+        // if multicast address (FF::/8) or loopback address (00::/8) -> drop
+        v6Gen.addLoad8(R0, IPV6_SRC_ADDR_OFFSET)
+                .addCountAndDropIfR0IsOneOf(Set.of(0L, 0xffL), DROPPED_IPV6_NS_INVALID);
+
+        // if multicast MAC in SLLA option -> drop
+        v6Gen.addLoad8(R0, ICMP6_NS_OPTION_TYPE_OFFSET + 2)
+                .addCountAndDropIfR0AnyBitsSet(1, DROPPED_IPV6_NS_INVALID);
+        generateNonDadNaTransmitLocked(v6Gen);
+        v6Gen.addCountAndDrop(Counter.DROPPED_IPV6_NS_REPLIED_NON_DAD);
     }
 
     /**
@@ -1986,10 +2033,19 @@
         //     drop
         //   if ICMPv6 code is not 0:
         //     drop
-        //   if target IP is none of interface unicast IPv6 addresses (incl. anycast):
-        //     drop
-        //   if payload len > 32 (8 bytes ICMP6 header + 16 bytes target address + 8 bytes option):
+        //   if target IP is one of tentative IPv6 addresses:
         //     pass
+        //   if target IP is none of non-tentative IPv6 addresses (incl. anycast):
+        //     drop
+        //   if IPv6 src is unspecified (::):
+        //     pass
+        //   if payload len < 32 (8 bytes ICMP6 header + 16 bytes target address + 8 bytes option):
+        //     pass
+        //   if IPv6 src is multicast address (FF::/8) or loopback address (00::/8):
+        //     drop
+        //   if multicast MAC in SLLA option:
+        //     drop
+        //   transmit NA and drop
         // if it's ICMPv6 RS to any:
         //   drop
         // if it's ICMPv6 NA to anything in ff02::/120
@@ -2041,7 +2097,7 @@
         // Not ICMPv6 NS -> skip.
         gen.addLoad8(R0, ICMP6_TYPE_OFFSET); // warning: also used further below.
         final ApfV6Generator v6Gen = tryToConvertToApfV6Generator(gen);
-        if (v6Gen != null) {
+        if (v6Gen != null && mShouldHandleNdOffload) {
             final String skipNsPacketFilter = v6Gen.getUniqueLabel();
             v6Gen.addJumpIfR0NotEquals(ICMPV6_NEIGHBOR_SOLICITATION, skipNsPacketFilter);
             generateNsFilterLocked(v6Gen);
@@ -2202,17 +2258,18 @@
      */
     @GuardedBy("this")
     @VisibleForTesting
-    protected ApfV4GeneratorBase<?> emitPrologueLocked() throws IllegalInstructionException {
+    public ApfV4GeneratorBase<?> emitPrologueLocked() throws IllegalInstructionException {
         // This is guaranteed to succeed because of the check in maybeCreate.
         ApfV4GeneratorBase<?> gen;
-        if (SdkLevel.isAtLeastV()
-                && ApfV6Generator.supportsVersion(mApfCapabilities.apfVersionSupported)) {
-            gen = new ApfV6Generator(mApfCapabilities.maximumApfProgramSize);
+        if (shouldUseApfV6Generator()) {
+            gen = new ApfV6Generator(mApfVersionSupported, mApfRamSize,
+                    mInstallableProgramSizeClamp);
         } else {
-            gen = new ApfV4Generator(mApfCapabilities.apfVersionSupported);
+            gen = new ApfV4Generator(mApfVersionSupported, mApfRamSize,
+                    mInstallableProgramSizeClamp);
         }
 
-        if (hasDataAccess(mApfCapabilities)) {
+        if (hasDataAccess(mApfVersionSupported)) {
             if (gen instanceof ApfV4Generator) {
                 // Increment TOTAL_PACKETS.
                 // Only needed in APFv4.
@@ -2333,16 +2390,6 @@
         ArrayList<Ra> rasToFilter = new ArrayList<>();
         final byte[] program;
         int programMinLft = Integer.MAX_VALUE;
-        int maximumApfProgramSize = mApfCapabilities.maximumApfProgramSize;
-        if (hasDataAccess(mApfCapabilities)) {
-            // Reserve space for the counters.
-            maximumApfProgramSize -= Counter.totalSize();
-        }
-
-        // Prevent generating (and thus installing) larger programs
-        if (maximumApfProgramSize > mInstallableProgramSizeClamp) {
-            maximumApfProgramSize = mInstallableProgramSizeClamp;
-        }
 
         // Ensure the entire APF program uses the same time base.
         int timeSeconds = secondsSinceBoot();
@@ -2355,8 +2402,8 @@
             emitEpilogue(gen);
 
             // Can't fit the program even without any RA filters?
-            if (gen.programLengthOverEstimate() > maximumApfProgramSize) {
-                Log.e(TAG, "Program exceeds maximum size " + maximumApfProgramSize);
+            if (gen.programLengthOverEstimate() > mMaximumApfProgramSize) {
+                Log.e(TAG, "Program exceeds maximum size " + mMaximumApfProgramSize);
                 sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_OVER_SIZE_FAILURE);
                 return;
             }
@@ -2366,7 +2413,7 @@
                 if (ra.getRemainingFilterLft(timeSeconds) <= 0) continue;
                 ra.generateFilterLocked(gen, timeSeconds);
                 // Stop if we get too big.
-                if (gen.programLengthOverEstimate() > maximumApfProgramSize) {
+                if (gen.programLengthOverEstimate() > mMaximumApfProgramSize) {
                     if (VDBG) Log.d(TAG, "Past maximum program size, skipping RAs");
                     sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_OVER_SIZE_FAILURE);
                     break;
@@ -2509,18 +2556,11 @@
             InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback,
             NetworkQuirkMetrics networkQuirkMetrics) {
         if (context == null || config == null || ifParams == null) return null;
-        ApfCapabilities apfCapabilities =  config.apfCapabilities;
-        if (apfCapabilities == null) return null;
-        if (apfCapabilities.apfVersionSupported < 2) return null;
-        if (apfCapabilities.maximumApfProgramSize < 512) {
-            Log.e(TAG, "Unacceptably small APF limit: " + apfCapabilities.maximumApfProgramSize);
+        if (!ApfV4Generator.supportsVersion(config.apfVersionSupported)) {
             return null;
         }
-        // For now only support generating programs for Ethernet frames. If this restriction is
-        // lifted the program generator will need its offsets adjusted.
-        if (apfCapabilities.apfPacketFormat != ARPHRD_ETHER) return null;
-        if (!ApfV4Generator.supportsVersion(apfCapabilities.apfVersionSupported)) {
-            Log.e(TAG, "Unsupported APF version: " + apfCapabilities.apfVersionSupported);
+        if (config.apfRamSize < 512) {
+            Log.e(TAG, "Unacceptably small APF limit: " + config.apfRamSize);
             return null;
         }
 
@@ -2529,7 +2569,7 @@
 
     private synchronized void collectAndSendMetrics() {
         if (mIpClientRaInfoMetrics == null || mApfSessionInfoMetrics == null) return;
-        final long sessionDurationMs = mClock.elapsedRealtime() - mSessionStartMs;
+        final long sessionDurationMs = mDependencies.elapsedRealtime() - mSessionStartMs;
         if (sessionDurationMs < mMinMetricsSessionDurationMs) return;
 
         // Collect and send IpClientRaInfoMetrics.
@@ -2543,8 +2583,8 @@
         mIpClientRaInfoMetrics.statsWrite();
 
         // Collect and send ApfSessionInfoMetrics.
-        mApfSessionInfoMetrics.setVersion(mApfCapabilities.apfVersionSupported);
-        mApfSessionInfoMetrics.setMemorySize(mApfCapabilities.maximumApfProgramSize);
+        mApfSessionInfoMetrics.setVersion(mApfVersionSupported);
+        mApfSessionInfoMetrics.setMemorySize(mApfRamSize);
         mApfSessionInfoMetrics.setApfSessionDurationSeconds(
                 (int) (sessionDurationMs / DateUtils.SECOND_IN_MILLIS));
         mApfSessionInfoMetrics.setNumOfTimesApfProgramUpdated(mNumProgramUpdates);
@@ -2658,6 +2698,15 @@
         installNewProgramLocked();
     }
 
+    @Override
+    public boolean supportNdOffload() {
+        return shouldUseApfV6Generator() && mShouldHandleNdOffload;
+    }
+
+    private boolean shouldUseApfV6Generator() {
+        return SdkLevel.isAtLeastV() && ApfV6Generator.supportsVersion(mApfVersionSupported);
+    }
+
     /**
      * Add TCP keepalive ack packet filter.
      * This will add a filter to drop acks to the keepalive packet passed as an argument.
@@ -2713,17 +2762,48 @@
     }
 
     public synchronized void dump(IndentingPrintWriter pw) {
-        pw.println("Capabilities: " + mApfCapabilities);
+        pw.println(String.format(
+                "Capabilities: { apfVersionSupported: %d, maximumApfProgramSize: %d }",
+                mApfVersionSupported, mApfRamSize));
         pw.println("InstallableProgramSizeClamp: " + mInstallableProgramSizeClamp);
         pw.println("Filter update status: " + (mIsRunning ? "RUNNING" : "PAUSED"));
         pw.println("Receive thread: " + (mReceiveThread != null ? "RUNNING" : "STOPPED"));
         pw.println("Multicast: " + (mMulticastFilter ? "DROP" : "ALLOW"));
         pw.println("Minimum RDNSS lifetime: " + mMinRdnssLifetimeSec);
+        pw.println("Interface MAC address: " + MacAddress.fromBytes(mHardwareAddress));
+        pw.println("Multicast MAC addresses: ");
+        pw.increaseIndent();
+        for (byte[] addr : mDependencies.getEtherMulticastAddresses(mInterfaceParams.name)) {
+            pw.println(MacAddress.fromBytes(addr));
+        }
+        pw.decreaseIndent();
         try {
             pw.println("IPv4 address: " + InetAddress.getByAddress(mIPv4Address).getHostAddress());
-            pw.println("IPv6 addresses: ");
+            pw.println("IPv6 non-tentative addresses: ");
             pw.increaseIndent();
-            for (Inet6Address addr: mIPv6NonTentativeAddresses) {
+            for (Inet6Address addr : mIPv6NonTentativeAddresses) {
+                pw.println(addr.getHostAddress());
+            }
+            pw.decreaseIndent();
+            pw.println("IPv6 tentative addresses: ");
+            pw.increaseIndent();
+            for (Inet6Address addr : mIPv6TentativeAddresses) {
+                pw.println(addr.getHostAddress());
+            }
+            pw.decreaseIndent();
+            pw.println("IPv6 anycast addresses:");
+            pw.increaseIndent();
+            final List<Inet6Address> anycastAddrs =
+                    ProcfsParsingUtils.getAnycast6Addresses(mInterfaceParams.name);
+            for (Inet6Address addr : anycastAddrs) {
+                pw.println(addr.getHostAddress());
+            }
+            pw.decreaseIndent();
+            pw.println("IPv6 multicast addresses:");
+            pw.increaseIndent();
+            final List<Inet6Address> multicastAddrs =
+                    ProcfsParsingUtils.getIpv6MulticastAddresses(mInterfaceParams.name);
+            for (Inet6Address addr : multicastAddrs) {
                 pw.println(addr.getHostAddress());
             }
             pw.decreaseIndent();
@@ -2796,7 +2876,7 @@
 
         pw.println("APF packet counters: ");
         pw.increaseIndent();
-        if (!hasDataAccess(mApfCapabilities)) {
+        if (!hasDataAccess(mApfVersionSupported)) {
             pw.println("APF counters not supported");
         } else if (mDataSnapshot == null) {
             pw.println("No last snapshot.");
@@ -2810,10 +2890,17 @@
                         pw.println(c.toString() + ": " + value);
                     }
 
-                    // If the counter's value decreases, it may have been cleaned up or there may be
-                    // a bug.
-                    if (value < mApfCounterTracker.getCounters().getOrDefault(c, 0L)) {
-                        Log.e(TAG, "Error: Counter value unexpectedly decreased.");
+                    final Set<Counter> skipCheckCounters = Set.of(FILTER_AGE_SECONDS,
+                            FILTER_AGE_16384THS);
+                    if (!skipCheckCounters.contains(c)) {
+                        // If the counter's value decreases, it may have been cleaned up or there
+                        // may be a bug.
+                        long oldValue = mApfCounterTracker.getCounters().getOrDefault(c, 0L);
+                        if (value < oldValue) {
+                            Log.e(TAG, String.format(
+                                    "Apf Counter: %s unexpectedly decreased. oldValue: %d. "
+                                            + "newValue: %d", c.toString(), oldValue, value));
+                        }
                     }
                 }
             } catch (ArrayIndexOutOfBoundsException e) {
diff --git a/src/android/net/apf/ApfV4Generator.java b/src/android/net/apf/ApfV4Generator.java
index a41f033..f9918b2 100644
--- a/src/android/net/apf/ApfV4Generator.java
+++ b/src/android/net/apf/ApfV4Generator.java
@@ -61,10 +61,10 @@
      * the requested version is unsupported.
      */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
-    public ApfV4Generator(int version, boolean disableCounterRangeCheck)
+    public ApfV4Generator(int version, int ramSize, int clampSize, boolean disableCounterRangeCheck)
             throws IllegalInstructionException {
         // make sure mVersion is not greater than 4 when using this class
-        super(version > 4 ? 4 : version, disableCounterRangeCheck);
+        super(version > 4 ? 4 : version, ramSize, clampSize, disableCounterRangeCheck);
         mCountAndDropLabel = version > 2 ? COUNT_AND_DROP_LABEL : DROP_LABEL;
         mCountAndPassLabel = version > 2 ? COUNT_AND_PASS_LABEL : PASS_LABEL;
     }
@@ -74,8 +74,9 @@
      * {@code version} of the APF interpreter. Throws {@code IllegalInstructionException} if
      * the requested version is unsupported.
      */
-    public ApfV4Generator(int version) throws IllegalInstructionException {
-        this(version, false);
+    public ApfV4Generator(int version, int ramSize, int clampSize)
+            throws IllegalInstructionException {
+        this(version, ramSize, clampSize, false);
     }
 
     @Override
@@ -202,20 +203,6 @@
     }
 
     @Override
-    public ApfV4Generator addCountAndDropIfBytesAtR0Equal(byte[] bytes,
-            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
-        final String tgt = getUniqueLabel();
-        return addJumpIfBytesAtR0NotEqual(bytes, tgt).addCountAndDrop(cnt).defineLabel(tgt);
-    }
-
-    @Override
-    public ApfV4Generator addCountAndPassIfBytesAtR0Equal(byte[] bytes,
-            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
-        final String tgt = getUniqueLabel();
-        return addJumpIfBytesAtR0NotEqual(bytes, tgt).addCountAndPass(cnt).defineLabel(tgt);
-    }
-
-    @Override
     public ApfV4Generator addCountAndPassIfR0IsOneOf(@NonNull Set<Long> values,
             ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
         if (values.isEmpty()) {
diff --git a/src/android/net/apf/ApfV4GeneratorBase.java b/src/android/net/apf/ApfV4GeneratorBase.java
index ced1d68..a00aa2f 100644
--- a/src/android/net/apf/ApfV4GeneratorBase.java
+++ b/src/android/net/apf/ApfV4GeneratorBase.java
@@ -52,9 +52,9 @@
      * the requested version is unsupported.
      */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
-    public ApfV4GeneratorBase(int version, boolean disableCounterRangeCheck)
-            throws IllegalInstructionException {
-        super(version, disableCounterRangeCheck);
+    public ApfV4GeneratorBase(int version, int ramSize, int clampSize,
+            boolean disableCounterRangeCheck) throws IllegalInstructionException {
+        super(version, ramSize, clampSize, disableCounterRangeCheck);
         requireApfVersion(APF_VERSION_2);
     }
 
@@ -504,16 +504,23 @@
      * bytes of the packet at an offset specified by register0 match {@code bytes}.
      * WARNING: may modify R1
      */
-    public abstract Type addCountAndDropIfBytesAtR0Equal(byte[] bytes,
-            ApfCounterTracker.Counter cnt) throws IllegalInstructionException;
+    public final Type addCountAndDropIfBytesAtR0Equal(byte[] bytes,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        final String tgt = getUniqueLabel();
+        return addJumpIfBytesAtR0NotEqual(bytes, tgt).addCountAndDrop(cnt).defineLabel(tgt);
+    }
+
 
     /**
      * Add instructions to the end of the program to increase counter and pass packet if the
      * bytes of the packet at an offset specified by register0 match {@code bytes}.
      * WARNING: may modify R1
      */
-    public abstract Type addCountAndPassIfBytesAtR0Equal(byte[] bytes,
-            ApfCounterTracker.Counter cnt) throws IllegalInstructionException;
+    public final Type addCountAndPassIfBytesAtR0Equal(byte[] bytes,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        final String tgt = getUniqueLabel();
+        return addJumpIfBytesAtR0NotEqual(bytes, tgt).addCountAndPass(cnt).defineLabel(tgt);
+    }
 
     /**
      * Add instructions to the end of the program to increase counter and pass packet if the
diff --git a/src/android/net/apf/ApfV6Generator.java b/src/android/net/apf/ApfV6Generator.java
index d425907..f943bed 100644
--- a/src/android/net/apf/ApfV6Generator.java
+++ b/src/android/net/apf/ApfV6Generator.java
@@ -35,14 +35,15 @@
     /**
      * Creates an ApfV6Generator instance which emits instructions for APFv6.
      */
-    public ApfV6Generator(int maximumApfProgramSize) throws IllegalInstructionException {
-        this(new byte[0], maximumApfProgramSize);
+    public ApfV6Generator(int version, int ramSize, int clampSize)
+            throws IllegalInstructionException {
+        this(new byte[0], version, ramSize, clampSize);
     }
 
     @Override
     void updateExceptionBufferSize(int programSize) throws IllegalInstructionException {
         mInstructions.get(1).updateExceptionBufferSize(
-                mMaximumApfProgramSize - ApfCounterTracker.Counter.totalSize() - programSize);
+                mRamSize - ApfCounterTracker.Counter.totalSize() - programSize);
     }
 
     /**
@@ -50,9 +51,9 @@
      * Initializes the data region with {@code bytes}.
      */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
-    public ApfV6Generator(byte[] bytes, int maximumApfProgramSize)
+    public ApfV6Generator(byte[] bytes, int version, int ramSize, int clampSize)
             throws IllegalInstructionException {
-        super(maximumApfProgramSize);
+        super(version, ramSize, clampSize);
         Objects.requireNonNull(bytes);
         addData(bytes);
         addExceptionBuffer(0);
diff --git a/src/android/net/apf/ApfV6GeneratorBase.java b/src/android/net/apf/ApfV6GeneratorBase.java
index a9abed6..17629d1 100644
--- a/src/android/net/apf/ApfV6GeneratorBase.java
+++ b/src/android/net/apf/ApfV6GeneratorBase.java
@@ -40,17 +40,15 @@
 public abstract class ApfV6GeneratorBase<Type extends ApfV6GeneratorBase<Type>> extends
         ApfV4GeneratorBase<Type> {
 
-    final int mMaximumApfProgramSize;
-
     /**
      * Creates an ApfV6GeneratorBase instance which is able to emit instructions for the specified
      * {@code version} of the APF interpreter. Throws {@code IllegalInstructionException} if
      * the requested version is unsupported.
      *
      */
-    public ApfV6GeneratorBase(int maximumApfProgramSize) throws IllegalInstructionException {
-        super(APF_VERSION_6, false);
-        this.mMaximumApfProgramSize = maximumApfProgramSize;
+    public ApfV6GeneratorBase(int version, int ramSize, int clampSize)
+            throws IllegalInstructionException {
+        super(version, ramSize, clampSize, false);
     }
 
     /**
@@ -712,20 +710,6 @@
     }
 
     @Override
-    public final Type addCountAndDropIfBytesAtR0Equal(byte[] bytes,
-            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
-        final String tgt = getUniqueLabel();
-        return addJumpIfBytesAtR0NotEqual(bytes, tgt).addCountAndDrop(cnt).defineLabel(tgt);
-    }
-
-    @Override
-    public final Type addCountAndPassIfBytesAtR0Equal(byte[] bytes,
-            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
-        final String tgt = getUniqueLabel();
-        return addJumpIfBytesAtR0NotEqual(bytes, tgt).addCountAndPass(cnt).defineLabel(tgt);
-    }
-
-    @Override
     public Type addCountAndPassIfR0IsOneOf(@NonNull Set<Long> values,
             ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
         if (values.isEmpty()) {
diff --git a/src/android/net/apf/BaseApfGenerator.java b/src/android/net/apf/BaseApfGenerator.java
index 5552165..2eab5ab 100644
--- a/src/android/net/apf/BaseApfGenerator.java
+++ b/src/android/net/apf/BaseApfGenerator.java
@@ -22,6 +22,7 @@
 
 import android.annotation.NonNull;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.ByteUtils;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.HexDump;
@@ -39,9 +40,12 @@
  */
 public abstract class BaseApfGenerator {
 
-    public BaseApfGenerator(int mVersion, boolean mDisableCounterRangeCheck) {
-        this.mVersion = mVersion;
-        this.mDisableCounterRangeCheck = mDisableCounterRangeCheck;
+    public BaseApfGenerator(int version, int ramSize, int clampSize,
+            boolean disableCounterRangeCheck) {
+        mVersion = version;
+        mRamSize = ramSize;
+        mClampSize = clampSize;
+        mDisableCounterRangeCheck = disableCounterRangeCheck;
     }
 
     /**
@@ -851,7 +855,8 @@
     /**
      * Return a unique label string.
      */
-    protected String getUniqueLabel() {
+    @VisibleForTesting
+    public String getUniqueLabel() {
         return "LABEL_" + mLabelCount++;
     }
 
@@ -954,6 +959,8 @@
     private final Instruction mDropLabel = new Instruction(Opcodes.LABEL);
     private final Instruction mPassLabel = new Instruction(Opcodes.LABEL);
     public final int mVersion;
+    public final int mRamSize;
+    public final int mClampSize;
     public boolean mGenerated;
     private final boolean mDisableCounterRangeCheck;
 }
diff --git a/src/android/net/apf/LegacyApfFilter.java b/src/android/net/apf/LegacyApfFilter.java
index e4f709b..d50d5cb 100644
--- a/src/android/net/apf/LegacyApfFilter.java
+++ b/src/android/net/apf/LegacyApfFilter.java
@@ -21,7 +21,6 @@
 import static android.net.apf.BaseApfGenerator.Register.R1;
 import static android.net.util.SocketUtils.makePacketSocketAddress;
 import static android.system.OsConstants.AF_PACKET;
-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;
@@ -126,7 +125,7 @@
      * When APFv4 is supported, loads R1 with the offset of the specified counter.
      */
     private void maybeSetupCounter(ApfV4Generator gen, Counter c) {
-        if (hasDataAccess(mApfCapabilities)) {
+        if (hasDataAccess(mApfVersionSupported)) {
             gen.addLoadImmediate(R1, c.offset());
         }
     }
@@ -214,7 +213,7 @@
                         .setZeroLifetimeRas(mZeroLifetimeRas)
                         .setProgramUpdates(mProgramUpdates)
                         .setDurationMs(nowMs - mStart)
-                        .setMaxProgramSize(mApfCapabilities.maximumApfProgramSize)
+                        .setMaxProgramSize(mMaximumApfProgramSize)
                         .setProgramUpdatesAll(mNumProgramUpdates)
                         .setProgramUpdatesAllowingMulticast(mNumProgramUpdatesAllowingMulticast)
                         .build();
@@ -306,7 +305,8 @@
             ETH_HEADER_LEN + UDP_HEADER_LEN + DNS_HEADER_LEN;
 
 
-    private final ApfCapabilities mApfCapabilities;
+    public final int mApfVersionSupported;
+    public final int mMaximumApfProgramSize;
     private final IpClientCallbacksWrapper mIpClientCallback;
     private final InterfaceParams mInterfaceParams;
     private final IpConnectivityLog mMetricsLog;
@@ -393,7 +393,8 @@
             InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback,
             IpConnectivityLog log, NetworkQuirkMetrics networkQuirkMetrics,
             ApfFilter.Dependencies dependencies, ApfFilter.Clock clock) {
-        mApfCapabilities = config.apfCapabilities;
+        mApfVersionSupported = config.apfVersionSupported;
+        mMaximumApfProgramSize = config.apfRamSize;
         mIpClientCallback = ipClientCallback;
         mInterfaceParams = ifParams;
         mMulticastFilter = config.multicastFilter;
@@ -408,7 +409,7 @@
         mSessionStartMs = mClock.elapsedRealtime();
         mMinMetricsSessionDurationMs = config.minMetricsSessionDurationMs;
 
-        if (hasDataAccess(mApfCapabilities)) {
+        if (hasDataAccess(mApfVersionSupported)) {
             mCountAndPassLabel = "countAndPass";
             mCountAndDropLabel = "countAndDrop";
         } else {
@@ -494,8 +495,8 @@
                 // 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 && hasDataAccess(mApfCapabilities)) {
-                    byte[] zeroes = new byte[mApfCapabilities.maximumApfProgramSize];
+                if (mIsRunning && hasDataAccess(mApfVersionSupported)) {
+                    byte[] zeroes = new byte[mMaximumApfProgramSize];
                     if (!mIpClientCallback.installPacketFilter(zeroes)) {
                         sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
                     }
@@ -1728,9 +1729,10 @@
     @GuardedBy("this")
     protected ApfV4Generator emitPrologueLocked() throws IllegalInstructionException {
         // This is guaranteed to succeed because of the check in maybeCreate.
-        ApfV4Generator gen = new ApfV4Generator(mApfCapabilities.apfVersionSupported);
+        ApfV4Generator gen = new ApfV4Generator(mApfVersionSupported, mMaximumApfProgramSize,
+                mMaximumApfProgramSize);
 
-        if (hasDataAccess(mApfCapabilities)) {
+        if (hasDataAccess(mApfVersionSupported)) {
             // Increment TOTAL_PACKETS
             maybeSetupCounter(gen, Counter.TOTAL_PACKETS);
             gen.addLoadData(R0, 0);  // load counter
@@ -1833,7 +1835,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 (!hasDataAccess(mApfCapabilities)) return;
+        if (!hasDataAccess(mApfVersionSupported)) return;
 
         // Execution will reach the bottom of the program if none of the filters match,
         // which will pass the packet to the application processor.
@@ -1868,8 +1870,8 @@
         ArrayList<Ra> rasToFilter = new ArrayList<>();
         final byte[] program;
         long programMinLifetime = Long.MAX_VALUE;
-        long maximumApfProgramSize = mApfCapabilities.maximumApfProgramSize;
-        if (hasDataAccess(mApfCapabilities)) {
+        long maximumApfProgramSize = mMaximumApfProgramSize;
+        if (hasDataAccess(mApfVersionSupported)) {
             // Reserve space for the counters.
             maximumApfProgramSize -= Counter.totalSize();
         }
@@ -2071,20 +2073,11 @@
             InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback,
             NetworkQuirkMetrics networkQuirkMetrics) {
         if (context == null || config == null || ifParams == null) return null;
-        ApfCapabilities apfCapabilities =  config.apfCapabilities;
-        if (apfCapabilities == null) return null;
-        if (apfCapabilities.apfVersionSupported == 0) return null;
-        if (apfCapabilities.maximumApfProgramSize < 512) {
-            Log.e(TAG, "Unacceptably small APF limit: " + apfCapabilities.maximumApfProgramSize);
+        if (!ApfV4Generator.supportsVersion(config.apfVersionSupported)) {
             return null;
         }
-        // For now only support generating programs for Ethernet frames. If this restriction is
-        // lifted:
-        //   1. the program generator will need its offsets adjusted.
-        //   2. the packet filter attached to our packet socket will need its offset adjusted.
-        if (apfCapabilities.apfPacketFormat != ARPHRD_ETHER) return null;
-        if (!ApfV4Generator.supportsVersion(apfCapabilities.apfVersionSupported)) {
-            Log.e(TAG, "Unsupported APF version: " + apfCapabilities.apfVersionSupported);
+        if (config.apfRamSize < 512) {
+            Log.e(TAG, "Unacceptably small APF limit: " + config.apfRamSize);
             return null;
         }
 
@@ -2108,8 +2101,8 @@
         mIpClientRaInfoMetrics.statsWrite();
 
         // Collect and send ApfSessionInfoMetrics.
-        mApfSessionInfoMetrics.setVersion(mApfCapabilities.apfVersionSupported);
-        mApfSessionInfoMetrics.setMemorySize(mApfCapabilities.maximumApfProgramSize);
+        mApfSessionInfoMetrics.setVersion(mApfVersionSupported);
+        mApfSessionInfoMetrics.setMemorySize(mMaximumApfProgramSize);
         mApfSessionInfoMetrics.setApfSessionDurationSeconds(
                 (int) (sessionDurationMs / DateUtils.SECOND_IN_MILLIS));
         mApfSessionInfoMetrics.setNumOfTimesApfProgramUpdated(mNumProgramUpdates);
@@ -2249,7 +2242,9 @@
     }
 
     public synchronized void dump(IndentingPrintWriter pw) {
-        pw.println("Capabilities: " + mApfCapabilities);
+        pw.println(String.format(
+                "Capabilities: { apfVersionSupported: %d, maximumApfProgramSize: %d }",
+                mApfVersionSupported, mMaximumApfProgramSize));
         pw.println("Filter update status: " + (mIsRunning ? "RUNNING" : "PAUSED"));
         pw.println("Receive thread: " + (mReceiveThread != null ? "RUNNING" : "STOPPED"));
         pw.println("Multicast: " + (mMulticastFilter ? "DROP" : "ALLOW"));
@@ -2325,7 +2320,7 @@
 
         pw.println("APF packet counters: ");
         pw.increaseIndent();
-        if (!hasDataAccess(mApfCapabilities)) {
+        if (!hasDataAccess(mApfVersionSupported)) {
             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 deaabac..7e21116 100644
--- a/src/android/net/ip/IpClient.java
+++ b/src/android/net/ip/IpClient.java
@@ -30,7 +30,9 @@
 import static android.net.ip.IpReachabilityMonitor.nudEventTypeToInt;
 import static android.net.util.SocketUtils.makePacketSocketAddress;
 import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
+import static android.stats.connectivity.NetworkQuirkEvent.QE_DHCP6_HEURISTIC_TRIGGERED;
 import static android.system.OsConstants.AF_PACKET;
+import static android.system.OsConstants.ARPHRD_ETHER;
 import static android.system.OsConstants.ETH_P_ARP;
 import static android.system.OsConstants.ETH_P_IPV6;
 import static android.system.OsConstants.IFA_F_NODAD;
@@ -46,8 +48,8 @@
 import static com.android.net.module.util.NetworkStackConstants.VENDOR_SPECIFIC_IE_ID;
 import static com.android.networkstack.apishim.ConstantsShim.IFA_F_MANAGETEMPADDR;
 import static com.android.networkstack.apishim.ConstantsShim.IFA_F_NOPREFIXROUTE;
-import static com.android.networkstack.util.NetworkStackUtils.APF_HANDLE_ARP_OFFLOAD_FORCE_DISABLE;
-import static com.android.networkstack.util.NetworkStackUtils.APF_HANDLE_LIGHT_DOZE_FORCE_DISABLE;
+import static com.android.networkstack.util.NetworkStackUtils.APF_HANDLE_ARP_OFFLOAD;
+import static com.android.networkstack.util.NetworkStackUtils.APF_HANDLE_ND_OFFLOAD;
 import static com.android.networkstack.util.NetworkStackUtils.APF_NEW_RA_FILTER_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.APF_POLLING_COUNTERS_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION;
@@ -597,7 +599,7 @@
     static final String CONFIG_APF_COUNTER_POLLING_INTERVAL_SECS =
             "ipclient_apf_counter_polling_interval_secs";
     @VisibleForTesting
-    static final int DEFAULT_APF_COUNTER_POLLING_INTERVAL_SECS = 300;
+    static final int DEFAULT_APF_COUNTER_POLLING_INTERVAL_SECS = 1800;
 
     // Used to wait for the provisioning to complete eventually and then decide the target
     // network type, which gives the accurate hint to set DTIM multiplier. Per current IPv6
@@ -731,10 +733,10 @@
     private final boolean mDhcp6PrefixDelegationEnabled;
     private final boolean mUseNewApfFilter;
     private final boolean mEnableIpClientIgnoreLowRaLifetime;
-    private final boolean mApfShouldHandleLightDoze;
     private final boolean mEnableApfPollingCounters;
     private final boolean mPopulateLinkAddressLifetime;
     private final boolean mApfShouldHandleArpOffload;
+    private final boolean mApfShouldHandleNdOffload;
 
     private InterfaceParams mInterfaceParams;
 
@@ -981,11 +983,10 @@
         mEnableIpClientIgnoreLowRaLifetime =
                 SdkLevel.isAtLeastV() || mDependencies.isFeatureEnabled(context,
                         IPCLIENT_IGNORE_LOW_RA_LIFETIME_VERSION);
-        // Light doze mode status checking API is only available at T or later releases.
-        mApfShouldHandleLightDoze = SdkLevel.isAtLeastT() && mDependencies.isFeatureNotChickenedOut(
-                mContext, APF_HANDLE_LIGHT_DOZE_FORCE_DISABLE);
         mApfShouldHandleArpOffload = mDependencies.isFeatureNotChickenedOut(
-                mContext, APF_HANDLE_ARP_OFFLOAD_FORCE_DISABLE);
+                mContext, APF_HANDLE_ARP_OFFLOAD);
+        mApfShouldHandleNdOffload = mDependencies.isFeatureNotChickenedOut(
+                mContext, APF_HANDLE_ND_OFFLOAD);
         mPopulateLinkAddressLifetime = mDependencies.isFeatureEnabled(context,
                 IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION);
 
@@ -1022,10 +1023,15 @@
                     public void onClatInterfaceStateUpdate(boolean add) {
                         getHandler().post(() -> {
                             if (mHasSeenClatInterface == add) return;
-                            // Clat interface information is spliced into LinkProperties by
-                            // ConnectivityService, so it cannot be added to the LinkProperties
-                            // here as those propagate back to ConnectivityService.
-                            mCallback.setNeighborDiscoveryOffload(add ? false : true);
+                            // If Apf is not supported or Apf doesn't support ND offload, then
+                            // configure the vendor ND offload feature based on the Clat
+                            // interface state.
+                            if (mApfFilter == null || !mApfFilter.supportNdOffload()) {
+                                // Clat interface information is spliced into LinkProperties by
+                                // ConnectivityService, so it cannot be added to the LinkProperties
+                                // here as those propagate back to ConnectivityService.
+                                mCallback.setNeighborDiscoveryOffload(add ? false : true);
+                            }
                             mHasSeenClatInterface = add;
                             if (mApfFilter != null) {
                                 mApfFilter.updateClatInterfaceState(add);
@@ -1390,7 +1396,8 @@
         pw.println(mTag + " APF dump:");
         pw.increaseIndent();
         if (apfFilter != null) {
-            if (apfCapabilities != null && apfFilter.hasDataAccess(apfCapabilities)) {
+            if (apfCapabilities != null && apfFilter.hasDataAccess(
+                    apfCapabilities.apfVersionSupported)) {
                 // Request a new snapshot, then wait for it.
                 mApfDataSnapshotComplete.close();
                 mCallback.startReadPacketFilter("dumpsys");
@@ -2160,17 +2167,20 @@
     // Returns false if we have lost provisioning, true otherwise.
     private boolean handleLinkPropertiesUpdate(boolean sendCallbacks) {
         final LinkProperties newLp = assembleLinkProperties();
-        // LinkProperties.equals just compares if the interface addresses are identical,
-        // it doesn't compare the LinkAddress objects, so it considers two LinkProperties
-        // objects are identical even with different address lifetime. However, we may want
-        // to notify the caller whenever the link address lifetime is updated, especially
-        // after we enable populating the deprecationTime/expirationTime fields. The caller
-        // can get the latest address lifetime from the onLinkPropertiesChange callback.
+
+        // We need to call mApfFilter.setLinkProperties(newLp) every time there is a LinkAddress
+        // change because ApfFilter needs to know when addresses change from tentative to
+        // non-tentative. setLinkProperties() inside IpClient won't be called if the
+        // LinkProperties.equal() check returns true. The LinkProperties.equal() check does not
+        // currently take into account the LinkAddress flag change.
+        // It is OK to call mApfFilter.setLinkProperties() multiple times because if IP
+        // addresses are not updated, ApfFilter won't generate new program.
+        if (mApfFilter != null) {
+            mApfFilter.setLinkProperties(newLp);
+        }
+
         if (Objects.equals(newLp, mLinkProperties)) {
-            if (!mPopulateLinkAddressLifetime) return true;
-            if (LinkPropertiesUtils.isIdenticalAllLinkAddresses(newLp, mLinkProperties)) {
-                return true;
-            }
+            return true;
         }
 
         // Set an alarm to wait for IPv6 autoconf via SLAAC to succeed after receiving an RA,
@@ -2556,17 +2566,24 @@
     @Nullable
     private AndroidPacketFilter maybeCreateApfFilter(final ApfCapabilities apfCaps) {
         ApfFilter.ApfConfiguration apfConfig = new ApfFilter.ApfConfiguration();
-        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 (apfCaps == null) {
+            return null;
         }
-        if (apfConfig.apfCapabilities != null && !SdkLevel.isAtLeastV()
-                && apfConfig.apfCapabilities.apfVersionSupported <= 4) {
+        // For now only support generating programs for Ethernet frames. If this restriction is
+        // lifted the program generator will need its offsets adjusted.
+        if (apfCaps.apfPacketFormat != ARPHRD_ETHER) return null;
+        if (SdkLevel.isAtLeastS()) {
+            apfConfig.apfVersionSupported = apfCaps.apfVersionSupported;
+        } else {
+            // In Android R, ApfCapabilities#hasDataAccess() can be modified by OEMs. The
+            // ApfFilter logic uses ApfCapabilities.apfVersionSupported to determine whether
+            // data region access is supported. Therefore, we need to recalculate
+            // ApfCapabilities.apfVersionSupported based on the return value of
+            // ApfCapabilities#hasDataAccess().
+            apfConfig.apfVersionSupported = apfCaps.hasDataAccess() ? 3 : 2;
+        }
+        apfConfig.apfRamSize = apfCaps.maximumApfProgramSize;
+        if (!SdkLevel.isAtLeastV() && apfConfig.apfVersionSupported <= 4) {
             apfConfig.installableProgramSizeClamp = 1024;
         }
         apfConfig.multicastFilter = mMulticastFiltering;
@@ -2593,8 +2610,8 @@
         } else {
             apfConfig.acceptRaMinLft = 0;
         }
-        apfConfig.shouldHandleLightDoze = mApfShouldHandleLightDoze;
         apfConfig.shouldHandleArpOffload = mApfShouldHandleArpOffload;
+        apfConfig.shouldHandleNdOffload = mApfShouldHandleNdOffload;
         apfConfig.minMetricsSessionDurationMs = mApfCounterPollingIntervalMs;
         apfConfig.hasClatInterface = mHasSeenClatInterface;
         return mDependencies.maybeCreateApfFilter(mContext, apfConfig, mInterfaceParams,
@@ -3092,6 +3109,10 @@
             // at the beginning.
             mHasSeenClatInterface = false;
             mApfFilter = maybeCreateApfFilter(mCurrentApfCapabilities);
+            // If Apf supports ND offload, then turn off the vendor ND offload feature.
+            if (mApfFilter != null && mApfFilter.supportNdOffload()) {
+                mCallback.setNeighborDiscoveryOffload(false);
+            }
             // TODO: investigate the effects of any multicast filtering racing/interfering with the
             // rest of this IP configuration startup.
             if (mApfFilter == null) {
@@ -3413,6 +3434,8 @@
                     if (!hasIpv6Address(mLinkProperties)
                             && mLinkProperties.hasIpv6DefaultRoute()) {
                         Log.d(TAG, "Network supports IPv6 but not autoconf, starting DHCPv6 PD");
+                        mNetworkQuirkMetrics.setEvent(QE_DHCP6_HEURISTIC_TRIGGERED);
+                        mNetworkQuirkMetrics.statsWrite();
                         startDhcp6PrefixDelegation();
                     }
                     break;
@@ -3547,6 +3570,10 @@
                     final ApfCapabilities apfCapabilities = (ApfCapabilities) msg.obj;
                     if (handleUpdateApfCapabilities(apfCapabilities)) {
                         mApfFilter = maybeCreateApfFilter(apfCapabilities);
+                        // If Apf supports ND offload, then turn off the vendor ND offload feature.
+                        if (mApfFilter != null && mApfFilter.supportNdOffload()) {
+                            mCallback.setNeighborDiscoveryOffload(false);
+                        }
                     }
                     break;
 
diff --git a/src/android/net/util/RawSocketUtils.java b/src/android/net/util/RawSocketUtils.java
new file mode 100644
index 0000000..a6c8a40
--- /dev/null
+++ b/src/android/net/util/RawSocketUtils.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.util;
+
+import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.system.OsConstants.AF_PACKET;
+import static android.system.OsConstants.SOCK_NONBLOCK;
+import static android.system.OsConstants.SOCK_RAW;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_DST_ADDR_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_LENGTH;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_OFFSET;
+
+import android.annotation.RequiresPermission;
+import android.content.Context;
+import android.net.TetheringManager;
+import android.system.Os;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.util.HexDump;
+
+import java.io.FileDescriptor;
+import java.net.NetworkInterface;
+import java.net.SocketAddress;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class RawSocketUtils {
+    // For isTetheredInterface, a quick Tethering event callback is expected
+    // since there's no start/stop Tethering involved. This timeout allows
+    // system messages to be handled, preventing flaky test results.
+    private static final int TETHERING_EVENT_CALLBACK_TIMEOUT_MS = 3000;
+
+    /**
+     * Send a raw packet represents in Hex format to the downstream interface.
+     * <p>
+     * Note that the target interface is limited to tethering downstream
+     * for security considerations.
+     */
+    @RequiresPermission(NETWORK_SETTINGS)
+    public static void sendRawPacketDownStream(@NonNull Context context, @NonNull String ifaceName,
+                                     @NonNull String packetInHex) throws Exception {
+        // 1. Verify Tethering Downstream Interface.
+        enforceTetheredInterface(context, ifaceName);
+
+        // 2. Hex to Byte Array Conversion
+        final byte[] packetData = HexDump.hexStringToByteArray(packetInHex);
+        final byte[] destMac = Arrays.copyOfRange(packetData, ETHER_DST_ADDR_OFFSET,
+                ETHER_DST_ADDR_OFFSET + ETHER_ADDR_LEN);
+        final byte[] etherTypeBytes = Arrays.copyOfRange(packetData, ETHER_TYPE_OFFSET,
+                ETHER_TYPE_OFFSET + ETHER_TYPE_LENGTH);
+        final int etherType = ((etherTypeBytes[0] & 0xFF) << 8) | (etherTypeBytes[1] & 0xFF);
+
+        // 3. Obtain Network Interface
+        final NetworkInterface iface = NetworkInterface.getByName(ifaceName);
+        if (iface == null) {
+            throw new IllegalArgumentException("Invalid network interface: " + ifaceName);
+        }
+
+        // 4. Construct and Send Packet.
+        final SocketAddress addr = SocketUtils.makePacketSocketAddress(
+                etherType,
+                iface.getIndex(),
+                destMac
+        );
+        final FileDescriptor sock = Os.socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, 0);
+        try {
+            Os.sendto(sock, packetData, 0, packetData.length, 0, addr);
+        } finally {
+            SocketUtils.closeSocket(sock);
+        }
+    }
+
+    @RequiresPermission(NETWORK_SETTINGS)
+    private static void enforceTetheredInterface(@NonNull Context context,
+                                               @NonNull String interfaceName)
+            throws ExecutionException, InterruptedException, TimeoutException {
+        final TetheringManager tm = context.getSystemService(TetheringManager.class);
+        final CompletableFuture<List<String>> tetheredInterfaces = new CompletableFuture<>();
+        final TetheringManager.TetheringEventCallback callback =
+                new TetheringManager.TetheringEventCallback() {
+                    @Override
+                    public void onTetheredInterfacesChanged(@NonNull List<String> interfaces) {
+                        tetheredInterfaces.complete(interfaces);
+                    }
+                };
+        tm.registerTetheringEventCallback(c -> c.run() /* executor */, callback);
+        final List<String> tetheredIfaces = tetheredInterfaces.get(
+                TETHERING_EVENT_CALLBACK_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        if (!tetheredIfaces.contains(interfaceName)) {
+            throw new SecurityException("Only tethered interfaces " + tetheredIfaces
+                    + " are expected, but got " + interfaceName);
+        }
+    }
+}
diff --git a/src/com/android/networkstack/metrics/ApfSessionInfoMetrics.java b/src/com/android/networkstack/metrics/ApfSessionInfoMetrics.java
index d1776c9..c2c51f6 100644
--- a/src/com/android/networkstack/metrics/ApfSessionInfoMetrics.java
+++ b/src/com/android/networkstack/metrics/ApfSessionInfoMetrics.java
@@ -16,11 +16,115 @@
 
 package com.android.networkstack.metrics;
 
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_802_3_FRAME;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_NON_IPV4;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_OTHER_HOST;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_REPLY_SPA_NO_HOST;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_REQUEST_ANYHOST;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_REQUEST_REPLIED;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_UNKNOWN;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_V6_ONLY;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_ETHERTYPE_NOT_ALLOWED;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_ETH_BROADCAST;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_GARP_REPLY;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_BROADCAST_ADDR;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_BROADCAST_NET;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_KEEPALIVE_ACK;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_L2_BROADCAST;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_MULTICAST;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_NATT_KEEPALIVE;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_NON_DHCP4;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_TCP_PORT7_UNICAST;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_KEEPALIVE_ACK;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MULTICAST;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MULTICAST_NA;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MULTICAST_PING;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NON_ICMP_MULTICAST;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_INVALID;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_OTHER_HOST;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_REPLIED_NON_DAD;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_ROUTER_SOLICITATION;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_MDNS;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_RA;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_ARP;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_ARP_BROADCAST_REPLY;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_ARP_NON_IPV4;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_ARP_REQUEST;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_ARP_UNICAST_REPLY;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_ARP_UNKNOWN;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_DHCP;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV4;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV4_FROM_DHCPV4_SERVER;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV4_UNICAST;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_ICMP;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NON_ICMP;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_DAD;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_NO_ADDRESS;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_NO_SLLA_OPTION;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_TENTATIVE;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_UNICAST_NON_ICMP;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_MDNS;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_MLD;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_NON_IP_UNICAST;
+import static android.net.apf.ApfCounterTracker.Counter.TOTAL_PACKETS;
+import static android.stats.connectivity.CounterName.CN_DROPPED_802_3_FRAME;
+import static android.stats.connectivity.CounterName.CN_DROPPED_ARP_NON_IPV4;
+import static android.stats.connectivity.CounterName.CN_DROPPED_ARP_OTHER_HOST;
+import static android.stats.connectivity.CounterName.CN_DROPPED_ARP_REPLY_SPA_NO_HOST;
+import static android.stats.connectivity.CounterName.CN_DROPPED_ARP_REQUEST_ANYHOST;
+import static android.stats.connectivity.CounterName.CN_DROPPED_ARP_REQUEST_REPLIED;
+import static android.stats.connectivity.CounterName.CN_DROPPED_ARP_UNKNOWN;
+import static android.stats.connectivity.CounterName.CN_DROPPED_ARP_V6_ONLY;
+import static android.stats.connectivity.CounterName.CN_DROPPED_ETHERTYPE_NOT_ALLOWED;
+import static android.stats.connectivity.CounterName.CN_DROPPED_ETH_BROADCAST;
+import static android.stats.connectivity.CounterName.CN_DROPPED_GARP_REPLY;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV4_BROADCAST_ADDR;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV4_BROADCAST_NET;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV4_KEEPALIVE_ACK;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV4_L2_BROADCAST;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV4_MULTICAST;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV4_NATT_KEEPALIVE;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV4_NON_DHCP4;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_KEEPALIVE_ACK;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_MULTICAST;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_MULTICAST_NA;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_MULTICAST_PING;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_NON_ICMP_MULTICAST;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_NS_INVALID;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_NS_OTHER_HOST;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_NS_REPLIED_NON_DAD;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_ROUTER_SOLICITATION;
+import static android.stats.connectivity.CounterName.CN_DROPPED_MDNS;
+import static android.stats.connectivity.CounterName.CN_DROPPED_RA;
+import static android.stats.connectivity.CounterName.CN_PASSED_ARP;
+import static android.stats.connectivity.CounterName.CN_PASSED_ARP_BROADCAST_REPLY;
+import static android.stats.connectivity.CounterName.CN_PASSED_ARP_REQUEST;
+import static android.stats.connectivity.CounterName.CN_PASSED_ARP_UNICAST_REPLY;
+import static android.stats.connectivity.CounterName.CN_PASSED_DHCP;
+import static android.stats.connectivity.CounterName.CN_PASSED_IPV4;
+import static android.stats.connectivity.CounterName.CN_PASSED_IPV4_FROM_DHCPV4_SERVER;
+import static android.stats.connectivity.CounterName.CN_PASSED_IPV4_UNICAST;
+import static android.stats.connectivity.CounterName.CN_PASSED_IPV6_ICMP;
+import static android.stats.connectivity.CounterName.CN_PASSED_IPV6_NON_ICMP;
+import static android.stats.connectivity.CounterName.CN_PASSED_IPV6_NS_DAD;
+import static android.stats.connectivity.CounterName.CN_PASSED_IPV6_NS_NO_ADDRESS;
+import static android.stats.connectivity.CounterName.CN_PASSED_IPV6_NS_NO_SLLA_OPTION;
+import static android.stats.connectivity.CounterName.CN_PASSED_IPV6_NS_TENTATIVE;
+import static android.stats.connectivity.CounterName.CN_PASSED_IPV6_UNICAST_NON_ICMP;
+import static android.stats.connectivity.CounterName.CN_PASSED_MDNS;
+import static android.stats.connectivity.CounterName.CN_PASSED_MLD;
+import static android.stats.connectivity.CounterName.CN_PASSED_NON_IP_UNICAST;
+import static android.stats.connectivity.CounterName.CN_TOTAL_PACKETS;
+import static android.stats.connectivity.CounterName.CN_UNKNOWN;
+
 import android.net.apf.ApfCounterTracker.Counter;
 import android.stats.connectivity.CounterName;
 
 import androidx.annotation.VisibleForTesting;
 
+import java.util.EnumMap;
+import java.util.Map;
+
 /**
  * Class to record the network stack ApfSessionInfo metrics into statsd.
  *
@@ -31,6 +135,65 @@
 public class ApfSessionInfoMetrics {
     // Define the maximum size of the counter list
     public static final int MAX_NUM_OF_COUNTERS = Counter.class.getEnumConstants().length - 1;
+    private static final EnumMap<Counter, CounterName> apfCounterMetricsMap = new EnumMap<>(
+            Map.ofEntries(
+                Map.entry(TOTAL_PACKETS, CN_TOTAL_PACKETS),
+                // The counter sequence should be keep the same in ApfCounterTracker.java
+                Map.entry(PASSED_ARP, CN_PASSED_ARP),
+                Map.entry(PASSED_ARP_BROADCAST_REPLY, CN_PASSED_ARP_BROADCAST_REPLY),
+                // deprecated in ApfFilter, PASSED_ARP_NON_IPV4 ==> DROPPED_ARP_NON_IPV4
+                Map.entry(PASSED_ARP_NON_IPV4, CN_UNKNOWN),
+                Map.entry(PASSED_ARP_REQUEST, CN_PASSED_ARP_REQUEST),
+                Map.entry(PASSED_ARP_UNICAST_REPLY, CN_PASSED_ARP_UNICAST_REPLY),
+                // deprecated in ApfFilter, PASSED_ARP_UNKNOWN  ==> DROPPED_ARP_UNKNOWN
+                Map.entry(PASSED_ARP_UNKNOWN, CN_UNKNOWN),
+                Map.entry(PASSED_DHCP, CN_PASSED_DHCP),
+                Map.entry(PASSED_IPV4, CN_PASSED_IPV4),
+                Map.entry(PASSED_IPV4_FROM_DHCPV4_SERVER, CN_PASSED_IPV4_FROM_DHCPV4_SERVER),
+                Map.entry(PASSED_IPV4_UNICAST, CN_PASSED_IPV4_UNICAST),
+                Map.entry(PASSED_IPV6_ICMP, CN_PASSED_IPV6_ICMP),
+                Map.entry(PASSED_IPV6_NON_ICMP, CN_PASSED_IPV6_NON_ICMP),
+                Map.entry(PASSED_IPV6_NS_DAD, CN_PASSED_IPV6_NS_DAD),
+                Map.entry(PASSED_IPV6_NS_NO_ADDRESS, CN_PASSED_IPV6_NS_NO_ADDRESS),
+                Map.entry(PASSED_IPV6_NS_NO_SLLA_OPTION, CN_PASSED_IPV6_NS_NO_SLLA_OPTION),
+                Map.entry(PASSED_IPV6_NS_TENTATIVE, CN_PASSED_IPV6_NS_TENTATIVE),
+                Map.entry(PASSED_IPV6_UNICAST_NON_ICMP, CN_PASSED_IPV6_UNICAST_NON_ICMP),
+                Map.entry(PASSED_NON_IP_UNICAST, CN_PASSED_NON_IP_UNICAST),
+                Map.entry(PASSED_MDNS, CN_PASSED_MDNS),
+                Map.entry(PASSED_MLD, CN_PASSED_MLD),
+                Map.entry(DROPPED_ETH_BROADCAST, CN_DROPPED_ETH_BROADCAST),
+                Map.entry(DROPPED_RA, CN_DROPPED_RA),
+                Map.entry(DROPPED_IPV4_L2_BROADCAST, CN_DROPPED_IPV4_L2_BROADCAST),
+                Map.entry(DROPPED_IPV4_BROADCAST_ADDR, CN_DROPPED_IPV4_BROADCAST_ADDR),
+                Map.entry(DROPPED_IPV4_BROADCAST_NET, CN_DROPPED_IPV4_BROADCAST_NET),
+                Map.entry(DROPPED_IPV4_MULTICAST, CN_DROPPED_IPV4_MULTICAST),
+                Map.entry(DROPPED_IPV4_NON_DHCP4, CN_DROPPED_IPV4_NON_DHCP4),
+                Map.entry(DROPPED_IPV6_ROUTER_SOLICITATION, CN_DROPPED_IPV6_ROUTER_SOLICITATION),
+                Map.entry(DROPPED_IPV6_MULTICAST_NA, CN_DROPPED_IPV6_MULTICAST_NA),
+                Map.entry(DROPPED_IPV6_MULTICAST, CN_DROPPED_IPV6_MULTICAST),
+                Map.entry(DROPPED_IPV6_MULTICAST_PING, CN_DROPPED_IPV6_MULTICAST_PING),
+                Map.entry(DROPPED_IPV6_NON_ICMP_MULTICAST, CN_DROPPED_IPV6_NON_ICMP_MULTICAST),
+                Map.entry(DROPPED_IPV6_NS_INVALID, CN_DROPPED_IPV6_NS_INVALID),
+                Map.entry(DROPPED_IPV6_NS_OTHER_HOST, CN_DROPPED_IPV6_NS_OTHER_HOST),
+                Map.entry(DROPPED_IPV6_NS_REPLIED_NON_DAD, CN_DROPPED_IPV6_NS_REPLIED_NON_DAD),
+                Map.entry(DROPPED_802_3_FRAME, CN_DROPPED_802_3_FRAME),
+                Map.entry(DROPPED_ETHERTYPE_NOT_ALLOWED, CN_DROPPED_ETHERTYPE_NOT_ALLOWED),
+                Map.entry(DROPPED_IPV4_KEEPALIVE_ACK, CN_DROPPED_IPV4_KEEPALIVE_ACK),
+                Map.entry(DROPPED_IPV6_KEEPALIVE_ACK, CN_DROPPED_IPV6_KEEPALIVE_ACK),
+                Map.entry(DROPPED_IPV4_NATT_KEEPALIVE, CN_DROPPED_IPV4_NATT_KEEPALIVE),
+                Map.entry(DROPPED_MDNS, CN_DROPPED_MDNS),
+                // TODO: Not supported yet in the metrics backend.
+                Map.entry(DROPPED_IPV4_TCP_PORT7_UNICAST, CN_UNKNOWN),
+                Map.entry(DROPPED_ARP_NON_IPV4, CN_DROPPED_ARP_NON_IPV4),
+                Map.entry(DROPPED_ARP_OTHER_HOST, CN_DROPPED_ARP_OTHER_HOST),
+                Map.entry(DROPPED_ARP_REPLY_SPA_NO_HOST, CN_DROPPED_ARP_REPLY_SPA_NO_HOST),
+                Map.entry(DROPPED_ARP_REQUEST_ANYHOST, CN_DROPPED_ARP_REQUEST_ANYHOST),
+                Map.entry(DROPPED_ARP_REQUEST_REPLIED, CN_DROPPED_ARP_REQUEST_REPLIED),
+                Map.entry(DROPPED_ARP_UNKNOWN, CN_DROPPED_ARP_UNKNOWN),
+                Map.entry(DROPPED_ARP_V6_ONLY, CN_DROPPED_ARP_V6_ONLY),
+                Map.entry(DROPPED_GARP_REPLY, CN_DROPPED_GARP_REPLY)
+            )
+    );
     private final ApfSessionInfoReported.Builder mStatsBuilder =
             ApfSessionInfoReported.newBuilder();
     private final ApfCounterList.Builder mApfCounterListBuilder = ApfCounterList.newBuilder();
@@ -104,85 +267,6 @@
      */
     @VisibleForTesting
     public static CounterName apfFilterCounterToEnum(final Counter counter) {
-        switch(counter) {
-            case TOTAL_PACKETS:
-                return CounterName.CN_TOTAL_PACKETS;
-            case PASSED_ARP:
-                return CounterName.CN_PASSED_ARP;
-            case PASSED_DHCP:
-                return CounterName.CN_PASSED_DHCP;
-            case PASSED_IPV4:
-                return CounterName.CN_PASSED_IPV4;
-            case PASSED_IPV6_NON_ICMP:
-                return CounterName.CN_PASSED_IPV6_NON_ICMP;
-            case PASSED_IPV4_UNICAST:
-                return CounterName.CN_PASSED_IPV4_UNICAST;
-            case PASSED_IPV6_ICMP:
-                return CounterName.CN_PASSED_IPV6_ICMP;
-            case PASSED_IPV6_UNICAST_NON_ICMP:
-                return CounterName.CN_PASSED_IPV6_UNICAST_NON_ICMP;
-            // PASSED_ARP_NON_IPV4 and PASSED_ARP_UNKNOWN were deprecated in ApfFilter:
-            //     PASSED_ARP_NON_IPV4 ==> DROPPED_ARP_NON_IPV4
-            //     PASSED_ARP_UNKNOWN  ==> DROPPED_ARP_UNKNOWN
-            // They are not supported in the metrics.
-            case PASSED_ARP_NON_IPV4:
-            case PASSED_ARP_UNKNOWN:
-                return CounterName.CN_UNKNOWN;
-            case PASSED_ARP_UNICAST_REPLY:
-                return CounterName.CN_PASSED_ARP_UNICAST_REPLY;
-            case PASSED_NON_IP_UNICAST:
-                return CounterName.CN_PASSED_NON_IP_UNICAST;
-            case PASSED_MDNS:
-                return CounterName.CN_PASSED_MDNS;
-            case DROPPED_ETH_BROADCAST:
-                return CounterName.CN_DROPPED_ETH_BROADCAST;
-            case DROPPED_RA:
-                return CounterName.CN_DROPPED_RA;
-            case DROPPED_GARP_REPLY:
-                return CounterName.CN_DROPPED_GARP_REPLY;
-            case DROPPED_ARP_OTHER_HOST:
-                return CounterName.CN_DROPPED_ARP_OTHER_HOST;
-            case DROPPED_IPV4_L2_BROADCAST:
-                return CounterName.CN_DROPPED_IPV4_L2_BROADCAST;
-            case DROPPED_IPV4_BROADCAST_ADDR:
-                return CounterName.CN_DROPPED_IPV4_BROADCAST_ADDR;
-            case DROPPED_IPV4_BROADCAST_NET:
-                return CounterName.CN_DROPPED_IPV4_BROADCAST_NET;
-            case DROPPED_IPV4_MULTICAST:
-                return CounterName.CN_DROPPED_IPV4_MULTICAST;
-            case DROPPED_IPV6_ROUTER_SOLICITATION:
-                return CounterName.CN_DROPPED_IPV6_ROUTER_SOLICITATION;
-            case DROPPED_IPV6_MULTICAST_NA:
-                return CounterName.CN_DROPPED_IPV6_MULTICAST_NA;
-            case DROPPED_IPV6_MULTICAST:
-                return CounterName.CN_DROPPED_IPV6_MULTICAST;
-            case DROPPED_IPV6_MULTICAST_PING:
-                return CounterName.CN_DROPPED_IPV6_MULTICAST_PING;
-            case DROPPED_IPV6_NON_ICMP_MULTICAST:
-                return CounterName.CN_DROPPED_IPV6_NON_ICMP_MULTICAST;
-            case DROPPED_802_3_FRAME:
-                return CounterName.CN_DROPPED_802_3_FRAME;
-            case DROPPED_ETHERTYPE_NOT_ALLOWED:
-                return CounterName.CN_DROPPED_ETHERTYPE_DENYLISTED;
-            case DROPPED_ARP_REPLY_SPA_NO_HOST:
-                return CounterName.CN_DROPPED_ARP_REPLY_SPA_NO_HOST;
-            case DROPPED_IPV4_KEEPALIVE_ACK:
-                return CounterName.CN_DROPPED_IPV4_KEEPALIVE_ACK;
-            case DROPPED_IPV6_KEEPALIVE_ACK:
-                return CounterName.CN_DROPPED_IPV6_KEEPALIVE_ACK;
-            case DROPPED_IPV4_NATT_KEEPALIVE:
-                return CounterName.CN_DROPPED_IPV4_NATT_KEEPALIVE;
-            case DROPPED_MDNS:
-                return CounterName.CN_DROPPED_MDNS;
-            case DROPPED_IPV4_TCP_PORT7_UNICAST:
-                // TODO: Not supported yet in the metrics backend.
-                return CounterName.CN_UNKNOWN;
-            case DROPPED_ARP_NON_IPV4:
-                return CounterName.CN_DROPPED_ARP_NON_IPV4;
-            case DROPPED_ARP_UNKNOWN:
-                return CounterName.CN_DROPPED_ARP_UNKNOWN;
-            default:
-                return CounterName.CN_UNKNOWN;
-        }
+        return apfCounterMetricsMap.getOrDefault(counter, CN_UNKNOWN);
     }
 }
diff --git a/src/com/android/networkstack/util/NetworkStackUtils.java b/src/com/android/networkstack/util/NetworkStackUtils.java
index ac2832b..33a8308 100755
--- a/src/com/android/networkstack/util/NetworkStackUtils.java
+++ b/src/com/android/networkstack/util/NetworkStackUtils.java
@@ -274,13 +274,12 @@
     public static final String IPCLIENT_DHCPV6_PD_PREFERRED_FLAG_VERSION =
             "ipclient_dhcpv6_pd_preferred_flag_version";
 
-    /**** BEGIN Feature Kill Switch Flags ****/
-
     /**
-     * Kill switch flag to disable the feature of handle light doze mode in Apf.
+     * Experiment flag to enable Discovery of Designated Resolvers (DDR).
      */
-    public static final String APF_HANDLE_LIGHT_DOZE_FORCE_DISABLE =
-            "apf_handle_light_doze_force_disable";
+    public static final String DNS_DDR_VERSION = "dns_ddr_version";
+
+    /**** BEGIN Feature Kill Switch Flags ****/
 
     /**
      * Kill switch flag to disable the feature of skipping Tcp socket info polling when light
@@ -302,9 +301,15 @@
 
     /**
      * Kill switch flag to disable the feature of handle arp offload in Apf.
+     * Warning: the following flag String is incorrect. The feature that is not chickened out is
+     * "ARP offload" not "ARP offload force disabled".
      */
-    public static final String APF_HANDLE_ARP_OFFLOAD_FORCE_DISABLE =
-            "apf_handle_arp_offload_force_disable";
+    public static final String APF_HANDLE_ARP_OFFLOAD = "apf_handle_arp_offload_force_disable";
+
+    /**
+     * Kill switch flag to disable the feature of handle nd offload in Apf.
+     */
+    public static final String APF_HANDLE_ND_OFFLOAD = "apf_handle_nd_offload";
 
     static {
         System.loadLibrary("networkstackutilsjni");
diff --git a/src/com/android/server/NetworkStackService.java b/src/com/android/server/NetworkStackService.java
index aa8f3fa..4d10c3b 100644
--- a/src/com/android/server/NetworkStackService.java
+++ b/src/com/android/server/NetworkStackService.java
@@ -19,6 +19,7 @@
 import static android.net.dhcp.IDhcpServer.STATUS_INVALID_ARGUMENT;
 import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
 import static android.net.dhcp.IDhcpServer.STATUS_UNKNOWN_ERROR;
+import static android.net.util.RawSocketUtils.sendRawPacketDownStream;
 
 import static com.android.net.module.util.DeviceConfigUtils.getResBooleanConfig;
 import static com.android.net.module.util.FeatureVersions.FEATURE_IS_UID_NETWORKING_BLOCKED;
@@ -549,6 +550,23 @@
                                 mContext.getSystemService(ConnectivityManager.class);
                         pw.println(cm.isUidNetworkingBlocked(uid, metered /* isNetworkMetered */));
                         return 0;
+                    case "send-raw-packet-downstream": {
+                        // Usage : cmd network_stack send-raw-packet-downstream
+                        //         <interface> <packet-in-hex>
+                        // If no argument, get and display the usage help.
+                        if (getRemainingArgsCount() != 2) {
+                            onHelp();
+                            throw new IllegalArgumentException("Incorrect number of arguments");
+                        }
+                        final String iface = getNextArg();
+                        final String packetInHex = getNextArg();
+                        try {
+                            sendRawPacketDownStream(mContext, iface, packetInHex);
+                        } catch (Exception e) {
+                            throw new RuntimeException(e);
+                        }
+                        return 0;
+                    }
                     case "apf":
                         // Usage: cmd network_stack apf <iface> <cmd>
                         final String iface = getNextArg();
@@ -585,6 +603,12 @@
                 pw.println("    Get whether the networking is blocked for given uid and metered.");
                 pw.println("    <uid>: The target uid.");
                 pw.println("    <metered>: [true|false], Whether the target network is metered.");
+                pw.println("  send-raw-packet-downstream <interface> <packet-in-hex>");
+                pw.println("    Send raw packet for testing purpose.");
+                pw.println("    <interface>: Target interface name, note that this is limited");
+                pw.println("      to tethering downstream for security considerations.");
+                pw.println("    <packet_in_hex>: A valid hexadecimal representation of ");
+                pw.println("      a packet starting from L2 header.");
                 pw.println("  apf <iface> <cmd>");
                 pw.println("    APF utility commands for integration tests.");
                 pw.println("    <iface>: the network interface the provided command operates on.");
diff --git a/src/com/android/server/connectivity/DdrTracker.java b/src/com/android/server/connectivity/DdrTracker.java
new file mode 100644
index 0000000..af86914
--- /dev/null
+++ b/src/com/android/server/connectivity/DdrTracker.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import static com.android.net.module.util.ConnectivitySettingsUtils.PRIVATE_DNS_MODE_OFF;
+import static com.android.net.module.util.ConnectivitySettingsUtils.PRIVATE_DNS_MODE_OPPORTUNISTIC;
+import static com.android.net.module.util.ConnectivitySettingsUtils.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.net.shared.PrivateDnsConfig;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A class to perform DDR on a given network (to be implemented).
+ *
+ */
+class DdrTracker {
+    @IntDef(prefix = { "PRIVATE_DNS_MODE_" }, value = {
+        PRIVATE_DNS_MODE_OFF,
+        PRIVATE_DNS_MODE_OPPORTUNISTIC,
+        PRIVATE_DNS_MODE_PROVIDER_HOSTNAME
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    private @interface PrivateDnsMode {}
+
+    // Stores the DNS information that is synced with current DNS configuration.
+    @NonNull
+    private DnsInfo mDnsInfo;
+
+    DdrTracker() {
+        mDnsInfo = new DnsInfo(new PrivateDnsConfig(false /* useTls */));
+    }
+
+    /**
+     * If the private DNS settings on the network has changed, this function updates
+     * the DnsInfo and returns true; otherwise, the DnsInfo remains the same and this function
+     * returns false.
+     */
+    boolean notifyPrivateDnsSettingsChanged(@NonNull PrivateDnsConfig cfg) {
+        if (arePrivateDnsSettingsEquals(cfg, mDnsInfo.cfg)) return false;
+
+        mDnsInfo = new DnsInfo(cfg);
+        return true;
+    }
+
+    @PrivateDnsMode int getPrivateDnsMode() {
+        return mDnsInfo.cfg.mode;
+    }
+
+    // Returns a non-empty string (strict mode) or an empty string (off/opportunistic mode) .
+    @VisibleForTesting
+    @NonNull
+    String getStrictModeHostname() {
+        return mDnsInfo.cfg.hostname;
+    }
+
+    @VisibleForTesting
+    private static boolean arePrivateDnsSettingsEquals(@NonNull PrivateDnsConfig a,
+            @NonNull PrivateDnsConfig b) {
+        return a.mode == b.mode && TextUtils.equals(a.hostname, b.hostname);
+    }
+
+    /**
+     * A class to store current DNS configuration. Only the information relevant to DDR is stored.
+     *   1. Private DNS setting.
+     *   2. A list of Unencrypted DNS servers (to be implemented)
+     */
+    private static class DnsInfo {
+        @NonNull
+        public final PrivateDnsConfig cfg;
+
+        DnsInfo(@NonNull PrivateDnsConfig cfg) {
+            this.cfg = cfg;
+        }
+    }
+}
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java
index 895fc54..05103d9 100755
--- a/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -462,6 +462,9 @@
     @VisibleForTesting
     static final int MAX_PROBE_THREAD_POOL_SIZE = 5;
     private String mPrivateDnsProviderHostname = "";
+    private final boolean mDdrEnabled;
+    @NonNull
+    private final DdrTracker mDdrTracker;
 
     private final Context mContext;
     private final INetworkMonitorCallbacks mCallback;
@@ -679,6 +682,7 @@
                 context, NetworkStackUtils.REEVALUATE_WHEN_RESUME);
         mAsyncPrivdnsResolutionEnabled = deps.isFeatureEnabled(context,
                 NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION);
+        mDdrEnabled = deps.isFeatureEnabled(context, NetworkStackUtils.DNS_DDR_VERSION);
         mUseHttps = getUseHttpsValidation();
         mCaptivePortalUserAgent = getCaptivePortalUserAgent();
         mCaptivePortalFallbackSpecs =
@@ -714,6 +718,8 @@
         mLinkProperties = new LinkProperties();
         mNetworkCapabilities = new NetworkCapabilities(null);
         mNetworkAgentConfig = NetworkAgentConfigShimImpl.newInstance(null);
+
+        mDdrTracker = new DdrTracker();
     }
 
     /**
@@ -1084,6 +1090,9 @@
                 case CMD_PRIVATE_DNS_SETTINGS_CHANGED: {
                     final PrivateDnsConfig cfg = (PrivateDnsConfig) message.obj;
                     final TcpSocketTracker tst = getTcpSocketTracker();
+                    if (mDdrEnabled) {
+                        mDdrTracker.notifyPrivateDnsSettingsChanged(cfg);
+                    }
                     if (!isPrivateDnsValidationRequired() || !cfg.inStrictMode()) {
                         // No DNS resolution required.
                         //
diff --git a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
index 84bf7e6..8c81de5 100644
--- a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
+++ b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
@@ -2855,17 +2855,6 @@
                 (byte) 0x06, data);
     }
 
-    private void assertDhcpResultsParcelable(final DhcpResultsParcelable lease) {
-        assertNotNull(lease);
-        assertEquals(CLIENT_ADDR, lease.baseConfiguration.getIpAddress().getAddress());
-        assertEquals(SERVER_ADDR, lease.baseConfiguration.getGateway());
-        assertEquals(1, lease.baseConfiguration.getDnsServers().size());
-        assertTrue(lease.baseConfiguration.getDnsServers().contains(SERVER_ADDR));
-        assertEquals(SERVER_ADDR, InetAddresses.parseNumericAddress(lease.serverAddress));
-        assertEquals(TEST_DEFAULT_MTU, lease.mtu);
-        assertEquals(TEST_LEASE_DURATION_S, lease.leaseDuration);
-    }
-
     private void doUpstreamHotspotDetectionTest(final int id, final String displayName,
             final String ssid, final byte[] oui, final byte type, final byte[] data,
             final boolean expectMetered) throws Exception {
@@ -2884,7 +2873,13 @@
                 ArgumentCaptor.forClass(DhcpResultsParcelable.class);
         verify(mCb, timeout(TEST_TIMEOUT_MS)).onNewDhcpResults(captor.capture());
         final DhcpResultsParcelable lease = captor.getValue();
-        assertDhcpResultsParcelable(lease);
+        assertNotNull(lease);
+        assertEquals(CLIENT_ADDR, lease.baseConfiguration.getIpAddress().getAddress());
+        assertEquals(SERVER_ADDR, lease.baseConfiguration.getGateway());
+        assertEquals(1, lease.baseConfiguration.getDnsServers().size());
+        assertTrue(lease.baseConfiguration.getDnsServers().contains(SERVER_ADDR));
+        assertEquals(SERVER_ADDR, InetAddresses.parseNumericAddress(lease.serverAddress));
+        assertEquals(TEST_DEFAULT_MTU, lease.mtu);
 
         if (expectMetered) {
             assertEquals(lease.vendorInfo, DhcpPacket.VENDOR_INFO_ANDROID_METERED);
@@ -5874,7 +5869,7 @@
         final ProvisioningConfiguration cfg = new ProvisioningConfiguration.Builder()
                 .withoutIPv6()
                 .build();
-        setDeviceConfigProperty(CONFIG_MINIMUM_LEASE,  5 /* default minimum lease */);
+        setDeviceConfigProperty(CONFIG_MINIMUM_LEASE,  5/* default minimum lease */);
         startIpClientProvisioning(cfg);
         handleDhcpPackets(true /* isSuccessLease */, 4 /* lease duration */,
                 false /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU,
@@ -5889,8 +5884,6 @@
         sendArpReply(request.senderHwAddress.toByteArray() /* dst */, ROUTER_MAC_BYTES /* srcMac */,
                 request.senderIp /* target IP */, SERVER_ADDR /* sender IP */);
 
-        clearInvocations(mCb);
-
         // Then client sends unicast DHCPREQUEST to extend the IPv4 address lifetime, and we reply
         // with DHCPACK to refresh the DHCP lease.
         final DhcpPacket packet = getNextDhcpPacket();
@@ -5900,32 +5893,12 @@
                 TEST_LEASE_DURATION_S, (short) TEST_DEFAULT_MTU,
                 false /* rapidCommit */, null /* captivePortalApiUrl */));
 
-        // The IPv4 link address lifetime should be also updated after a success DHCP renew, check
-        // that we should never see provisioning failure.
-        verify(mCb, after(100).never()).onProvisioningFailure(any());
-
-        final ArgumentCaptor<DhcpResultsParcelable> dhcpResultsCaptor =
-                ArgumentCaptor.forClass(DhcpResultsParcelable.class);
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).onNewDhcpResults(dhcpResultsCaptor.capture());
-        final DhcpResultsParcelable lease = dhcpResultsCaptor.getValue();
-        assertDhcpResultsParcelable(lease);
-
-        // Check if the IPv4 address lifetime has updated along with a success DHCP renew.
-        verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(argThat(x -> {
-            for (LinkAddress la : x.getLinkAddresses()) {
-                if (la.isIpv4()) {
-                    final long now = SystemClock.elapsedRealtime();
-                    final long when = now + 3600 * 1000;
-                    return (la.getDeprecationTime() != LinkAddress.LIFETIME_UNKNOWN)
-                            && (la.getExpirationTime() != LinkAddress.LIFETIME_UNKNOWN)
-                            && (la.getDeprecationTime() < when + TEST_LIFETIME_TOLERANCE_MS)
-                            && (la.getDeprecationTime() > when - TEST_LIFETIME_TOLERANCE_MS)
-                            && (la.getExpirationTime() < when + TEST_LIFETIME_TOLERANCE_MS)
-                            && (la.getExpirationTime() > when - TEST_LIFETIME_TOLERANCE_MS);
-                }
-            }
-            return false;
-        }));
+        // Once the IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION flag is enabled, the IP
+        // lease will be refreshed as well as the link address lifetime by transiting to
+        // ConfiguringInterfaceState, where IpClient sends a new RTM_NEWADDR message to kernel
+        // to update the IPv4 address, therefore, we should never see provisioning failure any
+        // more.
+        verify(mCb, never()).onProvisioningFailure(any());
     }
 
     private void doDhcpHostnameSettingTest(int hostnameSetting,
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 91e94a8..e26ea28 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -32,7 +32,7 @@
         "kotlin-reflect",
         "mockito-target-extended-minus-junit4",
         "net-tests-utils",
-        //"net-utils-framework-common",
+        "net-utils-framework-common",
         "testables",
     ],
     libs: [
diff --git a/tests/unit/src/android/net/apf/ApfFilterTest.kt b/tests/unit/src/android/net/apf/ApfFilterTest.kt
new file mode 100644
index 0000000..938bf25
--- /dev/null
+++ b/tests/unit/src/android/net/apf/ApfFilterTest.kt
@@ -0,0 +1,1197 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.apf
+
+import android.content.Context
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.MacAddress
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_REQUEST_REPLIED
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_ETHERTYPE_NOT_ALLOWED
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_NON_DHCP4
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_INVALID
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_OTHER_HOST
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_REPLIED_NON_DAD
+import android.net.apf.ApfCounterTracker.Counter.PASSED_ARP_REQUEST
+import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV4
+import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV4_FROM_DHCPV4_SERVER
+import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_ICMP
+import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_DAD
+import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_NO_ADDRESS
+import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_NO_SLLA_OPTION
+import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_TENTATIVE
+import android.net.apf.ApfFilter.Dependencies
+import android.net.apf.ApfTestHelpers.Companion.verifyProgramRun
+import android.net.apf.BaseApfGenerator.APF_VERSION_3
+import android.net.apf.BaseApfGenerator.APF_VERSION_6
+import android.net.ip.IpClient.IpClientCallbacksWrapper
+import android.os.Build
+import android.os.SystemClock
+import android.system.OsConstants.IFA_F_TENTATIVE
+import androidx.test.filters.SmallTest
+import com.android.internal.annotations.GuardedBy
+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
+import com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.ICMPV6_NA_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.ICMPV6_NS_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN
+import com.android.net.module.util.arp.ArpPacket
+import com.android.networkstack.metrics.NetworkQuirkMetrics
+import com.android.networkstack.packets.NeighborAdvertisement
+import com.android.networkstack.packets.NeighborSolicitation
+import com.android.networkstack.util.NetworkStackUtils
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.quitResources
+import java.net.Inet6Address
+import java.net.InetAddress
+import kotlin.test.assertContentEquals
+import kotlin.test.assertEquals
+import org.junit.After
+import org.junit.Before
+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.doAnswer
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+import org.mockito.invocation.InvocationOnMock
+
+/**
+ * Test for APF filter.
+ */
+@DevSdkIgnoreRunner.MonitorThreadLeak
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+class ApfFilterTest {
+    companion object {
+        private const val THREAD_QUIT_MAX_RETRY_COUNT = 3
+    }
+
+    @get:Rule
+    val ignoreRule = DevSdkIgnoreRule()
+
+    @Mock
+    private lateinit var context: Context
+
+    @Mock private lateinit var metrics: NetworkQuirkMetrics
+
+    @Mock private lateinit var dependencies: Dependencies
+
+    @Mock private lateinit var ipClientCallback: IpClientCallbacksWrapper
+
+    @GuardedBy("mApfFilterCreated")
+    private val mApfFilterCreated = ArrayList<AndroidPacketFilter>()
+    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 hostIpv4Address = byteArrayOf(10, 0, 0, 1)
+    private val senderIpv4Address = byteArrayOf(10, 0, 0, 2)
+    private val arpBroadcastMacAddress = intArrayOf(0xff, 0xff, 0xff, 0xff, 0xff, 0xff)
+        .map { it.toByte() }.toByteArray()
+    private val senderMacAddress = intArrayOf(0x02, 0x22, 0x33, 0x44, 0x55, 0x66)
+        .map { it.toByte() }.toByteArray()
+    private val senderIpv6Address =
+        // 2001::200:1a:1122:3344
+        intArrayOf(0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x02, 0, 0, 0x1a, 0x11, 0x22, 0x33, 0x44)
+            .map{ it.toByte() }.toByteArray()
+    private val hostIpv6Addresses = listOf(
+        // 2001::200:1a:3344:1122
+        intArrayOf(0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x02, 0, 0, 0x1a, 0x33, 0x44, 0x11, 0x22)
+            .map{ it.toByte() }.toByteArray(),
+        // 2001::100:1b:4455:6677
+        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)
+            .map{ it.toByte() }.toByteArray()
+    )
+    private val hostMulticastMacAddresses = listOf(
+        // 33:33:00:00:00:01
+        intArrayOf(0x33, 0x33, 0, 0, 0, 1).map { it.toByte() }.toByteArray(),
+        // 33:33:ff:44:11:22
+        intArrayOf(0x33, 0x33, 0xff, 0x44, 0x11, 0x22).map { it.toByte() }.toByteArray(),
+        // 33:33:ff:55:66:77
+        intArrayOf(0x33, 0x33, 0xff, 0x55, 0x66, 0x77).map { it.toByte() }.toByteArray(),
+        // 33:33:ff:bb:cc:dd
+        intArrayOf(0x33, 0x33, 0xff, 0xbb, 0xcc, 0xdd).map { it.toByte() }.toByteArray(),
+    )
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        // mock anycast6 address from /proc/net/anycast6
+        `when`(dependencies.getAnycast6Addresses(any())).thenReturn(hostAnycast6Addresses)
+
+        // mock ether multicast mac address from /proc/net/dev_mcast
+        `when`(dependencies.getEtherMulticastAddresses(any())).thenReturn(hostMulticastMacAddresses)
+
+        // mock nd traffic class from /proc/sys/net/ipv6/conf/{ifname}/ndisc_tclass
+        `when`(dependencies.getNdTrafficClass(any())).thenReturn(0)
+        doAnswer { invocation: InvocationOnMock ->
+            synchronized(mApfFilterCreated) {
+                mApfFilterCreated.add(invocation.getArgument(0))
+            }
+        }.`when`(dependencies).onApfFilterCreated(any())
+        `when`(dependencies.elapsedRealtime()).thenReturn(SystemClock.elapsedRealtime())
+    }
+
+    private fun shutdownApfFilters() {
+        quitResources(THREAD_QUIT_MAX_RETRY_COUNT, {
+            synchronized(mApfFilterCreated) {
+                val ret = ArrayList(mApfFilterCreated)
+                mApfFilterCreated.clear()
+                return@quitResources ret
+            }
+        }, { apf: AndroidPacketFilter ->
+            apf.shutdown()
+        })
+
+        synchronized(mApfFilterCreated) {
+            assertEquals(
+                0,
+                mApfFilterCreated.size.toLong(),
+                "ApfFilters did not fully shutdown."
+            )
+        }
+    }
+
+    @After
+    fun tearDown() {
+        shutdownApfFilters()
+        Mockito.framework().clearInlineMocks()
+        ApfJniUtils.resetTransmittedPacketMemory()
+    }
+
+    private fun getDefaultConfig(apfVersion: Int = APF_VERSION_6): ApfFilter.ApfConfiguration {
+        val config = ApfFilter.ApfConfiguration()
+        config.apfVersionSupported = apfVersion
+        // 4K is the highly recommended value in APFv6 for vendor
+        config.apfRamSize = 4096
+        config.multicastFilter = false
+        config.ieee802_3Filter = false
+        config.ethTypeBlackList = IntArray(0)
+        config.shouldHandleArpOffload = true
+        config.shouldHandleNdOffload = true
+        return config
+    }
+
+    private fun getApfFilter(
+        apfCfg: ApfFilter.ApfConfiguration = getDefaultConfig(APF_VERSION_6)
+    ): ApfFilter {
+        return ApfFilter(
+            context,
+            apfCfg,
+            ifParams,
+            ipClientCallback,
+            metrics,
+            dependencies
+        )
+    }
+
+    private fun doTestEtherTypeAllowListFilter(apfFilter: ApfFilter) {
+        val programCaptor = ArgumentCaptor.forClass(ByteArray::class.java)
+        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")
+        //   ip = IP(src="192.168.1.1")
+        //   udp = UDP(sport=5353, dport=5353)
+        //   dns = DNS(qd=DNSQR(qtype="PTR", qname="a.local"))
+        //   p = eth/ip/udp/dns
+        val mdnsPkt = "01005e0000fbe89f806660bb080045000035000100004011d812c0a80101e00000f" +
+                "b14e914e900214d970000010000010000000000000161056c6f63616c00000c0001"
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(mdnsPkt),
+            PASSED_IPV4
+        )
+
+        // Using scapy to generate RA packet:
+        //  eth = Ether(src="E8:9F:80:66:60:BB", dst="33:33:00:00:00:01")
+        //  ip6 = IPv6(src="fe80::1", dst="ff02::1")
+        //  icmp6 = ICMPv6ND_RA(routerlifetime=3600, retranstimer=3600)
+        //  p = eth/ip6/icmp6
+        val raPkt = "333300000001e89f806660bb86dd6000000000103afffe800000000000000000000000" +
+                "000001ff0200000000000000000000000000018600600700080e100000000000000e10"
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(raPkt),
+            PASSED_IPV6_ICMP
+        )
+
+        // Using scapy to generate ethernet packet with type 0x88A2:
+        //  p = Ether(type=0x88A2)/Raw(load="01")
+        val ethPkt = "ffffffffffff047bcb463fb588a23031"
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(ethPkt),
+            DROPPED_ETHERTYPE_NOT_ALLOWED
+        )
+    }
+
+    private fun generateNsPacket(
+        srcMac: ByteArray,
+        dstMac: ByteArray,
+        srcIp: ByteArray,
+        dstIp: ByteArray,
+        target: ByteArray,
+    ): ByteArray {
+        val nsPacketBuf = NeighborSolicitation.build(
+            MacAddress.fromBytes(srcMac),
+            MacAddress.fromBytes(dstMac),
+            InetAddress.getByAddress(srcIp) as Inet6Address,
+            InetAddress.getByAddress(dstIp) as Inet6Address,
+            InetAddress.getByAddress(target) as Inet6Address
+        )
+
+        val nsPacket = ByteArray(
+            ETHER_HEADER_LEN + IPV6_HEADER_LEN + ICMPV6_NS_HEADER_LEN + 8 // option length
+        )
+        nsPacketBuf.get(nsPacket)
+        return nsPacket
+    }
+
+    private fun generateNaPacket(
+        srcMac: ByteArray,
+        dstMac: ByteArray,
+        srcIp: ByteArray,
+        dstIp: ByteArray,
+        flags: Int,
+        target: ByteArray,
+    ): ByteArray {
+        val naPacketBuf = NeighborAdvertisement.build(
+            MacAddress.fromBytes(srcMac),
+            MacAddress.fromBytes(dstMac),
+            InetAddress.getByAddress(srcIp) as Inet6Address,
+            InetAddress.getByAddress(dstIp) as Inet6Address,
+            flags,
+            InetAddress.getByAddress(target) as Inet6Address
+        )
+        val naPacket = ByteArray(
+            ETHER_HEADER_LEN + IPV6_HEADER_LEN + ICMPV6_NA_HEADER_LEN + 8 // lla option length
+        )
+
+        naPacketBuf.get(naPacket)
+        return naPacket
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    fun testV4EtherTypeAllowListFilter() {
+        val apfFilter = getApfFilter(getDefaultConfig(APF_VERSION_3))
+        doTestEtherTypeAllowListFilter(apfFilter)
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    fun testV6EtherTypeAllowListFilter() {
+        val apfFilter = getApfFilter(getDefaultConfig(APF_VERSION_6))
+        doTestEtherTypeAllowListFilter(apfFilter)
+    }
+
+    @Test
+    fun testIPv4PacketFilterOnV6OnlyNetwork() {
+        val apfFilter = getApfFilter()
+        apfFilter.updateClatInterfaceState(true)
+        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")
+        //   ip = IP(src="192.168.1.1")
+        //   udp = UDP(sport=5353, dport=5353)
+        //   dns = DNS(qd=DNSQR(qtype="PTR", qname="a.local"))
+        //   p = eth/ip/udp/dns
+        val mdnsPkt = "01005e0000fbe89f806660bb080045000035000100004011d812c0a80101e00000f" +
+                "b14e914e900214d970000010000010000000000000161056c6f63616c00000c0001"
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(mdnsPkt),
+            DROPPED_IPV4_NON_DHCP4
+        )
+
+        // Using scapy to generate DHCP4 offer packet:
+        //   ether = Ether(src='00:11:22:33:44:55', dst='ff:ff:ff:ff:ff:ff')
+        //   ip = IP(src='192.168.1.1', dst='255.255.255.255')
+        //   udp = UDP(sport=67, dport=68)
+        //   bootp = BOOTP(op=2,
+        //                 yiaddr='192.168.1.100',
+        //                 siaddr='192.168.1.1',
+        //                 chaddr=b'\x00\x11\x22\x33\x44\x55')
+        //   dhcp_options = [('message-type', 'offer'),
+        //                   ('server_id', '192.168.1.1'),
+        //                   ('subnet_mask', '255.255.255.0'),
+        //                   ('router', '192.168.1.1'),
+        //                   ('lease_time', 86400),
+        //                   ('name_server', '8.8.8.8'),
+        //                   'end']
+        //   dhcp = DHCP(options=dhcp_options)
+        //   dhcp_offer_packet = ether/ip/udp/bootp/dhcp
+        val dhcp4Pkt =
+            "ffffffffffff00112233445508004500012e000100004011b815c0a80101ffffffff0043" +
+                    "0044011a5ffc02010600000000000000000000000000c0a80164c0a80101000000000011" +
+                    "223344550000000000000000000000000000000000000000000000000000000000000000" +
+                    "000000000000000000000000000000000000000000000000000000000000000000000000" +
+                    "000000000000000000000000000000000000000000000000000000000000000000000000" +
+                    "000000000000000000000000000000000000000000000000000000000000000000000000" +
+                    "000000000000000000000000000000000000000000000000000000000000000000000000" +
+                    "0000000000000000000000000000000000000000000000000000638253633501023604c0" +
+                    "a801010104ffffff000304c0a80101330400015180060408080808ff"
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(dhcp4Pkt),
+            PASSED_IPV4_FROM_DHCPV4_SERVER
+        )
+
+        // Using scapy to generate DHCP4 offer packet:
+        //   eth = Ether(src="E8:9F:80:66:60:BB", dst="01:00:5E:00:00:FB")
+        //   ip = IP(src="192.168.1.10", dst="192.168.1.20")  # IPv4
+        //   udp = UDP(sport=12345, dport=53)
+        //   dns = DNS(qd=DNSQR(qtype="PTR", qname="a.local"))
+        //   pkt = eth / ip / udp / dns
+        //   fragments = fragment(pkt, fragsize=30)
+        //   fragments[1]
+        val fragmentedUdpPkt =
+            "01005e0000fbe89f806660bb08004500001d000100034011f75dc0a8010ac0a8" +
+                    "01146f63616c00000c0001"
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(fragmentedUdpPkt),
+            DROPPED_IPV4_NON_DHCP4
+        )
+    }
+
+    // The APFv6 code path is only turned on in V+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testArpTransmit() {
+        val apfFilter = getApfFilter()
+        verify(ipClientCallback, times(2)).installPacketFilter(any())
+        val linkAddress = LinkAddress(InetAddress.getByAddress(hostIpv4Address), 24)
+        val lp = LinkProperties()
+        lp.addLinkAddress(linkAddress)
+        apfFilter.setLinkProperties(lp)
+        val programCaptor = ArgumentCaptor.forClass(ByteArray::class.java)
+        verify(ipClientCallback, times(3)).installPacketFilter(programCaptor.capture())
+        val program = programCaptor.value
+        val receivedArpPacketBuf = ArpPacket.buildArpPacket(
+            arpBroadcastMacAddress,
+            senderMacAddress,
+            hostIpv4Address,
+            HexDump.hexStringToByteArray("000000000000"),
+            senderIpv4Address,
+            ARP_REQUEST.toShort()
+        )
+        val receivedArpPacket = ByteArray(ARP_ETHER_IPV4_LEN)
+        receivedArpPacketBuf.get(receivedArpPacket)
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            receivedArpPacket,
+            DROPPED_ARP_REQUEST_REPLIED
+        )
+
+        val transmittedPacket = ApfJniUtils.getTransmittedPacket()
+        val expectedArpReplyBuf = ArpPacket.buildArpPacket(
+            senderMacAddress,
+            apfFilter.mHardwareAddress,
+            senderIpv4Address,
+            senderMacAddress,
+            hostIpv4Address,
+            ARP_REPLY.toShort()
+        )
+        val expectedArpReplyPacket = ByteArray(ARP_ETHER_IPV4_LEN)
+        expectedArpReplyBuf.get(expectedArpReplyPacket)
+        assertContentEquals(
+            expectedArpReplyPacket + ByteArray(18) { 0 },
+            transmittedPacket
+        )
+    }
+
+    @Test
+    fun testArpOffloadDisabled() {
+        val apfConfig = getDefaultConfig()
+        apfConfig.shouldHandleArpOffload = false
+        val apfFilter = getApfFilter(apfConfig)
+        verify(ipClientCallback, times(2)).installPacketFilter(any())
+        val linkAddress = LinkAddress(InetAddress.getByAddress(hostIpv4Address), 24)
+        val lp = LinkProperties()
+        lp.addLinkAddress(linkAddress)
+        apfFilter.setLinkProperties(lp)
+        val programCaptor = ArgumentCaptor.forClass(ByteArray::class.java)
+        verify(ipClientCallback, times(3)).installPacketFilter(programCaptor.capture())
+        val program = programCaptor.value
+        val receivedArpPacketBuf = ArpPacket.buildArpPacket(
+            arpBroadcastMacAddress,
+            senderMacAddress,
+            hostIpv4Address,
+            HexDump.hexStringToByteArray("000000000000"),
+            senderIpv4Address,
+            ARP_REQUEST.toShort()
+        )
+        val receivedArpPacket = ByteArray(ARP_ETHER_IPV4_LEN)
+        receivedArpPacketBuf.get(receivedArpPacket)
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            receivedArpPacket,
+            PASSED_ARP_REQUEST
+        )
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    fun testNsFilterNoIPv6() {
+        `when`(dependencies.getAnycast6Addresses(any())).thenReturn(listOf())
+        val apfFilter = getApfFilter()
+        // validate NS packet check when there is no IPv6 address
+        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)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // pkt = eth/ip6/icmp6
+        val nsPkt = "01020304050600010203040586DD6000000000183AFF200100000000000" +
+                "00200001A1122334420010000000000000200001A334411228700452900" +
+                "00000020010000000000000200001A33441122"
+        // when there is no IPv6 addresses -> pass NS packet
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(nsPkt),
+            PASSED_IPV6_NS_NO_ADDRESS
+        )
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    fun testNsFilter() {
+        val apfFilter = getApfFilter()
+        verify(ipClientCallback, times(2)).installPacketFilter(any())
+
+        val lp = LinkProperties()
+        for (addr in hostIpv6Addresses) {
+            lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
+        }
+
+        for (addr in hostIpv6TentativeAddresses) {
+            lp.addLinkAddress(
+                LinkAddress(
+                    InetAddress.getByAddress(addr),
+                    64,
+                    IFA_F_TENTATIVE,
+                    0
+                )
+            )
+        }
+
+        apfFilter.setLinkProperties(lp)
+        verify(ipClientCallback, times(3)).installPacketFilter(any())
+        apfFilter.updateClatInterfaceState(true)
+        val programCaptor = ArgumentCaptor.forClass(ByteArray::class.java)
+        verify(ipClientCallback, times(4)).installPacketFilter(programCaptor.capture())
+        val program = programCaptor.value
+
+        // validate Ethernet dst address check
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="00:05:04:03:02:01")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt = ICMPv6NDOptDstLLAddr(lladdr="00:01:02:03:04:05")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val nonHostDstMacNsPkt =
+            "00050403020100010203040586DD6000000000203AFF2001000000000000" +
+                    "0200001A1122334420010000000000000200001A3344112287003D170000" +
+                    "000020010000000000000200001A334411220201000102030405"
+        // invalid unicast ether dst -> pass
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(nonHostDstMacNsPkt),
+            DROPPED_IPV6_NS_OTHER_HOST
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="33:33:ff:03:02:01")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt = ICMPv6NDOptDstLLAddr(lladdr="00:01:02:03:04:05")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val nonMcastDstMacNsPkt = "3333FF03020100010203040586DD6000000000203AFF20010000000000" +
+                "000200001A1122334420010000000000000200001A3344112287003D17" +
+                "0000000020010000000000000200001A334411220201000102030405"
+        // mcast dst mac is not one of solicited mcast mac derived from one of device's ip -> pass
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(nonMcastDstMacNsPkt),
+            DROPPED_IPV6_NS_OTHER_HOST
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="33:33:ff:44:11:22")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="00:01:02:03:04:05")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val hostMcastDstMacNsPkt =
+            "3333FF44112200010203040586DD6000000000203AFF20010000000000" +
+                    "000200001A1122334420010000000000000200001A3344112287003E17" +
+                    "0000000020010000000000000200001A334411220101000102030405"
+        // mcast dst mac is one of solicited mcast mac derived from one of device's ip
+        // -> drop and replied
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(hostMcastDstMacNsPkt),
+            DROPPED_IPV6_NS_REPLIED_NON_DAD
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="FF:FF:FF:FF:FF:FF")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="00:01:02:03:04:05")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val broadcastNsPkt =
+            "FFFFFFFFFFFF00010203040586DD6000000000203AFF200100000000000002000" +
+                    "01A1122334420010000000000000200001A3344112287003E1700000000200100" +
+                    "00000000000200001A334411220101000102030405"
+        // mcast dst mac is broadcast address -> drop and replied
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(broadcastNsPkt),
+            DROPPED_IPV6_NS_REPLIED_NON_DAD
+        )
+
+        // validate IPv6 dst address check
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="00:01:02:03:04:05")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val validHostDstIpNsPkt =
+            "02030405060700010203040586DD6000000000203AFF200100000000000" +
+                    "00200001A1122334420010000000000000200001A3344112287003E1700" +
+                    "00000020010000000000000200001A334411220101000102030405"
+        // dst ip is one of device's ip -> drop and replied
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(validHostDstIpNsPkt),
+            DROPPED_IPV6_NS_REPLIED_NON_DAD
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::100:1b:aabb:ccdd", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::100:1b:aabb:ccdd")
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="00:01:02:03:04:05")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val validHostAnycastDstIpNsPkt =
+            "02030405060700010203040586DD6000000000203AFF20010000" +
+                    "000000000200001A1122334420010000000000000100001BAABB" +
+                    "CCDD8700D9AE0000000020010000000000000100001BAABBCCDD" +
+                    "0101000102030405"
+        // dst ip is device's anycast address -> drop and replied
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(validHostAnycastDstIpNsPkt),
+            DROPPED_IPV6_NS_REPLIED_NON_DAD
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:4444:5555", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="00:01:02:03:04:05")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val nonHostUcastDstIpNsPkt =
+            "02030405060700010203040586DD6000000000203AFF2001000000000" +
+                    "0000200001A1122334420010000000000000200001A444455558700E8" +
+                    "E30000000020010000000000000200001A334411220101000102030405"
+        // unicast dst ip is not one of device's ip -> pass
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(nonHostUcastDstIpNsPkt),
+            DROPPED_IPV6_NS_OTHER_HOST
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="ff02::1:ff44:1133", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="00:01:02:03:04:05")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val nonHostMcastDstIpNsPkt =
+            "02030405060700010203040586DD6000000000203AFF2001000000000" +
+                    "0000200001A11223344FF0200000000000000000001FF441133870095" +
+                    "1C0000000020010000000000000200001A334411220101000102030405"
+        // mcast dst ip is not one of solicited mcast ip derived from one of device's ip -> pass
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(nonHostMcastDstIpNsPkt),
+            DROPPED_IPV6_NS_OTHER_HOST
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="ff02::1:ff44:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="00:01:02:03:04:05")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val hostMcastDstIpNsPkt =
+            "02030405060700010203040586DD6000000000203AFF2001000000000000" +
+                    "0200001A11223344FF0200000000000000000001FF4411228700952D0000" +
+                    "000020010000000000000200001A334411220101000102030405"
+        // mcast dst ip is one of solicited mcast ip derived from one of device's ip
+        //   -> drop and replied
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(hostMcastDstIpNsPkt),
+            DROPPED_IPV6_NS_REPLIED_NON_DAD
+        )
+
+        // validate IPv6 NS payload check
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255, plen=20)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="01:02:03:04:05:06")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val shortNsPkt =
+            "02030405060700010203040586DD6000000000143AFF20010000000000000200001A1" +
+                    "122334420010000000000000200001A3344112287003B140000000020010000000000" +
+                    "000200001A334411220101010203040506"
+        // payload len < 24 -> drop
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(shortNsPkt),
+            DROPPED_IPV6_NS_INVALID
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:4444:5555")
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="01:02:03:04:05:06")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val otherHostNsPkt =
+            "02030405060700010203040586DD6000000000203AFF200100000000000002000" +
+                    "01A1122334420010000000000000200001A334411228700E5E000000000200100" +
+                    "00000000000200001A444455550101010203040506"
+        // target ip is not one of device's ip -> drop
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(otherHostNsPkt),
+            DROPPED_IPV6_NS_OTHER_HOST
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=20)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="01:02:03:04:05:06")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val invalidHoplimitNsPkt =
+            "02030405060700010203040586DD6000000000203A14200100000000000" +
+                    "00200001A1122334420010000000000000200001A3344112287003B1400" +
+                    "00000020010000000000000200001A334411220101010203040506"
+        // hoplimit is not 255 -> drop
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(invalidHoplimitNsPkt),
+            DROPPED_IPV6_NS_INVALID
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122", code=5)
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="01:02:03:04:05:06")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val invalidIcmpCodeNsPkt =
+            "02030405060700010203040586DD6000000000203AFF200100000000000" +
+                    "00200001A1122334420010000000000000200001A3344112287053B0F00" +
+                    "00000020010000000000000200001A334411220101010203040506"
+        // icmp6 code is not 0 -> drop
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(invalidIcmpCodeNsPkt),
+            DROPPED_IPV6_NS_INVALID
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:1234:5678")
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="01:02:03:04:05:06")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val tentativeTargetIpNsPkt =
+            "02030405060700010203040586DD6000000000203AFF200100000000" +
+                    "00000200001A1122334420010000000000000200001A334411228700" +
+                    "16CE0000000020010000000000000200001A123456780101010203040506"
+        // target ip is one of tentative address -> pass
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(tentativeTargetIpNsPkt),
+            PASSED_IPV6_NS_TENTATIVE
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1c:2255:6666")
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="01:02:03:04:05:06")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val invalidTargetIpNsPkt =
+            "02030405060700010203040586DD6000000000203AFF200100000000000" +
+                    "00200001A1122334420010000000000000200001A334411228700F6BC00" +
+                    "00000020010000000000000200001C225566660101010203040506"
+        // target ip is none of {non-tentative, anycast} -> drop
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(invalidTargetIpNsPkt),
+            DROPPED_IPV6_NS_OTHER_HOST
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="::", dst="ff02::1:ff44:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt = ICMPv6NDOptDstLLAddr(lladdr="02:03:04:05:06:07")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val dadNsPkt =
+            "02030405060700010203040586DD6000000000203AFF000000000000000000000000000" +
+                    "00000FF0200000000000000000001FF4411228700F4A800000000200100000000000002" +
+                    "00001A334411220201020304050607"
+        // DAD NS request -> pass
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(dadNsPkt),
+            PASSED_IPV6_NS_DAD
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // pkt = eth/ip6/icmp6
+        val noOptionNsPkt =
+            "02030405060700010203040586DD6000000000183AFF2001000000000000020000" +
+                    "1A1122334420010000000000000200001A33441122870045290000000020010000" +
+                    "000000000200001A33441122"
+        // payload len < 32 -> pass
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(noOptionNsPkt),
+            PASSED_IPV6_NS_NO_SLLA_OPTION
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="ff01::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="01:02:03:04:05:06")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val nonDadMcastSrcIpPkt =
+            "02030405060700010203040586DD6000000000203AFFFF01000000000000" +
+                    "0200001A1122334420010000000000000200001A3344112287005C130000" +
+                    "000020010000000000000200001A334411220101010203040506"
+        // non-DAD src IPv6 is FF::/8 -> drop
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(nonDadMcastSrcIpPkt),
+            DROPPED_IPV6_NS_INVALID
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="0001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="01:02:03:04:05:06")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val nonDadLoopbackSrcIpPkt =
+            "02030405060700010203040586DD6000000000203AFF0001000000000" +
+                    "0000200001A1122334420010000000000000200001A3344112287005B" +
+                    "140000000020010000000000000200001A334411220101010203040506"
+        // non-DAD src IPv6 is 00::/8 -> drop
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(nonDadLoopbackSrcIpPkt),
+            DROPPED_IPV6_NS_INVALID
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt1 = ICMPv6NDOptDstLLAddr(lladdr="01:02:03:04:05:06")
+        // icmp6_opt2 = ICMPv6NDOptSrcLLAddr(lladdr="01:02:03:04:05:06")
+        // pkt = eth/ip6/icmp6/icmp6_opt1/icmp6_opt2
+        val sllaNotFirstOptionNsPkt =
+            "02030405060700010203040586DD6000000000283AFF200100000000" +
+                    "00000200001A1122334420010000000000000200001A334411228700" +
+                    "2FFF0000000020010000000000000200001A33441122020101020304" +
+                    "05060101010203040506"
+        // non-DAD with multiple options, SLLA in 2nd option -> pass
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(sllaNotFirstOptionNsPkt),
+            PASSED_IPV6_NS_NO_SLLA_OPTION
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt = ICMPv6NDOptDstLLAddr(lladdr="01:02:03:04:05:06")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val noSllaOptionNsPkt =
+            "02030405060700010203040586DD6000000000203AFF200100000000000002" +
+                    "00001A1122334420010000000000000200001A3344112287003A1400000000" +
+                    "20010000000000000200001A334411220201010203040506"
+        // non-DAD with one option but not SLLA -> pass
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(noSllaOptionNsPkt),
+            PASSED_IPV6_NS_NO_SLLA_OPTION
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="01:02:03:04:05:06")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val mcastMacSllaOptionNsPkt =
+            "02030405060700010203040586DD6000000000203AFF200100000000" +
+                    "00000200001A1122334420010000000000000200001A334411228700" +
+                    "3B140000000020010000000000000200001A33441122010101020304" +
+                    "0506"
+        // non-DAD, SLLA is multicast MAC -> drop
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(mcastMacSllaOptionNsPkt),
+            DROPPED_IPV6_NS_INVALID
+        )
+    }
+
+    // The APFv6 code path is only turned on in V+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testNaTransmit() {
+        val apfFilter = getApfFilter()
+        val lp = LinkProperties()
+        for (addr in hostIpv6Addresses) {
+            lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
+        }
+
+        apfFilter.setLinkProperties(lp)
+        val programCaptor = ArgumentCaptor.forClass(ByteArray::class.java)
+        verify(ipClientCallback, times(3)).installPacketFilter(programCaptor.capture())
+        val program = programCaptor.allValues.last()
+        val validIpv6Addresses = hostIpv6Addresses + hostAnycast6Addresses
+        for (addr in validIpv6Addresses) {
+            // unicast solicited NS request
+            val receivedUcastNsPacket = generateNsPacket(
+                senderMacAddress,
+                apfFilter.mHardwareAddress,
+                senderIpv6Address,
+                addr,
+                addr
+            )
+
+            verifyProgramRun(
+                apfFilter.mApfVersionSupported,
+                program,
+                receivedUcastNsPacket,
+                DROPPED_IPV6_NS_REPLIED_NON_DAD
+            )
+
+            val transmittedUcastPacket = ApfJniUtils.getTransmittedPacket()
+            val expectedUcastNaPacket = generateNaPacket(
+                apfFilter.mHardwareAddress,
+                senderMacAddress,
+                addr,
+                senderIpv6Address,
+                0xe0000000.toInt(), //  R=1, S=1, O=1
+                addr
+            )
+
+            assertContentEquals(
+                expectedUcastNaPacket,
+                transmittedUcastPacket
+            )
+
+            val solicitedMcastAddr = NetworkStackUtils.ipv6AddressToSolicitedNodeMulticast(
+                InetAddress.getByAddress(addr) as Inet6Address
+            )!!
+            val mcastDa = NetworkStackUtils.ipv6MulticastToEthernetMulticast(solicitedMcastAddr)
+                .toByteArray()
+
+            // multicast solicited NS request
+            var receivedMcastNsPacket = generateNsPacket(
+                senderMacAddress,
+                mcastDa,
+                senderIpv6Address,
+                solicitedMcastAddr.address,
+                addr
+            )
+
+            verifyProgramRun(
+                apfFilter.mApfVersionSupported,
+                program,
+                receivedMcastNsPacket,
+                DROPPED_IPV6_NS_REPLIED_NON_DAD
+            )
+
+            val transmittedMcastPacket = ApfJniUtils.getTransmittedPacket()
+            val expectedMcastNaPacket = generateNaPacket(
+                apfFilter.mHardwareAddress,
+                senderMacAddress,
+                addr,
+                senderIpv6Address,
+                0xe0000000.toInt(), // R=1, S=1, O=1
+                addr
+            )
+
+            assertContentEquals(
+                expectedMcastNaPacket,
+                transmittedMcastPacket
+            )
+        }
+    }
+
+    // The APFv6 code path is only turned on in V+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testNaTransmitWithTclass() {
+        // mock nd traffic class from /proc/sys/net/ipv6/conf/{ifname}/ndisc_tclass to 20
+        `when`(dependencies.getNdTrafficClass(any())).thenReturn(20)
+        val apfFilter = getApfFilter()
+        val lp = LinkProperties()
+        for (addr in hostIpv6Addresses) {
+            lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
+        }
+        apfFilter.setLinkProperties(lp)
+        val programCaptor = ArgumentCaptor.forClass(ByteArray::class.java)
+        verify(ipClientCallback, times(3)).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="02:03:04:05:06:07")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="ff02::1:ff44:1122", hlim=255, tc=20)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="00:01:02:03:04:05")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val hostMcastDstIpNsPkt =
+            "02030405060700010203040586DD6140000000203AFF2001000000000000" +
+                    "0200001A11223344FF0200000000000000000001FF4411228700952D0000" +
+                    "000020010000000000000200001A334411220101000102030405"
+        verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(hostMcastDstIpNsPkt),
+            DROPPED_IPV6_NS_REPLIED_NON_DAD
+        )
+
+        val transmitPkt = ApfJniUtils.getTransmittedPacket()
+        // Using scapy to generate IPv6 NA packet:
+        // eth = Ether(src="02:03:04:05:06:07", dst="00:01:02:03:04:05")
+        // ip6 = IPv6(src="2001::200:1a:3344:1122", dst="2001::200:1a:1122:3344", hlim=255, tc=20)
+        // icmp6 = ICMPv6ND_NA(tgt="2001::200:1a:3344:1122", R=1, S=1, O=1)
+        // icmp6_opt = ICMPv6NDOptDstLLAddr(lladdr="02:03:04:05:06:07")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val expectedNaPacket =
+            "00010203040502030405060786DD6140000000203AFF2001000000000000020" +
+                    "0001A3344112220010000000000000200001A1122334488005610E000000020" +
+                    "010000000000000200001A334411220201020304050607"
+        assertContentEquals(
+            HexDump.hexStringToByteArray(expectedNaPacket),
+            transmitPkt
+        )
+    }
+
+    @Test
+    fun testNdOffloadDisabled() {
+        val apfConfig = getDefaultConfig()
+        apfConfig.shouldHandleNdOffload = false
+        val apfFilter = getApfFilter(apfConfig)
+        val lp = LinkProperties()
+        for (addr in hostIpv6Addresses) {
+            lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
+        }
+
+        apfFilter.setLinkProperties(lp)
+        val programCaptor = ArgumentCaptor.forClass(ByteArray::class.java)
+        verify(ipClientCallback, times(3)).installPacketFilter(programCaptor.capture())
+        val program = programCaptor.allValues.last()
+        val validIpv6Addresses = hostIpv6Addresses + hostAnycast6Addresses
+        for (addr in validIpv6Addresses) {
+            // unicast solicited NS request
+            val receivedUcastNsPacket = generateNsPacket(
+                senderMacAddress,
+                apfFilter.mHardwareAddress,
+                senderIpv6Address,
+                addr,
+                addr
+            )
+
+            verifyProgramRun(
+                apfFilter.mApfVersionSupported,
+                program,
+                receivedUcastNsPacket,
+                PASSED_IPV6_ICMP
+            )
+
+            val solicitedMcastAddr = NetworkStackUtils.ipv6AddressToSolicitedNodeMulticast(
+                InetAddress.getByAddress(addr) as Inet6Address
+            )!!
+            val mcastDa = NetworkStackUtils.ipv6MulticastToEthernetMulticast(solicitedMcastAddr)
+                .toByteArray()
+
+            // multicast solicited NS request
+            var receivedMcastNsPacket = generateNsPacket(
+                senderMacAddress,
+                mcastDa,
+                senderIpv6Address,
+                solicitedMcastAddr.address,
+                addr
+            )
+
+            verifyProgramRun(
+                apfFilter.mApfVersionSupported,
+                program,
+                receivedMcastNsPacket,
+                PASSED_IPV6_ICMP
+            )
+        }
+    }
+
+    @Test
+    fun testApfProgramUpdate() {
+        val apfFilter = getApfFilter()
+        verify(ipClientCallback, times(2)).installPacketFilter(any())
+        // add IPv4 address, expect to have apf program update
+        val lp = LinkProperties()
+        val linkAddress = LinkAddress(InetAddress.getByAddress(hostIpv4Address), 24)
+        lp.addLinkAddress(linkAddress)
+        apfFilter.setLinkProperties(lp)
+        verify(ipClientCallback, times(3)).installPacketFilter(any())
+
+        // add the same IPv4 address, expect to have no apf program update
+        apfFilter.setLinkProperties(lp)
+        verify(ipClientCallback, times(3)).installPacketFilter(any())
+
+        // add IPv6 addresses, expect to have apf program update
+        for (addr in hostIpv6Addresses) {
+            lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
+        }
+
+        apfFilter.setLinkProperties(lp)
+        verify(ipClientCallback, times(4)).installPacketFilter(any())
+
+        // add the same IPv6 addresses, expect to have no apf program update
+        apfFilter.setLinkProperties(lp)
+        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())
+    }
+
+    @Test
+    fun testApfFilterInitializationCleanUpTheApfMemoryRegion() {
+        val apfFilter = getApfFilter()
+        val programCaptor = ArgumentCaptor.forClass(ByteArray::class.java)
+        verify(ipClientCallback, times(2)).installPacketFilter(programCaptor.capture())
+        val program = programCaptor.allValues.first()
+        assertContentEquals(ByteArray(4096) { 0 }, program)
+    }
+}
diff --git a/tests/unit/src/android/net/apf/ApfNewTest.kt b/tests/unit/src/android/net/apf/ApfGeneratorTest.kt
similarity index 62%
rename from tests/unit/src/android/net/apf/ApfNewTest.kt
rename to tests/unit/src/android/net/apf/ApfGeneratorTest.kt
index 6863fb9..1364af5 100644
--- a/tests/unit/src/android/net/apf/ApfNewTest.kt
+++ b/tests/unit/src/android/net/apf/ApfGeneratorTest.kt
@@ -15,31 +15,16 @@
  */
 package android.net.apf
 
-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
 import android.net.apf.ApfCounterTracker.Counter.CORRUPT_DNS_PACKET
-import android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_REQUEST_REPLIED
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_ETHERTYPE_NOT_ALLOWED
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_ETH_BROADCAST
-import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_NON_DHCP4
-import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_INVALID
-import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_OTHER_HOST
 import android.net.apf.ApfCounterTracker.Counter.PASSED_ALLOCATE_FAILURE
 import android.net.apf.ApfCounterTracker.Counter.PASSED_ARP
-import android.net.apf.ApfCounterTracker.Counter.PASSED_ARP_REQUEST
-import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV4
-import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV4_FROM_DHCPV4_SERVER
-import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_ICMP
-import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_MULTIPLE_OPTIONS
-import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_NO_ADDRESS
 import android.net.apf.ApfCounterTracker.Counter.PASSED_TRANSMIT_FAILURE
 import android.net.apf.ApfCounterTracker.Counter.TOTAL_PACKETS
-import android.net.apf.ApfFilter.Dependencies
+import android.net.apf.ApfTestHelpers.Companion.decodeCountersIntoMap
+import android.net.apf.ApfTestHelpers.Companion.verifyProgramRun
 import android.net.apf.ApfTestUtils.DROP
 import android.net.apf.ApfTestUtils.MIN_PKT_SIZE
 import android.net.apf.ApfTestUtils.PASS
@@ -55,142 +40,53 @@
 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
 import com.android.net.module.util.Struct
-import com.android.net.module.util.arp.ArpPacket
 import com.android.net.module.util.structs.EthernetHeader
 import com.android.net.module.util.structs.Ipv4Header
 import com.android.net.module.util.structs.UdpHeader
-import com.android.networkstack.metrics.NetworkQuirkMetrics
 import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
-import java.net.InetAddress
 import java.nio.ByteBuffer
 import kotlin.test.assertContentEquals
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
-import org.junit.After
-import org.junit.Before
 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
 
 const val ETH_HLEN = 14
 const val IPV4_HLEN = 20
 const val IPPROTO_UDP = 17
 
 /**
- * Tests for APF instructions.
+ * Tests for APF generator instructions.
  */
 @RunWith(DevSdkIgnoreRunner::class)
 @SmallTest
-class ApfNewTest {
+class ApfGeneratorTest {
 
     @get:Rule val ignoreRule = DevSdkIgnoreRule()
 
-    @Mock private lateinit var context: Context
-
-    @Mock private lateinit var metrics: NetworkQuirkMetrics
-
-    @Mock private lateinit var dependencies: Dependencies
-
-    @Mock private lateinit var ipClientCallback: IpClientCallbacksWrapper
-
-    private val defaultMaximumApfProgramSize = 2048
-
-    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 ramSize = 2048
+    private val clampSize = 2048
 
     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)
-            .map { it.toByte() }.toByteArray()
-    private val senderMacAddress = intArrayOf(0x01, 0x22, 0x33, 0x44, 0x55, 0x66)
-        .map { it.toByte() }.toByteArray()
-    private val hostIpv6Addresses = listOf(
-        // 2001::200:1a:3344:1122
-        intArrayOf(0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x02, 0, 0, 0x1a, 0x33, 0x44, 0x11, 0x22)
-            .map{ it.toByte() }.toByteArray(),
-        // 2001::100:1b:4455:6677
-        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)
-            .map{ it.toByte() }.toByteArray()
-    )
-    private val hostMulticastMacAddresses = listOf(
-            // 33:33:00:00:00:01
-            intArrayOf(0x33, 0x33, 0, 0, 0, 1).map { it.toByte() }.toByteArray(),
-            // 33:33:ff:44:11:22
-            intArrayOf(0x33, 0x33, 0xff, 0x44, 0x11, 0x22).map { it.toByte() }.toByteArray(),
-            // 33:33:ff:55:66:77
-            intArrayOf(0x33, 0x33, 0xff, 0x55, 0x66, 0x77).map { it.toByte() }.toByteArray(),
-            // 33:33:ff:bb:cc:dd
-            intArrayOf(0x33, 0x33, 0xff, 0xbb, 0xcc, 0xdd).map { it.toByte() }.toByteArray(),
-    )
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-        // mock anycast6 address from /proc/net/anycast6
-        `when`(dependencies.getAnycast6Addresses(any())).thenReturn(hostAnycast6Addresses)
-        // mock host mac address and ethernet multicast addresses from /proc/net/dev_mcast
-        `when`(dependencies.getEtherMulticastAddresses(any())).thenReturn(hostMulticastMacAddresses)
-    }
-
-    @After
-    fun tearDown() {
-        Mockito.framework().clearInlineMocks()
-        ApfJniUtils.resetTransmittedPacketMemory()
-    }
 
     @Test
     fun testDataInstructionMustComeFirst() {
-        var gen = ApfV6Generator(defaultMaximumApfProgramSize)
+        var gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
         gen.addAllocateR0()
         assertFailsWith<IllegalInstructionException> { gen.addData(ByteArray(3) { 0x01 }) }
     }
 
     @Test
     fun testApfInstructionEncodingSizeCheck() {
-        var gen = ApfV6Generator(defaultMaximumApfProgramSize)
+        var gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
         assertFailsWith<IllegalArgumentException> {
-            ApfV6Generator(ByteArray(65536) { 0x01 }, defaultMaximumApfProgramSize)
+            ApfV6Generator(ByteArray(65536) { 0x01 }, APF_VERSION_6, ramSize, clampSize)
         }
         assertFailsWith<IllegalArgumentException> { gen.addAllocate(65536) }
         assertFailsWith<IllegalArgumentException> { gen.addAllocate(-1) }
@@ -439,7 +335,7 @@
             )
         }
 
-        val v4gen = ApfV4Generator(APF_VERSION_3)
+        val v4gen = ApfV4Generator(APF_VERSION_3, ramSize, clampSize)
         assertFailsWith<IllegalArgumentException> { v4gen.addCountAndDrop(PASSED_ARP) }
         assertFailsWith<IllegalArgumentException> { v4gen.addCountAndPass(DROPPED_ETH_BROADCAST) }
         assertFailsWith<IllegalArgumentException> {
@@ -517,7 +413,7 @@
     fun testValidateDnsNames() {
         // '%' is a valid label character in mDNS subtype
         // byte == 0xff means it is a '*' wildcard, which is a valid encoding.
-        val program = ApfV6Generator(defaultMaximumApfProgramSize).addJumpIfPktAtR0ContainDnsQ(
+        val program = ApfV6Generator(ramSize, ramSize, clampSize).addJumpIfPktAtR0ContainDnsQ(
                 byteArrayOf(1, '%'.code.toByte(), 0, 0),
                 1,
                 DROP_LABEL
@@ -529,7 +425,7 @@
 
     @Test
     fun testApfInstructionsEncoding() {
-        val v4gen = ApfV4Generator(APF_VERSION_2)
+        val v4gen = ApfV4Generator(APF_VERSION_2, ramSize, clampSize)
         v4gen.addPass()
         var program = v4gen.generate()
         // encoding PASS opcode: opcode=0, imm_len=0, R=0
@@ -542,7 +438,7 @@
                 ApfJniUtils.disassembleApf(program).map { it.trim() }
         )
 
-        var gen = ApfV6Generator(defaultMaximumApfProgramSize)
+        var gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
         gen.addDrop()
         program = gen.generate().skipDataAndDebug()
         // encoding DROP opcode: opcode=0, imm_len=0, R=1
@@ -555,7 +451,7 @@
                 ApfJniUtils.disassembleApf(program).map { it.trim() }
         )
 
-        gen = ApfV6Generator(defaultMaximumApfProgramSize)
+        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
         gen.addCountAndPass(129)
         program = gen.generate().skipDataAndDebug()
         // encoding COUNT(PASS) opcode: opcode=0, imm_len=size_of(imm), R=0, imm=counterNumber
@@ -571,7 +467,7 @@
                 ApfJniUtils.disassembleApf(program).map { it.trim() }
         )
 
-        gen = ApfV6Generator(defaultMaximumApfProgramSize)
+        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
         gen.addCountAndDrop(1000)
         program = gen.generate().skipDataAndDebug()
         // encoding COUNT(DROP) opcode: opcode=0, imm_len=size_of(imm), R=1, imm=counterNumber
@@ -588,7 +484,7 @@
                 ApfJniUtils.disassembleApf(program).map { it.trim() }
         )
 
-        gen = ApfV6Generator(defaultMaximumApfProgramSize)
+        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
         gen.addCountAndPass(PASSED_ARP)
         program = gen.generate().skipDataAndDebug()
         // encoding COUNT(PASS) opcode: opcode=0, imm_len=size_of(imm), R=0, imm=counterNumber
@@ -604,7 +500,7 @@
                 ApfJniUtils.disassembleApf(program).map { it.trim() }
         )
 
-        gen = ApfV6Generator(defaultMaximumApfProgramSize)
+        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
         gen.addCountAndDrop(DROPPED_ETHERTYPE_NOT_ALLOWED)
         program = gen.generate().skipDataAndDebug()
         // encoding COUNT(DROP) opcode: opcode=0, imm_len=size_of(imm), R=1, imm=counterNumber
@@ -616,11 +512,11 @@
                 program
         )
         assertContentEquals(
-                listOf("0: drop        counter=43"),
+                listOf("0: drop        counter=46"),
                 ApfJniUtils.disassembleApf(program).map { it.trim() }
         )
 
-        gen = ApfV6Generator(defaultMaximumApfProgramSize)
+        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
         gen.addAllocateR0()
         gen.addAllocate(1500)
         program = gen.generate().skipDataAndDebug()
@@ -642,7 +538,7 @@
                 "2: allocate    1500"
         ), ApfJniUtils.disassembleApf(program).map { it.trim() })
 
-        gen = ApfV6Generator(defaultMaximumApfProgramSize)
+        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
         gen.addTransmitWithoutChecksum()
         gen.addTransmitL4(30, 40, 50, 256, true)
         program = gen.generate().skipDataAndDebug()
@@ -659,25 +555,25 @@
         ), ApfJniUtils.disassembleApf(program).map { it.trim() })
 
         val largeByteArray = ByteArray(256) { 0x01 }
-        gen = ApfV6Generator(largeByteArray, defaultMaximumApfProgramSize)
+        gen = ApfV6Generator(largeByteArray, APF_VERSION_6, ramSize, clampSize)
         program = gen.generate()
         assertContentEquals(
                 byteArrayOf(
                         encodeInstruction(opcode = 14, immLength = 2, register = 1), 1, 0
                 ) + largeByteArray + byteArrayOf(
-                        encodeInstruction(opcode = 21, immLength = 1, register = 0), 48, 6, 25
+                        encodeInstruction(opcode = 21, immLength = 1, register = 0), 48, 6, 13
                 ),
                 program
         )
         assertContentEquals(
                 listOf(
                         "0: data        256, " + "01".repeat(256),
-                        "259: debugbuf    size=1561"
+                        "259: debugbuf    size=1549"
                 ),
                 ApfJniUtils.disassembleApf(program).map { it.trim() }
         )
 
-        gen = ApfV6Generator(defaultMaximumApfProgramSize)
+        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
         gen.addWriteU8(0x01)
         gen.addWriteU16(0x0102)
         gen.addWriteU32(0x01020304)
@@ -718,7 +614,7 @@
                 "35: write       0xfffefdfc"
         ), ApfJniUtils.disassembleApf(program).map { it.trim() })
 
-        gen = ApfV6Generator(defaultMaximumApfProgramSize)
+        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
         gen.addWriteU8(R0)
         gen.addWriteU16(R0)
         gen.addWriteU32(R0)
@@ -743,7 +639,7 @@
                 "10: ewrite4     r1"
         ), ApfJniUtils.disassembleApf(program).map { it.trim() })
 
-        gen = ApfV6Generator(defaultMaximumApfProgramSize)
+        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
         gen.addDataCopy(0, 10)
         gen.addDataCopy(1, 5)
         gen.addPacketCopy(1000, 255)
@@ -760,7 +656,7 @@
                 "5: pktcopy     src=1000, len=255"
         ), ApfJniUtils.disassembleApf(program).map { it.trim() })
 
-        gen = ApfV6Generator(defaultMaximumApfProgramSize)
+        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
         gen.addDataCopyFromR0(5)
         gen.addPacketCopyFromR0(5)
         gen.addDataCopyFromR0LenR1()
@@ -779,7 +675,7 @@
                 "8: epktcopy     src=r0, len=r1"
         ), ApfJniUtils.disassembleApf(program).map{ it.trim() })
 
-        gen = ApfV6Generator(defaultMaximumApfProgramSize)
+        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
         gen.addJumpIfBytesAtR0Equal(byteArrayOf('a'.code.toByte()), ApfV4Generator.DROP_LABEL)
         program = gen.generate().skipDataAndDebug()
         assertContentEquals(byteArrayOf(
@@ -793,7 +689,7 @@
         ), ApfJniUtils.disassembleApf(program).map{ it.trim() })
 
         val qnames = byteArrayOf(1, 'A'.code.toByte(), 1, 'B'.code.toByte(), 0, 0)
-        gen = ApfV6Generator(defaultMaximumApfProgramSize)
+        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
         gen.addJumpIfPktAtR0DoesNotContainDnsQ(qnames, 0x0c, ApfV4Generator.DROP_LABEL)
         gen.addJumpIfPktAtR0ContainDnsQ(qnames, 0x0c, ApfV4Generator.DROP_LABEL)
         program = gen.generate().skipDataAndDebug()
@@ -807,7 +703,7 @@
                 "10: jdnsqeq     r0, DROP, 12, (1)A(1)B(0)(0)"
         ), ApfJniUtils.disassembleApf(program).map{ it.trim() })
 
-        gen = ApfV6Generator(defaultMaximumApfProgramSize)
+        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
         gen.addJumpIfPktAtR0DoesNotContainDnsQSafe(qnames, 0x0c, ApfV4Generator.DROP_LABEL)
         gen.addJumpIfPktAtR0ContainDnsQSafe(qnames, 0x0c, ApfV4Generator.DROP_LABEL)
         program = gen.generate().skipDataAndDebug()
@@ -821,7 +717,7 @@
                 "10: jdnsqeqsafe r0, DROP, 12, (1)A(1)B(0)(0)"
         ), ApfJniUtils.disassembleApf(program).map{ it.trim() })
 
-        gen = ApfV6Generator(defaultMaximumApfProgramSize)
+        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
         gen.addJumpIfPktAtR0DoesNotContainDnsA(qnames, ApfV4Generator.DROP_LABEL)
         gen.addJumpIfPktAtR0ContainDnsA(qnames, ApfV4Generator.DROP_LABEL)
         program = gen.generate().skipDataAndDebug()
@@ -835,7 +731,7 @@
                 "9: jdnsaeq     r0, DROP, (1)A(1)B(0)(0)"
         ), ApfJniUtils.disassembleApf(program).map{ it.trim() })
 
-        gen = ApfV6Generator(defaultMaximumApfProgramSize)
+        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
         gen.addJumpIfPktAtR0DoesNotContainDnsASafe(qnames, ApfV4Generator.DROP_LABEL)
         gen.addJumpIfPktAtR0ContainDnsASafe(qnames, ApfV4Generator.DROP_LABEL)
         program = gen.generate().skipDataAndDebug()
@@ -849,7 +745,7 @@
                 "9: jdnsaeqsafe r0, DROP, (1)A(1)B(0)(0)"
         ), ApfJniUtils.disassembleApf(program).map{ it.trim() })
 
-        gen = ApfV6Generator(defaultMaximumApfProgramSize)
+        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
         gen.addJumpIfOneOf(R1, List(32) { (it + 1).toLong() }.toSet(), DROP_LABEL)
         gen.addJumpIfOneOf(R0, setOf(0, 257, 65536), DROP_LABEL)
         gen.addJumpIfNoneOf(R0, setOf(1, 2, 3), DROP_LABEL)
@@ -863,7 +759,7 @@
                 encodeInstruction(21, 1, 0), 47, 1, 9, 1, 2, 3
         ), program)
 
-        gen = ApfV6Generator(defaultMaximumApfProgramSize)
+        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
         gen.addJumpIfOneOf(R0, setOf(0, 128, 256, 65536), DROP_LABEL)
         gen.addJumpIfNoneOf(R1, setOf(0, 128, 256, 65536), DROP_LABEL)
         program = gen.generate().skipDataAndDebug()
@@ -872,7 +768,7 @@
                 "20: jnoneof     r1, DROP, { 0, 128, 256, 65536 }"
         ), ApfJniUtils.disassembleApf(program).map{ it.trim() })
 
-        gen = ApfV6Generator(defaultMaximumApfProgramSize)
+        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
         gen.addJumpIfBytesAtR0EqualsAnyOf(listOf(byteArrayOf(1, 2), byteArrayOf(3, 4)), DROP_LABEL)
         gen.addJumpIfBytesAtR0EqualNoneOf(listOf(byteArrayOf(1, 2), byteArrayOf(3, 4)), DROP_LABEL)
         gen.addJumpIfBytesAtR0EqualNoneOf(listOf(byteArrayOf(1, 1), byteArrayOf(1, 1)), DROP_LABEL)
@@ -894,7 +790,7 @@
 
     @Test
     fun testWriteToTxBuffer() {
-        var program = ApfV6Generator(defaultMaximumApfProgramSize)
+        var program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addAllocate(14)
                 .addWriteU8(0x01)
                 .addWriteU16(0x0203)
@@ -921,7 +817,7 @@
 
     @Test
     fun testCopyToTxBuffer() {
-        var program = ApfV6Generator(byteArrayOf(33, 34, 35), defaultMaximumApfProgramSize)
+        var program = ApfV6Generator(byteArrayOf(33, 34, 35), APF_VERSION_6, ramSize, clampSize)
                 .addAllocate(14)
                 .addDataCopy(3, 2) // arg1=src, arg2=len
                 .addDataCopy(5, 1) // arg1=src, arg2=len
@@ -948,7 +844,7 @@
 
     @Test
     fun testCopyContentToTxBuffer() {
-        val program = ApfV6Generator(defaultMaximumApfProgramSize)
+        val program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addAllocate(18)
                 .addDataCopy(HexDump.hexStringToByteArray("112233445566"))
                 .addDataCopy(HexDump.hexStringToByteArray("223344"))
@@ -958,7 +854,7 @@
                 .generate()
         assertContentEquals(listOf(
                 "0: data        9, 112233445566778899",
-                "12: debugbuf    size=1788",
+                "12: debugbuf    size=1776",
                 "16: allocate    18",
                 "20: datacopy    src=3, len=6",
                 "23: datacopy    src=4, len=3",
@@ -973,18 +869,18 @@
 
     @Test
     fun testPassDrop() {
-        var program = ApfV6Generator(defaultMaximumApfProgramSize)
+        var program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addDrop()
                 .addPass()
                 .generate()
         assertDrop(APF_VERSION_6, program, testPacket)
 
-        program = ApfV6Generator(defaultMaximumApfProgramSize)
+        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addCountAndDrop(Counter.DROPPED_ETH_BROADCAST)
                 .generate()
         verifyProgramRun(APF_VERSION_6, program, testPacket, DROPPED_ETH_BROADCAST)
 
-        program = ApfV6Generator(defaultMaximumApfProgramSize)
+        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addCountAndPass(Counter.PASSED_ARP)
                 .generate()
         verifyProgramRun(APF_VERSION_6, program, testPacket, PASSED_ARP)
@@ -994,11 +890,11 @@
     fun testLoadStoreCounter() {
         doTestLoadStoreCounter (
                 { mutableMapOf() },
-                { ApfV4Generator(APF_VERSION_3) }
+                { ApfV4Generator(APF_VERSION_3, ramSize, clampSize) }
         )
         doTestLoadStoreCounter (
                 { mutableMapOf(TOTAL_PACKETS to 1) },
-                { ApfV6Generator(defaultMaximumApfProgramSize) }
+                { ApfV6Generator(APF_VERSION_6, ramSize, clampSize) }
         )
     }
 
@@ -1021,7 +917,7 @@
     @Test
     fun testV4CountAndPassDropCompareR0() {
         doTestCountAndPassDropCompareR0(
-                getGenerator = { ApfV4Generator(APF_VERSION_3) },
+                getGenerator = { ApfV4Generator(APF_VERSION_3, ramSize, clampSize) },
                 incTotal = false
         )
     }
@@ -1029,7 +925,7 @@
     @Test
     fun testV6CountAndPassDropCompareR0() {
         doTestCountAndPassDropCompareR0(
-                getGenerator = { ApfV6Generator(defaultMaximumApfProgramSize) },
+                getGenerator = { ApfV6Generator(APF_VERSION_6, ramSize, clampSize) },
                 incTotal = true
         )
     }
@@ -1341,72 +1237,9 @@
         verifyProgramRun(APF_VERSION_6, program, testPacket, PASSED_ARP, incTotal = incTotal)
     }
 
-    private fun doTestEtherTypeAllowListFilter(apfVersion: Int) {
-        val programCaptor = ArgumentCaptor.forClass(ByteArray::class.java)
-        val apfFilter =
-            ApfFilter(
-                context,
-                getDefaultConfig(apfVersion),
-                ifParams,
-                ipClientCallback,
-                metrics,
-                dependencies
-            )
-        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")
-        //   ip = IP(src="192.168.1.1")
-        //   udp = UDP(sport=5353, dport=5353)
-        //   dns = DNS(qd=DNSQR(qtype="PTR", qname="a.local"))
-        //   p = eth/ip/udp/dns
-        val mdnsPkt = "01005e0000fbe89f806660bb080045000035000100004011d812c0a80101e00000f" +
-                      "b14e914e900214d970000010000010000000000000161056c6f63616c00000c0001"
-        verifyProgramRun(APF_VERSION_6, program, HexDump.hexStringToByteArray(mdnsPkt), PASSED_IPV4)
-
-        // Using scapy to generate RA packet:
-        //  eth = Ether(src="E8:9F:80:66:60:BB", dst="33:33:00:00:00:01")
-        //  ip6 = IPv6(src="fe80::1", dst="ff02::1")
-        //  icmp6 = ICMPv6ND_RA(routerlifetime=3600, retranstimer=3600)
-        //  p = eth/ip6/icmp6
-        val raPkt = "333300000001e89f806660bb86dd6000000000103afffe800000000000000000000000" +
-                    "000001ff0200000000000000000000000000018600600700080e100000000000000e10"
-        verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(raPkt),
-                PASSED_IPV6_ICMP
-        )
-
-        // Using scapy to generate ethernet packet with type 0x88A2:
-        //  p = Ether(type=0x88A2)/Raw(load="01")
-        val ethPkt = "ffffffffffff047bcb463fb588a23031"
-        verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(ethPkt),
-                DROPPED_ETHERTYPE_NOT_ALLOWED
-        )
-
-        apfFilter.shutdown()
-    }
-
-    @Test
-    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-    fun testV4EtherTypeAllowListFilter() {
-        doTestEtherTypeAllowListFilter(APF_VERSION_3)
-    }
-
-    @Test
-    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-    fun testV6EtherTypeAllowListFilter() {
-        doTestEtherTypeAllowListFilter(APF_VERSION_6)
-    }
-
     @Test
     fun testV4CountAndPassDrop() {
-        var program = ApfV4Generator(APF_VERSION_3)
+        var program = ApfV4Generator(APF_VERSION_3, ramSize, clampSize)
                 .addCountAndDrop(Counter.DROPPED_ETH_BROADCAST)
                 .addCountTrampoline()
                 .generate()
@@ -1418,7 +1251,7 @@
                 incTotal = false
         )
 
-        program = ApfV4Generator(APF_VERSION_3)
+        program = ApfV4Generator(APF_VERSION_3, ramSize, clampSize)
                 .addCountAndPass(Counter.PASSED_ARP)
                 .addCountTrampoline()
                 .generate()
@@ -1427,7 +1260,7 @@
 
     @Test
     fun testV2CountAndPassDrop() {
-        var program = ApfV4Generator(APF_VERSION_2)
+        var program = ApfV4Generator(APF_VERSION_2, ramSize, clampSize)
                 .addCountAndDrop(Counter.DROPPED_ETH_BROADCAST)
                 .addCountTrampoline()
                 .generate()
@@ -1435,7 +1268,7 @@
         assertVerdict(APF_VERSION_6, DROP, program, testPacket, dataRegion)
         assertContentEquals(ByteArray(Counter.totalSize()) { 0 }, dataRegion)
 
-        program = ApfV4Generator(APF_VERSION_2)
+        program = ApfV4Generator(APF_VERSION_2, ramSize, clampSize)
                 .addCountAndPass(PASSED_ARP)
                 .addCountTrampoline()
                 .generate()
@@ -1446,7 +1279,7 @@
 
     @Test
     fun testAllocateFailure() {
-        val program = ApfV6Generator(defaultMaximumApfProgramSize)
+        val program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 // allocate size: 65535 > sizeof(apf_test_buffer): 1514, trigger allocate failure.
                 .addAllocate(65535)
                 .addDrop()
@@ -1456,7 +1289,7 @@
 
     @Test
     fun testTransmitFailure() {
-        val program = ApfV6Generator(defaultMaximumApfProgramSize)
+        val program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addAllocate(14)
                 // len: 13 is less than ETH_HLEN, trigger transmit failure.
                 .addLoadImmediate(R0, 13)
@@ -1492,7 +1325,7 @@
                 0x00, 0x00, 0x01, 0x80, 0x01, 0x00, 0x00, 0x00, 0x78, 0x00, 0x04, 0xc0, 0xa8, 0x01,
                 0x09,
         ).map { it.toByte() }.toByteArray()
-        val program = ApfV6Generator(etherIpv4UdpPacket, defaultMaximumApfProgramSize)
+        val program = ApfV6Generator(etherIpv4UdpPacket, APF_VERSION_6, ramSize, clampSize)
                 .addAllocate(etherIpv4UdpPacket.size)
                 .addDataCopy(3, etherIpv4UdpPacket.size) // arg1=src, arg2=len
                 .addTransmitL4(
@@ -1538,28 +1371,28 @@
                 0x00, 0x01, 0x00, 0x01 // type = A, class = 0x0001
         ).map { it.toByte() }.toByteArray()
 
-        var program = ApfV6Generator(defaultMaximumApfProgramSize)
+        var program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfPktAtR0ContainDnsQ(needlesMatch, 0x01, DROP_LABEL) // arg2=qtype
                 .addPass()
                 .generate()
         assertDrop(APF_VERSION_6, program, udpPayload)
 
-        program = ApfV6Generator(defaultMaximumApfProgramSize)
+        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfPktAtR0ContainDnsQSafe(needlesMatch, 0x01, DROP_LABEL)
                 .addPass()
                 .generate()
         assertDrop(APF_VERSION_6, program, udpPayload)
 
-        program = ApfV6Generator(defaultMaximumApfProgramSize)
+        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfPktAtR0DoesNotContainDnsQ(needlesMatch, 0x01, DROP_LABEL) // arg2=qtype
                 .addPass()
                 .generate()
         assertPass(APF_VERSION_6, program, udpPayload)
 
-        program = ApfV6Generator(defaultMaximumApfProgramSize)
+        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfPktAtR0DoesNotContainDnsQSafe(needlesMatch, 0x01, DROP_LABEL) // arg2=qtype
                 .addPass()
@@ -1581,14 +1414,14 @@
                 0x00, 0x01, 0x00, 0x01 // type = A, class = 0x0001
         ).map { it.toByte() }.toByteArray()
 
-        program = ApfV6Generator(defaultMaximumApfProgramSize)
+        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfPktAtR0ContainDnsQ(needlesMatch, 0x01, DROP_LABEL) // arg2=qtype
                 .addPass()
                 .generate()
         verifyProgramRun(APF_VERSION_6, program, badUdpPayload, CORRUPT_DNS_PACKET, result = DROP)
 
-        program = ApfV6Generator(defaultMaximumApfProgramSize)
+        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfPktAtR0ContainDnsQSafe(needlesMatch, 0x01, DROP_LABEL) // arg2=qtype
                 .addPass()
@@ -1627,28 +1460,28 @@
                 0x00, 0x04, 0xc0, 0xa8, 0x01, 0x09 // rdlengh = 4, rdata = 192.168.1.9
         ).map { it.toByte() }.toByteArray()
 
-        var program = ApfV6Generator(defaultMaximumApfProgramSize)
+        var program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfPktAtR0ContainDnsA(needlesMatch, DROP_LABEL)
                 .addPass()
                 .generate()
         assertDrop(APF_VERSION_6, program, udpPayload)
 
-        program = ApfV6Generator(defaultMaximumApfProgramSize)
+        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfPktAtR0ContainDnsASafe(needlesMatch, DROP_LABEL)
                 .addPass()
                 .generate()
         assertDrop(APF_VERSION_6, program, udpPayload)
 
-        program = ApfV6Generator(defaultMaximumApfProgramSize)
+        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfPktAtR0DoesNotContainDnsA(needlesMatch, DROP_LABEL)
                 .addPass()
                 .generate()
         assertPass(APF_VERSION_6, program, udpPayload)
 
-        program = ApfV6Generator(defaultMaximumApfProgramSize)
+        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfPktAtR0DoesNotContainDnsASafe(needlesMatch, DROP_LABEL)
                 .addPass()
@@ -1674,14 +1507,14 @@
                 0x00, 0x04, 0xc0, 0xa8, 0x01, 0x09 // rdlengh = 4, rdata = 192.168.1.9
         ).map { it.toByte() }.toByteArray()
 
-        program = ApfV6Generator(defaultMaximumApfProgramSize)
+        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfPktAtR0ContainDnsA(needlesMatch, DROP_LABEL)
                 .addPass()
                 .generate()
         verifyProgramRun(APF_VERSION_6, program, badUdpPayload, CORRUPT_DNS_PACKET, result = DROP)
 
-        program = ApfV6Generator(defaultMaximumApfProgramSize)
+        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfPktAtR0ContainDnsASafe(needlesMatch, DROP_LABEL)
                 .addPass()
@@ -1698,7 +1531,7 @@
 
     @Test
     fun testJumpMultipleByteSequencesMatch() {
-        var program = ApfV6Generator(defaultMaximumApfProgramSize)
+        var program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfBytesAtR0EqualsAnyOf(
                         listOf(byteArrayOf(1, 2, 3), byteArrayOf(6, 5, 4)),
@@ -1708,7 +1541,7 @@
                 .generate()
         assertDrop(APF_VERSION_6, program, testPacket)
 
-        program = ApfV6Generator(defaultMaximumApfProgramSize)
+        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addLoadImmediate(R0, 2)
                 .addJumpIfBytesAtR0EqualsAnyOf(
                         listOf(byteArrayOf(1, 2, 3), byteArrayOf(6, 5, 4)),
@@ -1718,7 +1551,7 @@
                 .generate()
         assertPass(APF_VERSION_6, program, testPacket)
 
-        program = ApfV6Generator(defaultMaximumApfProgramSize)
+        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addLoadImmediate(R0, 1)
                 .addJumpIfBytesAtR0EqualNoneOf(
                         listOf(byteArrayOf(1, 2, 3), byteArrayOf(6, 5, 4)),
@@ -1728,7 +1561,7 @@
                 .generate()
         assertDrop(APF_VERSION_6, program, testPacket)
 
-        program = ApfV6Generator(defaultMaximumApfProgramSize)
+        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfBytesAtR0EqualNoneOf(
                         listOf(byteArrayOf(1, 2, 3), byteArrayOf(6, 5, 4)),
@@ -1741,28 +1574,28 @@
 
     @Test
     fun testJumpOneOf() {
-        var program = ApfV6Generator(defaultMaximumApfProgramSize)
+        var program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addLoadImmediate(R0, 255)
                 .addJumpIfOneOf(R0, setOf(1, 2, 3, 128, 255), DROP_LABEL)
                 .addPass()
                 .generate()
         assertDrop(APF_VERSION_6, program, testPacket)
 
-        program = ApfV6Generator(defaultMaximumApfProgramSize)
+        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addLoadImmediate(R0, 254)
                 .addJumpIfOneOf(R0, setOf(1, 2, 3, 128, 255), DROP_LABEL)
                 .addPass()
                 .generate()
         assertPass(APF_VERSION_6, program, testPacket)
 
-        program = ApfV6Generator(defaultMaximumApfProgramSize)
+        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addLoadImmediate(R0, 254)
                 .addJumpIfNoneOf(R0, setOf(1, 2, 3, 128, 255), DROP_LABEL)
                 .addPass()
                 .generate()
         assertDrop(APF_VERSION_6, program, testPacket)
 
-        program = ApfV6Generator(defaultMaximumApfProgramSize)
+        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addLoadImmediate(R0, 255)
                 .addJumpIfNoneOf(R0, setOf(1, 2, 3, 128, 255), DROP_LABEL)
                 .addPass()
@@ -1772,575 +1605,16 @@
 
     @Test
     fun testDebugBuffer() {
-        val program = ApfV6Generator(defaultMaximumApfProgramSize)
+        val program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
                 .addLoad8(R0, 255)
                 .generate()
-        val dataRegion = ByteArray(defaultMaximumApfProgramSize - program.size) { 0 }
+        val dataRegion = ByteArray(ramSize - program.size) { 0 }
 
         assertVerdict(APF_VERSION_6, PASS, program, testPacket, dataRegion)
         // offset 3 in the data region should contain if the interpreter is APFv6 mode or not
         assertEquals(1, dataRegion[3])
     }
 
-    @Test
-    fun testIPv4PacketFilterOnV6OnlyNetwork() {
-        val apfFilter =
-            ApfFilter(
-                context,
-                getDefaultConfig(),
-                ifParams,
-                ipClientCallback,
-                metrics,
-                dependencies
-        )
-        apfFilter.updateClatInterfaceState(true)
-        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")
-        //   ip = IP(src="192.168.1.1")
-        //   udp = UDP(sport=5353, dport=5353)
-        //   dns = DNS(qd=DNSQR(qtype="PTR", qname="a.local"))
-        //   p = eth/ip/udp/dns
-        val mdnsPkt = "01005e0000fbe89f806660bb080045000035000100004011d812c0a80101e00000f" +
-                      "b14e914e900214d970000010000010000000000000161056c6f63616c00000c0001"
-        verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(mdnsPkt),
-                DROPPED_IPV4_NON_DHCP4
-        )
-
-        // Using scapy to generate DHCP4 offer packet:
-        //   ether = Ether(src='00:11:22:33:44:55', dst='ff:ff:ff:ff:ff:ff')
-        //   ip = IP(src='192.168.1.1', dst='255.255.255.255')
-        //   udp = UDP(sport=67, dport=68)
-        //   bootp = BOOTP(op=2,
-        //                 yiaddr='192.168.1.100',
-        //                 siaddr='192.168.1.1',
-        //                 chaddr=b'\x00\x11\x22\x33\x44\x55')
-        //   dhcp_options = [('message-type', 'offer'),
-        //                   ('server_id', '192.168.1.1'),
-        //                   ('subnet_mask', '255.255.255.0'),
-        //                   ('router', '192.168.1.1'),
-        //                   ('lease_time', 86400),
-        //                   ('name_server', '8.8.8.8'),
-        //                   'end']
-        //   dhcp = DHCP(options=dhcp_options)
-        //   dhcp_offer_packet = ether/ip/udp/bootp/dhcp
-        val dhcp4Pkt = "ffffffffffff00112233445508004500012e000100004011b815c0a80101ffffffff0043" +
-                       "0044011a5ffc02010600000000000000000000000000c0a80164c0a80101000000000011" +
-                       "223344550000000000000000000000000000000000000000000000000000000000000000" +
-                       "000000000000000000000000000000000000000000000000000000000000000000000000" +
-                       "000000000000000000000000000000000000000000000000000000000000000000000000" +
-                       "000000000000000000000000000000000000000000000000000000000000000000000000" +
-                       "000000000000000000000000000000000000000000000000000000000000000000000000" +
-                       "0000000000000000000000000000000000000000000000000000638253633501023604c0" +
-                       "a801010104ffffff000304c0a80101330400015180060408080808ff"
-        verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(dhcp4Pkt),
-                PASSED_IPV4_FROM_DHCPV4_SERVER
-        )
-
-        // Using scapy to generate DHCP4 offer packet:
-        //   eth = Ether(src="E8:9F:80:66:60:BB", dst="01:00:5E:00:00:FB")
-        //   ip = IP(src="192.168.1.10", dst="192.168.1.20")  # IPv4
-        //   udp = UDP(sport=12345, dport=53)
-        //   dns = DNS(qd=DNSQR(qtype="PTR", qname="a.local"))
-        //   pkt = eth / ip / udp / dns
-        //   fragments = fragment(pkt, fragsize=30)
-        //   fragments[1]
-        val fragmentedUdpPkt = "01005e0000fbe89f806660bb08004500001d000100034011f75dc0a8010ac0a8" +
-                               "01146f63616c00000c0001"
-        verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(fragmentedUdpPkt),
-                DROPPED_IPV4_NON_DHCP4
-        )
-        apfFilter.shutdown()
-    }
-
-    // The APFv6 code path is only turned on in V+
-    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-    @Test
-    fun testArpTransmit() {
-        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)
-        apfFilter.setLinkProperties(lp)
-        val programCaptor = ArgumentCaptor.forClass(ByteArray::class.java)
-        verify(ipClientCallback, times(3)).installPacketFilter(programCaptor.capture())
-        val program = programCaptor.value
-        val receivedArpPacketBuf = ArpPacket.buildArpPacket(
-                arpBroadcastMacAddress,
-                senderMacAddress,
-                hostIpv4Address,
-                HexDump.hexStringToByteArray("000000000000"),
-                senderIpv4Address,
-                ARP_REQUEST.toShort()
-        )
-        val receivedArpPacket = ByteArray(ARP_ETHER_IPV4_LEN)
-        receivedArpPacketBuf.get(receivedArpPacket)
-        verifyProgramRun(APF_VERSION_6, program, receivedArpPacket, DROPPED_ARP_REQUEST_REPLIED)
-
-        val transmittedPacket = ApfJniUtils.getTransmittedPacket()
-        val expectedArpReplyBuf = ArpPacket.buildArpPacket(
-                senderMacAddress,
-                apfFilter.mHardwareAddress,
-                senderIpv4Address,
-                senderMacAddress,
-                hostIpv4Address,
-                ARP_REPLY.toShort()
-        )
-        val expectedArpReplyPacket = ByteArray(ARP_ETHER_IPV4_LEN)
-        expectedArpReplyBuf.get(expectedArpReplyPacket)
-        assertContentEquals(
-                expectedArpReplyPacket + ByteArray(18) {0},
-                transmittedPacket
-        )
-        apfFilter.shutdown()
-    }
-
-    @Test
-    fun testArpOffloadDisabled() {
-        val apfConfig = getDefaultConfig()
-        apfConfig.shouldHandleArpOffload = false
-        val apfFilter =
-            ApfFilter(
-                context,
-                apfConfig,
-                ifParams,
-                ipClientCallback,
-                metrics,
-                dependencies
-            )
-        verify(ipClientCallback, times(2)).installPacketFilter(any())
-        val linkAddress = LinkAddress(InetAddress.getByAddress(hostIpv4Address), 24)
-        val lp = LinkProperties()
-        lp.addLinkAddress(linkAddress)
-        apfFilter.setLinkProperties(lp)
-        val programCaptor = ArgumentCaptor.forClass(ByteArray::class.java)
-        verify(ipClientCallback, times(3)).installPacketFilter(programCaptor.capture())
-        val program = programCaptor.value
-        val receivedArpPacketBuf = ArpPacket.buildArpPacket(
-            arpBroadcastMacAddress,
-            senderMacAddress,
-            hostIpv4Address,
-            HexDump.hexStringToByteArray("000000000000"),
-            senderIpv4Address,
-            ARP_REQUEST.toShort()
-        )
-        val receivedArpPacket = ByteArray(ARP_ETHER_IPV4_LEN)
-        receivedArpPacketBuf.get(receivedArpPacket)
-        verifyProgramRun(APF_VERSION_6, program, receivedArpPacket, PASSED_ARP_REQUEST)
-        apfFilter.shutdown()
-    }
-
-    @Test
-    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-    fun testNsFilterNoIPv6() {
-        `when`(dependencies.getAnycast6Addresses(any())).thenReturn(listOf())
-        val apfFilter =
-            ApfFilter(
-                context,
-                getDefaultConfig(),
-                ifParams,
-                ipClientCallback,
-                metrics,
-                dependencies
-        )
-
-        // validate NS packet check when there is no IPv6 address
-        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)
-        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
-        // pkt = eth/ip6/icmp6
-        val nsPkt = "01020304050600010203040586DD6000000000183AFF200100000000000" +
-                    "00200001A1122334420010000000000000200001A334411228700452900" +
-                    "00000020010000000000000200001A33441122"
-        // when there is no IPv6 addresses -> pass NS packet
-        verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(nsPkt),
-                PASSED_IPV6_NS_NO_ADDRESS
-        )
-
-        apfFilter.shutdown()
-    }
-
-    @Test
-    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-    fun testNsFilter() {
-        val apfFilter =
-            ApfFilter(
-                context,
-                getDefaultConfig(),
-                ifParams,
-                ipClientCallback,
-                metrics,
-                dependencies
-        )
-        verify(ipClientCallback, times(2)).installPacketFilter(any())
-
-        // validate Ethernet dst address check
-
-        val lp = LinkProperties()
-        for (addr in hostIpv6Addresses) {
-            lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
-        }
-
-        apfFilter.setLinkProperties(lp)
-        verify(ipClientCallback, times(3)).installPacketFilter(any())
-        apfFilter.updateClatInterfaceState(true)
-        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")
-        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
-        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
-        // pkt = eth/ip6/icmp6
-        val nonHostDstMacNsPkt = "00050403020100010203040586DD6000000000183AFF2001000000000000" +
-                                 "0200001A1122334420010000000000000200001A33441122870045290000" +
-                                 "000020010000000000000200001A33441122"
-        // invalid unicast ether dst -> pass
-        verifyProgramRun(
-            APF_VERSION_6,
-            program,
-            HexDump.hexStringToByteArray(nonHostDstMacNsPkt),
-            DROPPED_IPV6_NS_OTHER_HOST
-        )
-
-        // Using scapy to generate IPv6 NS packet:
-        // eth = Ether(src="00:01:02:03:04:05", dst="33:33:ff:03:02:01")
-        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
-        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
-        // pkt = eth/ip6/icmp6
-        val nonMcastDstMacNsPkt = "3333ff03020100010203040586DD6000000000183AFF2001000000000000" +
-                                  "0200001A1122334420010000000000000200001A33441122870045290000" +
-                                  "000020010000000000000200001A33441122"
-        // mcast dst mac is not one of solicited mcast mac derived from one of device's ip -> pass
-        verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(nonMcastDstMacNsPkt),
-                DROPPED_IPV6_NS_OTHER_HOST
-        )
-
-        // Using scapy to generate IPv6 NS packet:
-        // eth = Ether(src="00:01:02:03:04:05", dst="33:33:ff:44:11:22")
-        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
-        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
-        // pkt = eth/ip6/icmp6
-        val hostMcastDstMacNsPkt = "3333ff44112200010203040586DD6000000000183AFF2001000000000000" +
-                                   "0200001A1122334420010000000000000200001A33441122870045290000" +
-                                   "000020010000000000000200001A33441122"
-        // mcast dst mac is one of solicited mcast mac derived from one of device's ip -> pass
-        verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(hostMcastDstMacNsPkt),
-                PASSED_IPV6_ICMP
-        )
-
-        // Using scapy to generate IPv6 NS packet:
-        // eth = Ether(src="00:01:02:03:04:05", dst="FF:FF:FF:FF:FF:FF")
-        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
-        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
-        // pkt = eth/ip6/icmp6
-        val broadcastNsPkt = "FFFFFFFFFFFF00010203040586DD6000000000183AFF2001000000000000" +
-                             "0200001A1122334420010000000000000200001A33441122870045290000" +
-                             "000020010000000000000200001A33441122"
-        // mcast dst mac is broadcast address -> pass
-        verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(broadcastNsPkt),
-                PASSED_IPV6_ICMP
-        )
-
-        // validate IPv6 dst address check
-
-        // Using scapy to generate IPv6 NS packet:
-        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
-        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
-        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
-        // pkt = eth/ip6/icmp6
-        val validHostDstIpNsPkt = "02030405060700010203040586DD6000000000183AFF200100000000000" +
-                                  "00200001A1122334420010000000000000200001A334411228700452900" +
-                                  "00000020010000000000000200001A33441122"
-        // dst ip is one of device's ip -> Pass
-        verifyProgramRun(
-            APF_VERSION_6,
-            program,
-            HexDump.hexStringToByteArray(validHostDstIpNsPkt),
-            PASSED_IPV6_ICMP
-        )
-
-        // Using scapy to generate IPv6 NS packet:
-        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
-        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::100:1b:aabb:ccdd", hlim=255)
-        // icmp6 = ICMPv6ND_NS(tgt="2001::100:1b:aabb:ccdd")
-        // pkt = eth/ip6/icmp6
-        val validHostAnycastDstIpNsPkt = "02030405060700010203040586DD6000000000183AFF20010000" +
-                                         "000000000200001A1122334420010000000000000100001BAABB" +
-                                         "CCDD8700E0C00000000020010000000000000100001BAABBCCDD"
-        // dst ip is device's anycast address -> Pass
-        verifyProgramRun(
-            APF_VERSION_6,
-            program,
-            HexDump.hexStringToByteArray(validHostAnycastDstIpNsPkt),
-            PASSED_IPV6_ICMP
-        )
-
-        // Using scapy to generate IPv6 NS packet:
-        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
-        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:4444:5555", hlim=255)
-        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
-        // pkt = eth/ip6/icmp6
-        val nonHostUcastDstIpNsPkt = "02030405060700010203040586DD6000000000183AFF200100000000" +
-                                     "00000200001A1122334420010000000000000200001A444455558700" +
-                                     "EFF50000000020010000000000000200001A33441122"
-        // unicast dst ip is not one of device's ip -> pass
-        verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(nonHostUcastDstIpNsPkt),
-                DROPPED_IPV6_NS_OTHER_HOST
-        )
-
-        // Using scapy to generate IPv6 NS packet:
-        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
-        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="ff02::1:ff44:1133", hlim=255)
-        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
-        // pkt = eth/ip6/icmp6
-        val nonHostMcastDstIpNsPkt = "02030405060700010203040586DD6000000000183AFF200100000000" +
-                                     "00000200001A11223344FF0200000000000000000001FF4411338700" +
-                                     "9C2E0000000020010000000000000200001A33441122"
-        // mcast dst ip is not one of solicited mcast ip derived from one of device's ip -> pass
-        verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(nonHostMcastDstIpNsPkt),
-                DROPPED_IPV6_NS_OTHER_HOST
-        )
-
-        // Using scapy to generate IPv6 NS packet:
-        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
-        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="ff02::1:ff44:1122", hlim=255)
-        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
-        // pkt = eth/ip6/icmp6
-        val hostMcastDstIpNsPkt = "02030405060700010203040586DD6000000000183AFF200100000000" +
-                                  "00000200001A11223344FF0200000000000000000001FF4411228700" +
-                                  "9C2E0000000020010000000000000200001A33441122"
-        // mcast dst ip is one of solicited mcast ip derived from one of device's ip -> pass
-        verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(hostMcastDstIpNsPkt),
-                PASSED_IPV6_ICMP
-        )
-
-        // validate IPv6 NS payload check
-
-        // Using scapy to generate IPv6 NS packet:
-        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
-        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255, plen=20)
-        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
-        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="01:02:03:04:05:06")
-        // pkt = eth/ip6/icmp6/icmp6_opt
-        val shortNsPkt = "02030405060700010203040586DD6000000000143AFF20010000000000000200001A1" +
-                         "122334420010000000000000200001A3344112287003B140000000020010000000000" +
-                         "000200001A334411220101010203040506"
-        // payload len < 24 -> drop
-        verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(shortNsPkt),
-                DROPPED_IPV6_NS_INVALID
-        )
-
-        // Using scapy to generate IPv6 NS packet:
-        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
-        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
-        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
-        // icmp6_opt_1 = ICMPv6NDOptSrcLLAddr(lladdr="01:02:03:04:05:06")
-        // icmp6_opt_2 = ICMPv6NDOptUnknown(type=14, len=6, data='\x11\x22\x33\x44\x55\x66')
-        // pkt = eth/ip6/icmp6/icmp6_opt_1/icmp6_opt_2
-        val longNsPkt = "02030405060700010203040586DD6000000000283AFF20010000000000000200001A11" +
-                        "22334420010000000000000200001A3344112287009339000000002001000000000000" +
-                        "0200001A3344112201010102030405060E06112233445566"
-        // payload len > 32 -> pass
-        verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(longNsPkt),
-                PASSED_IPV6_NS_MULTIPLE_OPTIONS
-        )
-
-        // Using scapy to generate IPv6 NS packet:
-        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
-        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
-        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:4444:5555")
-        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="01:02:03:04:05:06")
-        // pkt = eth/ip6/icmp6/icmp6_opt
-        val otherHostNsPkt = "02030405060700010203040586DD6000000000203AFF200100000000000002000" +
-                             "01A1122334420010000000000000200001A334411228700E5E000000000200100" +
-                             "00000000000200001A444455550101010203040506"
-        // target ip is not one of device's ip -> drop
-        verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(otherHostNsPkt),
-                DROPPED_IPV6_NS_OTHER_HOST
-        )
-
-        // Using scapy to generate IPv6 NS packet:
-        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
-        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=20)
-        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
-        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="01:02:03:04:05:06")
-        // pkt = eth/ip6/icmp6/icmp6_opt
-        val invalidHoplimitNsPkt = "02030405060700010203040586DD6000000000203A14200100000000000" +
-                                   "00200001A1122334420010000000000000200001A3344112287003B1400" +
-                                   "00000020010000000000000200001A334411220101010203040506"
-        // hoplimit is not 255 -> drop
-        verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(invalidHoplimitNsPkt),
-                DROPPED_IPV6_NS_INVALID
-        )
-
-        // Using scapy to generate IPv6 NS packet:
-        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
-        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
-        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122", code=5)
-        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="01:02:03:04:05:06")
-        // pkt = eth/ip6/icmp6/icmp6_opt
-        val invalidIcmpCodeNsPkt = "02030405060700010203040586DD6000000000203AFF200100000000000" +
-                                   "00200001A1122334420010000000000000200001A3344112287053B0F00" +
-                                   "00000020010000000000000200001A334411220101010203040506"
-        // icmp6 code is not 0 -> drop
-        verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(invalidIcmpCodeNsPkt),
-                DROPPED_IPV6_NS_INVALID
-        )
-
-        apfFilter.shutdown()
-    }
-
-    @Test
-    fun testApfProgramUpdate() {
-        val apfFilter =
-            ApfFilter(
-                context,
-                getDefaultConfig(),
-                ifParams,
-                ipClientCallback,
-                metrics,
-                dependencies
-        )
-
-        verify(ipClientCallback, times(2)).installPacketFilter(any())
-        // add IPv4 address, expect to have apf program update
-        val lp = LinkProperties()
-        val linkAddress = LinkAddress(InetAddress.getByAddress(hostIpv4Address), 24)
-        lp.addLinkAddress(linkAddress)
-        apfFilter.setLinkProperties(lp)
-        verify(ipClientCallback, times(3)).installPacketFilter(any())
-
-        // add the same IPv4 address, expect to have no apf program update
-        apfFilter.setLinkProperties(lp)
-        verify(ipClientCallback, times(3)).installPacketFilter(any())
-
-        // add IPv6 addresses, expect to have apf program update
-        for (addr in hostIpv6Addresses) {
-            lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
-        }
-
-        apfFilter.setLinkProperties(lp)
-        verify(ipClientCallback, times(4)).installPacketFilter(any())
-
-        // add the same IPv6 addresses, expect to have no apf program update
-        apfFilter.setLinkProperties(lp)
-        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(
-            version: Int,
-            program: ByteArray,
-            pkt: ByteArray,
-            targetCnt: Counter,
-            cntMap: MutableMap<Counter, Long> = mutableMapOf(),
-            dataRegion: ByteArray = ByteArray(Counter.totalSize()) { 0 },
-            incTotal: Boolean = true,
-            result: Int = if (targetCnt.name.startsWith("PASSED")) PASS else DROP
-    ) {
-        assertVerdict(version, result, program, pkt, dataRegion)
-        cntMap[targetCnt] = cntMap.getOrDefault(targetCnt, 0) + 1
-        if (incTotal) {
-            cntMap[TOTAL_PACKETS] = cntMap.getOrDefault(TOTAL_PACKETS, 0) + 1
-        }
-        val errMsg = "Counter is not increased properly. To debug: \n" +
-                     " apf_run --program ${HexDump.toHexString(program)} " +
-                     "--packet ${HexDump.toHexString(pkt)} " +
-                     "--data ${HexDump.toHexString(dataRegion)} --age 0 " +
-                     "${if (version == APF_VERSION_6) "--v6" else "" } --trace  | less \n"
-        assertEquals(cntMap, decodeCountersIntoMap(dataRegion), errMsg)
-    }
-
-    private fun decodeCountersIntoMap(counterBytes: ByteArray): Map<Counter, Long> {
-        val counters = Counter::class.java.enumConstants
-        val ret = HashMap<Counter, Long>()
-        val skippedCounters = setOf(APF_PROGRAM_ID, APF_VERSION)
-        // starting from index 2 to skip the endianness mark
-        for (c in listOf(*counters).subList(2, counters.size)) {
-            if (c in skippedCounters) continue
-            val value = ApfCounterTracker.getCounterValue(counterBytes, c)
-            if (value != 0L) {
-                ret[c] = value
-            }
-        }
-        return ret
-    }
-
     private fun encodeInstruction(opcode: Int, immLength: Int, register: Int): Byte {
         val immLengthEncoding = if (immLength == 4) 3 else immLength
         return opcode.shl(3).or(immLengthEncoding.shl(1)).or(register).toByte()
@@ -2360,15 +1634,4 @@
         )
         return this.drop(7).toByteArray()
     }
-
-    private fun getDefaultConfig(apfVersion: Int = APF_VERSION_6): ApfFilter.ApfConfiguration {
-        val config = ApfFilter.ApfConfiguration()
-        config.apfCapabilities =
-                ApfCapabilities(apfVersion, 4096, ARPHRD_ETHER)
-        config.multicastFilter = false
-        config.ieee802_3Filter = false
-        config.ethTypeBlackList = IntArray(0)
-        config.shouldHandleArpOffload = true
-        return config
-    }
 }
diff --git a/tests/unit/src/android/net/apf/ApfStandaloneTest.kt b/tests/unit/src/android/net/apf/ApfStandaloneTest.kt
index 1a2307d..531c4cc 100644
--- a/tests/unit/src/android/net/apf/ApfStandaloneTest.kt
+++ b/tests/unit/src/android/net/apf/ApfStandaloneTest.kt
@@ -54,6 +54,8 @@
 class ApfStandaloneTest {
 
     private val etherTypeDenyList = listOf(0x88A2, 0x88A4, 0x88B8, 0x88CD, 0x88E1, 0x88E3)
+    private val ramSize = 1024
+    private val clampSize = 1024
 
     fun runApfTest(isSuspendMode: Boolean) {
         val program = generateApfV4Program(isSuspendMode)
@@ -248,7 +250,7 @@
         val endOfDhcpFilter = "endOfDhcpFilter"
         val endOfRsFilter = "endOfRsFiler"
         val endOfPingFilter = "endOfPingFilter"
-        val gen = ApfV4Generator(APF_VERSION_4)
+        val gen = ApfV4Generator(APF_VERSION_4, ramSize, clampSize)
 
         maybeSetupCounter(gen, Counter.TOTAL_PACKETS)
         gen.addLoadData(R0, 0)
diff --git a/tests/unit/src/android/net/apf/ApfTest.java b/tests/unit/src/android/net/apf/ApfTest.java
index 05e2e39..c80aaa9 100644
--- a/tests/unit/src/android/net/apf/ApfTest.java
+++ b/tests/unit/src/android/net/apf/ApfTest.java
@@ -17,7 +17,10 @@
 package android.net.apf;
 
 import static android.net.apf.ApfCounterTracker.Counter.getCounterEnumFromOffset;
+import static android.net.apf.ApfTestHelpers.consumeInstalledProgram;
 import static android.net.apf.BaseApfGenerator.APF_VERSION_3;
+import static android.net.apf.BaseApfGenerator.APF_VERSION_4;
+import static android.net.apf.BaseApfGenerator.APF_VERSION_6;
 import static android.net.apf.BaseApfGenerator.DROP_LABEL;
 import static android.net.apf.BaseApfGenerator.MemorySlot;
 import static android.net.apf.BaseApfGenerator.PASS_LABEL;
@@ -32,7 +35,7 @@
 import static android.net.apf.ApfTestUtils.assertProgramEquals;
 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.AF_UNIX;
 import static android.system.OsConstants.ETH_P_ARP;
 import static android.system.OsConstants.ETH_P_IP;
 import static android.system.OsConstants.ETH_P_IPV6;
@@ -40,24 +43,26 @@
 import static android.system.OsConstants.IPPROTO_IPV6;
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_STREAM;
 
 import static com.android.net.module.util.HexDump.hexStringToByteArray;
 import static com.android.net.module.util.HexDump.toHexString;
 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.junit.Assert.fail;
-import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.clearInvocations;
 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.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -71,14 +76,15 @@
 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.BaseApfGenerator.IllegalInstructionException;
+import android.net.ip.IpClient;
 import android.net.metrics.IpConnectivityLog;
 import android.os.Build;
 import android.os.PowerManager;
+import android.os.SystemClock;
 import android.stats.connectivity.NetworkQuirkEvent;
 import android.system.ErrnoException;
+import android.system.Os;
 import android.text.TextUtils;
 import android.text.format.DateUtils;
 import android.util.ArrayMap;
@@ -93,6 +99,7 @@
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.DnsPacket;
 import com.android.net.module.util.Inet4AddressUtils;
+import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.NetworkStackConstants;
 import com.android.net.module.util.PacketBuilder;
 import com.android.networkstack.metrics.ApfSessionInfoMetrics;
@@ -103,6 +110,7 @@
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 
+import libcore.io.IoUtils;
 import libcore.io.Streams;
 
 import org.junit.After;
@@ -118,6 +126,7 @@
 
 import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.FileDescriptor;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -144,6 +153,8 @@
 @SmallTest
 public class ApfTest {
     private static final int APF_VERSION_2 = 2;
+    private int mRamSize = 1024;
+    private int mClampSize = 1024;
 
     @Rule
     public DevSdkIgnoreRule mDevSdkIgnoreRule = new DevSdkIgnoreRule();
@@ -164,12 +175,15 @@
     @Mock private NetworkQuirkMetrics mNetworkQuirkMetrics;
     @Mock private ApfSessionInfoMetrics mApfSessionInfoMetrics;
     @Mock private IpClientRaInfoMetrics mIpClientRaInfoMetrics;
-    @Mock private ApfFilter.Clock mClock;
+    @Mock private IpClient.IpClientCallbacksWrapper mIpClientCb;
     @GuardedBy("mApfFilterCreated")
     private final ArrayList<AndroidPacketFilter> mApfFilterCreated = new ArrayList<>();
     @GuardedBy("mThreadsToBeCleared")
     private final ArrayList<Thread> mThreadsToBeCleared = new ArrayList<>();
 
+    private FileDescriptor mWriteSocket;
+    private long mCurrentTimeMs;
+
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -188,13 +202,20 @@
             }
             return null;
         }).when(mDependencies).onThreadCreated(any());
+        FileDescriptor readSocket = new FileDescriptor();
+        mWriteSocket = new FileDescriptor();
+        Os.socketpair(AF_UNIX, SOCK_STREAM, 0, mWriteSocket, readSocket);
+        doReturn(readSocket).when(mDependencies).createRaReaderSocket(anyInt());
+        mCurrentTimeMs = SystemClock.elapsedRealtime();
+        doReturn(mCurrentTimeMs).when(mDependencies).elapsedRealtime();
+        doReturn(true).when(mIpClientCb).installPacketFilter(any());
     }
 
     private void quitThreads() throws Exception {
         ConcurrentUtils.quitThreads(
                 THREAD_QUIT_MAX_RETRY_COUNT,
                 false /* interrupt */,
-                HANDLER_TIMEOUT_MS,
+                TIMEOUT_MS,
                 () -> {
                     synchronized (mThreadsToBeCleared) {
                         final ArrayList<Thread> ret = new ArrayList<>(mThreadsToBeCleared);
@@ -230,12 +251,12 @@
         shutdownApfFilters();
         // Clear mocks to prevent from stubs holding instances and cause memory leaks.
         Mockito.framework().clearInlineMocks();
+        IoUtils.closeQuietly(mWriteSocket);
+        mWriteSocket = null;
     }
 
     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;
@@ -246,7 +267,8 @@
     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 TIMEOUT_MS = 1000;
+    private static final int NO_CALLBACK_TIMEOUT_MS = 500;
     private static final int THREAD_QUIT_MAX_RETRY_COUNT = 3;
 
     // Constants for opcode encoding
@@ -259,9 +281,15 @@
     private static final byte SIZE32  = (byte)(3 << 1);
     private static final byte R1_REG = 1;
 
+    private static final byte[] TEST_MAC_ADDR = {2, 3, 4, 5, 6, 7};
+    private static final int TEST_IFACE_IDX = 1234;
+    private static final InterfaceParams TEST_PARAMS = new InterfaceParams("lo", TEST_IFACE_IDX,
+            MacAddress.fromBytes(TEST_MAC_ADDR), 1500 /* defaultMtu */);
+
     private static ApfConfiguration getDefaultConfig() {
         ApfFilter.ApfConfiguration config = new ApfConfiguration();
-        config.apfCapabilities = MOCK_APF_CAPABILITIES;
+        config.apfVersionSupported = 2;
+        config.apfRamSize = 4096;
         config.multicastFilter = ALLOW_MULTICAST;
         config.ieee802_3Filter = ALLOW_802_3_FRAMES;
         config.ethTypeBlackList = new int[0];
@@ -336,17 +364,17 @@
         // Empty program should pass because having the program counter reach the
         // location immediately after the program indicates the packet should be
         // passed to the AP.
-        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_2);
+        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         assertPass(gen);
 
         // Test pass opcode
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addPass();
         gen.addJump(DROP_LABEL);
         assertPass(gen);
 
         // Test jumping to pass label.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addJump(PASS_LABEL);
         byte[] program = gen.generate();
         assertEquals(1, program.length);
@@ -354,7 +382,7 @@
         assertPass(program, new byte[MIN_PKT_SIZE], 0);
 
         // Test jumping to drop label.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addJump(DROP_LABEL);
         program = gen.generate();
         assertEquals(2, program.length);
@@ -363,127 +391,127 @@
         assertDrop(program, new byte[15], 15);
 
         // Test jumping if equal to 0.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addJumpIfR0Equals(0, DROP_LABEL);
         assertDrop(gen);
 
         // Test jumping if not equal to 0.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addJumpIfR0NotEquals(0, DROP_LABEL);
         assertPass(gen);
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 1);
         gen.addJumpIfR0NotEquals(0, DROP_LABEL);
         assertDrop(gen);
 
         // Test jumping if registers equal.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addJumpIfR0EqualsR1(DROP_LABEL);
         assertDrop(gen);
 
         // Test jumping if registers not equal.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addJumpIfR0NotEqualsR1(DROP_LABEL);
         assertPass(gen);
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 1);
         gen.addJumpIfR0NotEqualsR1(DROP_LABEL);
         assertDrop(gen);
 
         // Test load immediate.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 1234567890);
         gen.addJumpIfR0Equals(1234567890, DROP_LABEL);
         assertDrop(gen);
 
         // Test add.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addAdd(1234567890);
         gen.addJumpIfR0Equals(1234567890, DROP_LABEL);
         assertDrop(gen);
 
         // Test add with a small signed negative value.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addAdd(-1);
         gen.addJumpIfR0Equals(-1, DROP_LABEL);
         assertDrop(gen);
 
         // Test subtract.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addAdd(-1234567890);
         gen.addJumpIfR0Equals(-1234567890, DROP_LABEL);
         assertDrop(gen);
 
         // Test or.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addOr(1234567890);
         gen.addJumpIfR0Equals(1234567890, DROP_LABEL);
         assertDrop(gen);
 
         // Test and.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 1234567890);
         gen.addAnd(123456789);
         gen.addJumpIfR0Equals(1234567890 & 123456789, DROP_LABEL);
         assertDrop(gen);
 
         // Test left shift.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 1234567890);
         gen.addLeftShift(1);
         gen.addJumpIfR0Equals(1234567890 << 1, DROP_LABEL);
         assertDrop(gen);
 
         // Test right shift.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 1234567890);
         gen.addRightShift(1);
         gen.addJumpIfR0Equals(1234567890 >> 1, DROP_LABEL);
         assertDrop(gen);
 
         // Test multiply.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 123456789);
         gen.addMul(2);
         gen.addJumpIfR0Equals(123456789 * 2, DROP_LABEL);
         assertDrop(gen);
 
         // Test divide.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 1234567890);
         gen.addDiv(2);
         gen.addJumpIfR0Equals(1234567890 / 2, DROP_LABEL);
         assertDrop(gen);
 
         // Test divide by zero.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addDiv(0);
         gen.addJump(DROP_LABEL);
         assertPass(gen);
 
         // Test add.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R1, 1234567890);
         gen.addAddR1ToR0();
         gen.addJumpIfR0Equals(1234567890, DROP_LABEL);
         assertDrop(gen);
 
         // Test subtract.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R1, -1234567890);
         gen.addAddR1ToR0();
         gen.addJumpIfR0Equals(-1234567890, DROP_LABEL);
         assertDrop(gen);
 
         // Test or.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R1, 1234567890);
         gen.addOrR0WithR1();
         gen.addJumpIfR0Equals(1234567890, DROP_LABEL);
         assertDrop(gen);
 
         // Test and.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 1234567890);
         gen.addLoadImmediate(R1, 123456789);
         gen.addAndR0WithR1();
@@ -491,7 +519,7 @@
         assertDrop(gen);
 
         // Test left shift.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 1234567890);
         gen.addLoadImmediate(R1, 1);
         gen.addLeftShiftR0ByR1();
@@ -499,7 +527,7 @@
         assertDrop(gen);
 
         // Test right shift.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 1234567890);
         gen.addLoadImmediate(R1, -1);
         gen.addLeftShiftR0ByR1();
@@ -507,7 +535,7 @@
         assertDrop(gen);
 
         // Test multiply.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 123456789);
         gen.addLoadImmediate(R1, 2);
         gen.addMulR0ByR1();
@@ -515,7 +543,7 @@
         assertDrop(gen);
 
         // Test divide.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 1234567890);
         gen.addLoadImmediate(R1, 2);
         gen.addDivR0ByR1();
@@ -523,136 +551,136 @@
         assertDrop(gen);
 
         // Test divide by zero.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addDivR0ByR1();
         gen.addJump(DROP_LABEL);
         assertPass(gen);
 
         // Test byte load.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoad8(R0, 1);
         gen.addJumpIfR0Equals(45, DROP_LABEL);
         assertDrop(gen, new byte[]{123,45,0,0,0,0,0,0,0,0,0,0,0,0,0}, 0);
 
         // Test out of bounds load.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoad8(R0, 16);
         gen.addJumpIfR0Equals(0, DROP_LABEL);
         assertPass(gen, new byte[]{123,45,0,0,0,0,0,0,0,0,0,0,0,0,0}, 0);
 
         // Test half-word load.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoad16(R0, 1);
         gen.addJumpIfR0Equals((45 << 8) | 67, DROP_LABEL);
         assertDrop(gen, new byte[]{123,45,67,0,0,0,0,0,0,0,0,0,0,0,0}, 0);
 
         // Test word load.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoad32(R0, 1);
         gen.addJumpIfR0Equals((45 << 24) | (67 << 16) | (89 << 8) | 12, DROP_LABEL);
         assertDrop(gen, new byte[]{123,45,67,89,12,0,0,0,0,0,0,0,0,0,0}, 0);
 
         // Test byte indexed load.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R1, 1);
         gen.addLoad8Indexed(R0, 0);
         gen.addJumpIfR0Equals(45, DROP_LABEL);
         assertDrop(gen, new byte[]{123,45,0,0,0,0,0,0,0,0,0,0,0,0,0}, 0);
 
         // Test out of bounds indexed load.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R1, 8);
         gen.addLoad8Indexed(R0, 8);
         gen.addJumpIfR0Equals(0, DROP_LABEL);
         assertPass(gen, new byte[]{123,45,0,0,0,0,0,0,0,0,0,0,0,0,0}, 0);
 
         // Test half-word indexed load.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R1, 1);
         gen.addLoad16Indexed(R0, 0);
         gen.addJumpIfR0Equals((45 << 8) | 67, DROP_LABEL);
         assertDrop(gen, new byte[]{123,45,67,0,0,0,0,0,0,0,0,0,0,0,0}, 0);
 
         // Test word indexed load.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R1, 1);
         gen.addLoad32Indexed(R0, 0);
         gen.addJumpIfR0Equals((45 << 24) | (67 << 16) | (89 << 8) | 12, DROP_LABEL);
         assertDrop(gen, new byte[]{123,45,67,89,12,0,0,0,0,0,0,0,0,0,0}, 0);
 
         // Test jumping if greater than.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addJumpIfR0GreaterThan(0, DROP_LABEL);
         assertPass(gen);
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 1);
         gen.addJumpIfR0GreaterThan(0, DROP_LABEL);
         assertDrop(gen);
 
         // Test jumping if less than.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addJumpIfR0LessThan(0, DROP_LABEL);
         assertPass(gen);
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addJumpIfR0LessThan(1, DROP_LABEL);
         assertDrop(gen);
 
         // Test jumping if any bits set.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addJumpIfR0AnyBitsSet(3, DROP_LABEL);
         assertPass(gen);
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 1);
         gen.addJumpIfR0AnyBitsSet(3, DROP_LABEL);
         assertDrop(gen);
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 3);
         gen.addJumpIfR0AnyBitsSet(3, DROP_LABEL);
         assertDrop(gen);
 
         // Test jumping if register greater than.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addJumpIfR0GreaterThanR1(DROP_LABEL);
         assertPass(gen);
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 2);
         gen.addLoadImmediate(R1, 1);
         gen.addJumpIfR0GreaterThanR1(DROP_LABEL);
         assertDrop(gen);
 
         // Test jumping if register less than.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addJumpIfR0LessThanR1(DROP_LABEL);
         assertPass(gen);
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R1, 1);
         gen.addJumpIfR0LessThanR1(DROP_LABEL);
         assertDrop(gen);
 
         // Test jumping if any bits set in register.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R1, 3);
         gen.addJumpIfR0AnyBitsSetR1(DROP_LABEL);
         assertPass(gen);
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R1, 3);
         gen.addLoadImmediate(R0, 1);
         gen.addJumpIfR0AnyBitsSetR1(DROP_LABEL);
         assertDrop(gen);
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R1, 3);
         gen.addLoadImmediate(R0, 3);
         gen.addJumpIfR0AnyBitsSetR1(DROP_LABEL);
         assertDrop(gen);
 
         // Test load from memory.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadFromMemory(R0, MemorySlot.SLOT_0);
         gen.addJumpIfR0Equals(0, DROP_LABEL);
         assertDrop(gen);
 
         // Test store to memory.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R1, 1234567890);
         gen.addStoreToMemory(MemorySlot.RAM_LEN, R1);
         gen.addLoadFromMemory(R0, MemorySlot.RAM_LEN);
@@ -660,63 +688,63 @@
         assertDrop(gen);
 
         // Test filter age pre-filled memory.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_SECONDS);
         gen.addJumpIfR0Equals(123, DROP_LABEL);
         assertDrop(gen, new byte[MIN_PKT_SIZE], 123);
 
         // Test packet size pre-filled memory.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadFromMemory(R0, MemorySlot.PACKET_SIZE);
         gen.addJumpIfR0Equals(MIN_PKT_SIZE, DROP_LABEL);
         assertDrop(gen);
 
         // Test IPv4 header size pre-filled memory.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadFromMemory(R0, MemorySlot.IPV4_HEADER_SIZE);
         gen.addJumpIfR0Equals(20, DROP_LABEL);
         assertDrop(gen, new byte[]{0,0,0,0,0,0,0,0,0,0,0,0,8,0,0x45,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}, 0);
 
         // Test not.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 1234567890);
         gen.addNot(R0);
         gen.addJumpIfR0Equals(~1234567890, DROP_LABEL);
         assertDrop(gen);
 
         // Test negate.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 1234567890);
         gen.addNeg(R0);
         gen.addJumpIfR0Equals(-1234567890, DROP_LABEL);
         assertDrop(gen);
 
         // Test move.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R1, 1234567890);
         gen.addMove(R0);
         gen.addJumpIfR0Equals(1234567890, DROP_LABEL);
         assertDrop(gen);
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 1234567890);
         gen.addMove(R1);
         gen.addJumpIfR0Equals(1234567890, DROP_LABEL);
         assertDrop(gen);
 
         // Test swap.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R1, 1234567890);
         gen.addSwap();
         gen.addJumpIfR0Equals(1234567890, DROP_LABEL);
         assertDrop(gen);
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 1234567890);
         gen.addSwap();
         gen.addJumpIfR0Equals(0, DROP_LABEL);
         assertDrop(gen);
 
         // Test jump if bytes not equal.
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 1);
         gen.addJumpIfBytesAtR0NotEqual(new byte[]{123}, DROP_LABEL);
         program = gen.generate();
@@ -728,20 +756,20 @@
         assertEquals(1, program[4]);
         assertEquals(123, program[5]);
         assertDrop(program, new byte[MIN_PKT_SIZE], 0);
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 1);
         gen.addJumpIfBytesAtR0NotEqual(new byte[]{123}, DROP_LABEL);
         byte[] packet123 = {0,123,0,0,0,0,0,0,0,0,0,0,0,0,0};
         assertPass(gen, packet123, 0);
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addJumpIfBytesAtR0NotEqual(new byte[]{123}, DROP_LABEL);
         assertDrop(gen, packet123, 0);
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 1);
         gen.addJumpIfBytesAtR0NotEqual(new byte[]{1, 2, 30, 4, 5}, DROP_LABEL);
         byte[] packet12345 = {0,1,2,3,4,5,0,0,0,0,0,0,0,0,0};
         assertDrop(gen, packet12345, 0);
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 1);
         gen.addJumpIfBytesAtR0NotEqual(new byte[]{1, 2, 3, 4, 5}, DROP_LABEL);
         assertPass(gen, packet12345, 0);
@@ -750,12 +778,12 @@
     @Test(expected = ApfV4Generator.IllegalInstructionException.class)
     public void testApfGeneratorWantsV2OrGreater() throws Exception {
         // The minimum supported APF version is 2.
-        new ApfV4Generator(1);
+        new ApfV4Generator(1, mRamSize, mClampSize);
     }
 
     @Test
     public void testApfDataOpcodesWantApfV3() throws IllegalInstructionException, Exception {
-        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_2);
+        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         try {
             gen.addStoreData(R0, 0);
             fail();
@@ -778,22 +806,22 @@
         ApfV4Generator gen;
 
         // 0-byte immediate: li R0, 0
-        gen = new ApfV4Generator(4);
+        gen = new ApfV4Generator(APF_VERSION_4, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 0);
         assertProgramEquals(new byte[]{LI_OP | SIZE0}, gen.generate());
 
         // 1-byte immediate: li R0, 42
-        gen = new ApfV4Generator(4);
+        gen = new ApfV4Generator(APF_VERSION_4, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 42);
         assertProgramEquals(new byte[]{LI_OP | SIZE8, 42}, gen.generate());
 
         // 2-byte immediate: li R1, 0x1234
-        gen = new ApfV4Generator(4);
+        gen = new ApfV4Generator(APF_VERSION_4, mRamSize, mClampSize);
         gen.addLoadImmediate(R1, 0x1234);
         assertProgramEquals(new byte[]{LI_OP | SIZE16 | R1_REG, 0x12, 0x34}, gen.generate());
 
         // 4-byte immediate: li R0, 0x12345678
-        gen = new ApfV4Generator(3);
+        gen = new ApfV4Generator(APF_VERSION_3, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 0x12345678);
         assertProgramEquals(
                 new byte[]{LI_OP | SIZE32, 0x12, 0x34, 0x56, 0x78},
@@ -808,18 +836,18 @@
         ApfV4Generator gen;
 
         // 1-byte negative immediate: li R0, -42
-        gen = new ApfV4Generator(3);
+        gen = new ApfV4Generator(APF_VERSION_3, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, -42);
         assertProgramEquals(new byte[]{LI_OP | SIZE8, -42}, gen.generate());
 
         // 2-byte negative immediate: li R1, -0x1122
-        gen = new ApfV4Generator(3);
+        gen = new ApfV4Generator(APF_VERSION_3, mRamSize, mClampSize);
         gen.addLoadImmediate(R1, -0x1122);
         assertProgramEquals(new byte[]{LI_OP | SIZE16 | R1_REG, (byte)0xEE, (byte)0xDE},
                 gen.generate());
 
         // 4-byte negative immediate: li R0, -0x11223344
-        gen = new ApfV4Generator(3);
+        gen = new ApfV4Generator(APF_VERSION_3, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, -0x11223344);
         assertProgramEquals(
                 new byte[]{LI_OP | SIZE32, (byte)0xEE, (byte)0xDD, (byte)0xCC, (byte)0xBC},
@@ -834,23 +862,23 @@
         ApfV4Generator gen;
 
         // Load data with no offset: lddw R0, [0 + r1]
-        gen = new ApfV4Generator(APF_VERSION_3);
+        gen = new ApfV4Generator(APF_VERSION_3, mRamSize, mClampSize);
         gen.addLoadData(R0, 0);
         assertProgramEquals(new byte[]{LDDW_OP | SIZE0}, gen.generate());
 
         // Store data with 8bit negative offset: lddw r0, [-42 + r1]
-        gen = new ApfV4Generator(APF_VERSION_3);
+        gen = new ApfV4Generator(APF_VERSION_3, mRamSize, mClampSize);
         gen.addStoreData(R0, -42);
         assertProgramEquals(new byte[]{STDW_OP | SIZE8, -42}, gen.generate());
 
         // Store data to R1 with 16bit negative offset: stdw r1, [-0x1122 + r0]
-        gen = new ApfV4Generator(APF_VERSION_3);
+        gen = new ApfV4Generator(APF_VERSION_3, mRamSize, mClampSize);
         gen.addStoreData(R1, -0x1122);
         assertProgramEquals(new byte[]{STDW_OP | SIZE16 | R1_REG, (byte)0xEE, (byte)0xDE},
                 gen.generate());
 
         // Load data to R1 with 32bit negative offset: lddw r1, [0xDEADBEEF + r0]
-        gen = new ApfV4Generator(APF_VERSION_3);
+        gen = new ApfV4Generator(APF_VERSION_3, mRamSize, mClampSize);
         gen.addLoadData(R1, 0xDEADBEEF);
         assertProgramEquals(
                 new byte[]{LDDW_OP | SIZE32 | R1_REG,
@@ -868,12 +896,12 @@
         byte[] expected_data = data.clone();
 
         // No memory access instructions: should leave the data segment untouched.
-        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_3);
+        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_3, mRamSize, mClampSize);
         assertDataMemoryContents(PASS, gen.generate(), packet, data, expected_data);
 
         // Expect value 0x87654321 to be stored starting from address -11 from the end of the
         // data buffer, in big-endian order.
-        gen = new ApfV4Generator(APF_VERSION_3);
+        gen = new ApfV4Generator(APF_VERSION_3, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 0x87654321);
         gen.addLoadImmediate(R1, -5);
         gen.addStoreData(R0, -6);  // -5 + -6 = -11 (offset +5 with data_len=16)
@@ -890,7 +918,7 @@
     @Test
     public void testApfDataRead() throws IllegalInstructionException, Exception {
         // Program that DROPs if address 10 (-6) contains 0x87654321.
-        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_3);
+        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_3, mRamSize, mClampSize);
         gen.addLoadImmediate(R1, 1000);
         gen.addLoadData(R0, -1006);  // 1000 + -1006 = -6 (offset +10 with data_len=16)
         gen.addJumpIfR0Equals(0x87654321, DROP_LABEL);
@@ -919,7 +947,7 @@
      */
     @Test
     public void testApfDataReadModifyWrite() throws IllegalInstructionException, Exception {
-        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_3);
+        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_3, mRamSize, mClampSize);
         gen.addLoadImmediate(R1, -22);
         gen.addLoadData(R0, 0);  // Load from address 32 -22 + 0 = 10
         gen.addAdd(0x78453412);  // 87654321 + 78453412 = FFAA7733
@@ -946,7 +974,7 @@
         byte[] expected_data = data;
 
         // Program that DROPs unconditionally. This is our the baseline.
-        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_3);
+        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_3, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 3);
         gen.addLoadData(R1, 7);
         gen.addJump(DROP_LABEL);
@@ -956,7 +984,7 @@
         // 3 instructions, all normal opcodes (LI, LDDW, JMP) with 1 byte immediate = 6 byte program
         // 32 byte data length, for a total of 38 byte ram len.
         // APFv6 needs to round this up to be a multiple of 4, so 40.
-        gen = new ApfV4Generator(APF_VERSION_3);
+        gen = new ApfV4Generator(APF_VERSION_3, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 20);
         if (mApfVersion == 4) {
             gen.addLoadData(R1, 15);  // R0(20)+15+U32[0..3] >= 6 prog + 32 data, so invalid
@@ -967,21 +995,21 @@
         assertDataMemoryContents(PASS, gen.generate(), packet, data, expected_data);
 
         // Subtracting an immediate should work...
-        gen = new ApfV4Generator(APF_VERSION_3);
+        gen = new ApfV4Generator(APF_VERSION_3, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 20);
         gen.addLoadData(R1, -4);
         gen.addJump(DROP_LABEL);
         assertDataMemoryContents(DROP, gen.generate(), packet, data, expected_data);
 
         // ...and underflowing simply wraps around to the end of the buffer...
-        gen = new ApfV4Generator(APF_VERSION_3);
+        gen = new ApfV4Generator(APF_VERSION_3, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 20);
         gen.addLoadData(R1, -30);
         gen.addJump(DROP_LABEL);
         assertDataMemoryContents(DROP, gen.generate(), packet, data, expected_data);
 
         // ...but doesn't allow accesses before the start of the buffer
-        gen = new ApfV4Generator(APF_VERSION_3);
+        gen = new ApfV4Generator(APF_VERSION_3, mRamSize, mClampSize);
         gen.addLoadImmediate(R0, 20);
         gen.addLoadData(R1, -1000);
         gen.addJump(DROP_LABEL);  // Not reached.
@@ -1006,6 +1034,11 @@
         }
     }
 
+    private void pretendPacketReceived(byte[] packet)
+            throws IOException, ErrnoException {
+        Os.write(mWriteSocket, packet, 0, packet.length);
+    }
+
     /**
      * Generate APF program, run pcap file though APF filter, then check all the packets in the file
      * should be dropped.
@@ -1014,20 +1047,20 @@
     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.apfVersionSupported = 4;
+        config.apfRamSize = 1700;
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
-                mNetworkQuirkMetrics, mDependencies);
+        ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS,
+                mIpClientCb, mNetworkQuirkMetrics, mDependencies);
+        consumeInstalledProgram(mIpClientCb, 2 /* installCnt */);
         apfFilter.setLinkProperties(lp);
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         byte[] data = new byte[Counter.totalSize()];
         final boolean result;
 
@@ -1193,18 +1226,18 @@
 
     @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 apfFilter = new ApfFilter(mContext, config, TEST_PARAMS,
+                mIpClientCb, mNetworkQuirkMetrics, mDependencies);
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         apfFilter.setLinkProperties(lp);
 
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
         if (SdkLevel.isAtLeastV()) {
@@ -1216,7 +1249,7 @@
         }
 
         // Verify unicast IPv4 packet is passed
-        put(packet, ETH_DEST_ADDR_OFFSET, TestApfFilter.MOCK_MAC_ADDR);
+        put(packet, ETH_DEST_ADDR_OFFSET, TEST_MAC_ADDR);
         packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP);
         put(packet, IPV4_DEST_ADDR_OFFSET, MOCK_IPV4_ADDR);
         assertPass(program, packet.array());
@@ -1244,21 +1277,20 @@
         assertDrop(program, packet.array());
 
         // Verify broadcast IPv4 DHCP to us is passed
-        put(packet, DHCP_CLIENT_MAC_OFFSET, TestApfFilter.MOCK_MAC_ADDR);
+        put(packet, DHCP_CLIENT_MAC_OFFSET, TEST_MAC_ADDR);
         assertPass(program, packet.array());
 
         // Verify unicast IPv4 DHCP to us is passed
-        put(packet, ETH_DEST_ADDR_OFFSET, TestApfFilter.MOCK_MAC_ADDR);
+        put(packet, ETH_DEST_ADDR_OFFSET, TEST_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,
+        ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
                 mNetworkQuirkMetrics, mDependencies);
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Verify empty IPv6 packet is passed
         ByteBuffer packet = makeIpv6Packet(IPPROTO_UDP);
@@ -1473,12 +1505,12 @@
 
     @Test
     public void testAddNopAddsOneByte() throws Exception {
-        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_2);
+        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addNop();
         assertEquals(1, gen.generate().length);
 
         final int count = 42;
-        gen = new ApfV4Generator(APF_VERSION_2);
+        gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         for (int i = 0; i < count; i++) {
             gen.addNop();
         }
@@ -1486,7 +1518,7 @@
     }
 
     private ApfV4Generator generateDnsFilter(boolean ipv6, String... labels) throws Exception {
-        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_2);
+        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
         gen.addLoadImmediate(R1, ipv6 ? IPV6_HEADER_LEN : IPV4_HEADER_LEN);
         DnsUtils.generateFilter(gen, labels);
         return gen;
@@ -1664,18 +1696,18 @@
         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,
+        ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
                 mNetworkQuirkMetrics, mDependencies);
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         apfFilter.setLinkProperties(lp);
 
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Construct IPv4 and IPv6 multicast packets.
         ByteBuffer mcastv4packet = makeIpv4Packet(IPPROTO_UDP);
@@ -1697,7 +1729,7 @@
 
         // Construct IPv4 broadcast with L2 unicast address packet (b/30231088).
         ByteBuffer bcastv4unicastl2packet = makeIpv4Packet(IPPROTO_UDP);
-        bcastv4unicastl2packet.put(TestApfFilter.MOCK_MAC_ADDR);
+        bcastv4unicastl2packet.put(TEST_MAC_ADDR);
         bcastv4unicastl2packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP);
         put(bcastv4unicastl2packet, IPV4_DEST_ADDR_OFFSET, broadcastIpv4Addr);
 
@@ -1709,9 +1741,8 @@
         assertPass(program, bcastv4unicastl2packet.array());
 
         // Turn on multicast filter and verify it works
-        ipClientCallback.resetApfProgramWait();
         apfFilter.setMulticastFilter(true);
-        program = ipClientCallback.assertProgramUpdateAndGet();
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         assertDrop(program, mcastv4packet.array());
         assertDrop(program, mcastv6packet.array());
         assertDrop(program, bcastv4packet1.array());
@@ -1719,9 +1750,8 @@
         assertDrop(program, bcastv4unicastl2packet.array());
 
         // Turn off multicast filter and verify it's off
-        ipClientCallback.resetApfProgramWait();
         apfFilter.setMulticastFilter(false);
-        program = ipClientCallback.assertProgramUpdateAndGet();
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         assertPass(program, mcastv4packet.array());
         assertPass(program, mcastv6packet.array());
         assertPass(program, bcastv4packet1.array());
@@ -1729,13 +1759,14 @@
         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);
+        clearInvocations(mIpClientCb);
+        apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
+                mNetworkQuirkMetrics, mDependencies);
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         apfFilter.setLinkProperties(lp);
-        program = ipClientCallback.assertProgramUpdateAndGet();
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         assertDrop(program, mcastv4packet.array());
         assertDrop(program, mcastv6packet.array());
         assertDrop(program, bcastv4packet1.array());
@@ -1757,32 +1788,14 @@
         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 ApfFilter apfFilter = new ApfFilter(mContext, configuration, TEST_PARAMS,
+                mIpClientCb, mNetworkQuirkMetrics, mDependencies);
+        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
                 ArgumentCaptor.forClass(BroadcastReceiver.class);
-        verify(mDependencies).addDeviceIdleReceiver(receiverCaptor.capture(), anyBoolean());
+        verify(mDependencies).addDeviceIdleReceiver(receiverCaptor.capture());
         final BroadcastReceiver receiver = receiverCaptor.getValue();
 
         // Construct a multicast ICMPv6 ECHO request.
@@ -1792,7 +1805,7 @@
         put(packet, IPV6_DEST_ADDR_OFFSET, multicastIpv6Addr);
 
         // Normally, we let multicast pings alone...
-        assertPass(ipClientCallback.assertProgramUpdateAndGet(), packet.array());
+        assertPass(program, packet.array());
 
         if (isLightDozing) {
             doReturn(true).when(mPowerManager).isDeviceLightIdleMode();
@@ -1801,19 +1814,21 @@
             doReturn(true).when(mPowerManager).isDeviceIdleMode();
             receiver.onReceive(mContext, new Intent(ACTION_DEVICE_IDLE_MODE_CHANGED));
         }
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         // ...and even while dozing...
-        assertPass(ipClientCallback.assertProgramUpdateAndGet(), packet.array());
+        assertPass(program, 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());
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        assertDrop(program, 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());
+        assertPass(program, 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).
@@ -1824,17 +1839,17 @@
             doReturn(false).when(mPowerManager).isDeviceIdleMode();
             receiver.onReceive(mContext, new Intent(ACTION_DEVICE_IDLE_MODE_CHANGED));
         }
-        assertPass(ipClientCallback.assertProgramUpdateAndGet(), packet.array());
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        assertPass(program, 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,
+        ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
                 mNetworkQuirkMetrics, mDependencies);
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Verify empty packet of 100 zero bytes is passed
         // Note that eth-type = 0 makes it an IEEE802.3 frame
@@ -1850,11 +1865,10 @@
         assertPass(program, packet.array());
 
         // Now turn on the filter
-        ipClientCallback.resetApfProgramWait();
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        apfFilter = TestApfFilter.createTestApfFilter(mContext, ipClientCallback, config,
+        apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
                 mNetworkQuirkMetrics, mDependencies);
-        program = ipClientCallback.assertProgramUpdateAndGet();
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Verify that IEEE802.3 frame is dropped
         // In this case ethtype is used for payload length
@@ -1877,11 +1891,10 @@
         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,
+        ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
                 mNetworkQuirkMetrics, mDependencies);
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Verify empty packet of 100 zero bytes is passed
         // Note that eth-type = 0 makes it an IEEE802.3 frame
@@ -1897,11 +1910,10 @@
         assertPass(program, packet.array());
 
         // Now add IPv4 to the black list
-        ipClientCallback.resetApfProgramWait();
         config.ethTypeBlackList = ipv4BlackList;
-        apfFilter = TestApfFilter.createTestApfFilter(mContext, ipClientCallback, config,
+        apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
                 mNetworkQuirkMetrics, mDependencies);
-        program = ipClientCallback.assertProgramUpdateAndGet();
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Verify that IPv4 frame will be dropped
         setIpv4VersionFields(packet);
@@ -1912,11 +1924,10 @@
         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,
+        apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
                 mNetworkQuirkMetrics, mDependencies);
-        program = ipClientCallback.assertProgramUpdateAndGet();
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Verify that IPv4 frame will be dropped
         setIpv4VersionFields(packet);
@@ -1927,12 +1938,6 @@
         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));
@@ -1956,24 +1961,28 @@
 
     @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,
+        ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
                 mNetworkQuirkMetrics, mDependencies);
+        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Verify initially ARP request filter is off, and GARP filter is on.
-        verifyArpFilter(ipClientCallback.assertProgramUpdateAndGet(), PASS);
+        verifyArpFilter(program, 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);
+        apfFilter.setLinkProperties(lp);
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        verifyArpFilter(program, DROP);
 
+        apfFilter.setLinkProperties(new LinkProperties());
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         // Inform ApfFilter of loss of IP and verify ARP filtering is off
-        verifyArpFilter(getProgram(ipClientCallback, apfFilter, new LinkProperties()), PASS);
+        verifyArpFilter(program, PASS);
     }
 
     private static byte[] arpReply(byte[] sip, byte[] tip) {
@@ -2015,12 +2024,12 @@
 
     @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,
+        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
                 mNetworkQuirkMetrics, mDependencies);
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         byte[] program;
         final int srcPort = 12345;
         final int dstPort = 54321;
@@ -2049,7 +2058,7 @@
         parcel.ack = ackNum;
 
         apfFilter.addTcpKeepalivePacketFilter(slot1, parcel);
-        program = cb.assertProgramUpdateAndGet();
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Verify IPv4 keepalive ack packet is dropped
         // src: 10.0.0.6, port: 54321
@@ -2071,6 +2080,7 @@
 
         // Remove IPv4 keepalive filter
         apfFilter.removeKeepalivePacketFilter(slot1);
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         try {
             // src: 2404:0:0:0:0:0:faf1, port: 12345
@@ -2088,7 +2098,7 @@
             ipv6Parcel.ack = ackNum;
 
             apfFilter.addTcpKeepalivePacketFilter(slot1, ipv6Parcel);
-            program = cb.assertProgramUpdateAndGet();
+            program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
             // Verify IPv6 keepalive ack packet is dropped
             // src: 2404:0:0:0:0:0:faf2, port: 54321
@@ -2111,7 +2121,7 @@
             // Verify multiple filters
             apfFilter.addTcpKeepalivePacketFilter(slot1, parcel);
             apfFilter.addTcpKeepalivePacketFilter(slot2, ipv6Parcel);
-            program = cb.assertProgramUpdateAndGet();
+            program = consumeInstalledProgram(mIpClientCb, 3 /* installCnt */);
 
             // Verify IPv4 keepalive ack packet is dropped
             // src: 10.0.0.6, port: 54321
@@ -2150,8 +2160,6 @@
             // TODO: support V6 packets
         }
 
-        program = cb.assertProgramUpdateAndGet();
-
         // Verify IPv4, IPv6 packets are passed
         assertPass(program,
                 ipv4TcpPacket(IPV4_KEEPALIVE_DST_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
@@ -2207,12 +2215,12 @@
 
     @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,
+        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
                 mNetworkQuirkMetrics, mDependencies);
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         byte[] program;
         final int srcPort = 1024;
         final int dstPort = 4500;
@@ -2233,7 +2241,7 @@
         parcel.dstPort = dstPort;
 
         apfFilter.addNattKeepalivePacketFilter(slot1, parcel);
-        program = cb.assertProgramUpdateAndGet();
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Verify IPv4 keepalive packet is dropped
         // src: 10.0.0.6, port: 4500
@@ -2458,10 +2466,9 @@
 
     @Test
     public void testRaToString() throws Exception {
-        MockIpClientCallback cb = new MockIpClientCallback();
         ApfConfiguration config = getDefaultConfig();
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb, mNetworkQuirkMetrics,
-                mDependencies);
+        ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
+                mNetworkQuirkMetrics, mDependencies);
 
         byte[] packet = buildLargeRa();
         ApfFilter.Ra ra = apfFilter.new Ra(packet, packet.length);
@@ -2511,29 +2518,32 @@
 
     // 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 {
+    private byte[] verifyRaLifetime(ByteBuffer packet, int lifetime)
+            throws IOException, ErrnoException {
         // Verify new program generated if ApfFilter witnesses RA
-        apfFilter.pretendPacketReceived(packet.array());
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        clearInvocations(mIpClientCb);
+        pretendPacketReceived(packet.array());
+        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         verifyRaLifetime(program, packet, lifetime);
+        return program;
     }
 
-    private void assertInvalidRa(TestApfFilter apfFilter, MockIpClientCallback ipClientCallback,
-            ByteBuffer packet) throws IOException, ErrnoException {
-        apfFilter.pretendPacketReceived(packet.array());
-        ipClientCallback.assertNoProgramUpdate();
+    private void assertInvalidRa(ByteBuffer packet)
+            throws IOException, ErrnoException, InterruptedException {
+        clearInvocations(mIpClientCb);
+        pretendPacketReceived(packet.array());
+        Thread.sleep(NO_CALLBACK_TIMEOUT_MS);
+        verify(mIpClientCb, never()).installPacketFilter(any());
     }
 
     @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,
+        ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
                 mNetworkQuirkMetrics, mDependencies);
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         final int ROUTER_LIFETIME = 1000;
         final int PREFIX_VALID_LIFETIME = 200;
@@ -2548,7 +2558,7 @@
         ByteBuffer basePacket = ByteBuffer.wrap(ra.build());
         assertPass(program, basePacket.array());
 
-        verifyRaLifetime(apfFilter, ipClientCallback, basePacket, ROUTER_LIFETIME);
+        verifyRaLifetime(basePacket, ROUTER_LIFETIME);
 
         ra = new RaPacketBuilder(ROUTER_LIFETIME);
         // Check that changes are ignored in every byte of the flow label.
@@ -2560,7 +2570,7 @@
         ra = new RaPacketBuilder(ROUTER_LIFETIME);
         ra.addZeroLengthOption();
         ByteBuffer zeroLengthOptionPacket = ByteBuffer.wrap(ra.build());
-        assertInvalidRa(apfFilter, ipClientCallback, zeroLengthOptionPacket);
+        assertInvalidRa(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
@@ -2568,43 +2578,39 @@
         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);
+        verifyRaLifetime(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);
+        verifyRaLifetime(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);
+        verifyRaLifetime(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);
+        program = verifyRaLifetime(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);
+        verifyRaLifetime(dnsslOptionPacket, ROUTER_LIFETIME);
 
         ByteBuffer largeRaPacket = ByteBuffer.wrap(buildLargeRa());
-        verifyRaLifetime(apfFilter, ipClientCallback, largeRaPacket, 300);
+        program = verifyRaLifetime(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);
@@ -2617,13 +2623,12 @@
 
     @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,
+        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
                 mNetworkQuirkMetrics, mDependencies);
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         final int RA_REACHABLE_TIME = 1800;
         final int RA_RETRANSMISSION_TIMER = 1234;
 
@@ -2637,8 +2642,8 @@
         assertPass(program, raPacket);
 
         // Assume apf is shown the given RA, it generates program to filter it.
-        apfFilter.pretendPacketReceived(raPacket);
-        program = ipClientCallback.assertProgramUpdateAndGet();
+        pretendPacketReceived(raPacket);
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         assertDrop(program, raPacket);
 
         // A packet with different reachable time should be passed.
@@ -2659,13 +2664,12 @@
     @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,
+        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
                 mNetworkQuirkMetrics, mDependencies);
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         final int routerLifetime = 1000;
         final int timePassedSeconds = 12;
@@ -2673,26 +2677,32 @@
         // 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);
+        verifyRaLifetime(basePacket, routerLifetime);
+
+        mCurrentTimeMs += timePassedSeconds * DateUtils.SECOND_IN_MILLIS;
+        doReturn(mCurrentTimeMs).when(mDependencies).elapsedRealtime();
         synchronized (apfFilter) {
             apfFilter.installNewProgramLocked();
         }
-        program = ipClientCallback.assertProgramUpdateAndGet();
+        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         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);
+        mCurrentTimeMs +=
+                ((routerLifetime / 6) - timePassedSeconds - 1) * DateUtils.SECOND_IN_MILLIS;
+        doReturn(mCurrentTimeMs).when(mDependencies).elapsedRealtime();
         synchronized (apfFilter) {
             apfFilter.installNewProgramLocked();
         }
-        program = ipClientCallback.assertProgramUpdateAndGet();
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         assertDrop(program, basePacket.array());
-        apfFilter.increaseCurrentTimeSeconds(1);
+
+        mCurrentTimeMs += DateUtils.SECOND_IN_MILLIS;
+        doReturn(mCurrentTimeMs).when(mDependencies).elapsedRealtime();
         synchronized (apfFilter) {
             apfFilter.installNewProgramLocked();
         }
-        program = ipClientCallback.assertProgramUpdateAndGet();
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         assertPass(program, basePacket.array());
     }
 
@@ -2727,12 +2737,11 @@
     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);
+        ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
+                mNetworkQuirkMetrics, mDependencies);
         for (int i = 0; i < 1000; i++) {
             byte[] packet = new byte[r.nextInt(maxRandomPacketSize + 1)];
             r.nextBytes(packet);
@@ -2749,12 +2758,11 @@
     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);
+        ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
+                mNetworkQuirkMetrics, mDependencies);
         for (int i = 0; i < 1000; i++) {
             byte[] packet = new byte[r.nextInt(maxRandomPacketSize + 1)];
             r.nextBytes(packet);
@@ -2768,34 +2776,34 @@
 
     @Test
     public void testMatchedRaUpdatesLifetime() throws Exception {
-        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
-        final TestApfFilter apfFilter = new TestApfFilter(mContext, getDefaultConfig(),
-                ipClientCallback, mNetworkQuirkMetrics, mDependencies);
+        final ApfFilter apfFilter = new ApfFilter(mContext, getDefaultConfig(), TEST_PARAMS,
+                mIpClientCb, mNetworkQuirkMetrics, mDependencies);
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Create an RA and build an APF program
         byte[] ra = new RaPacketBuilder(1800 /* router lifetime */).build();
-        apfFilter.pretendPacketReceived(ra);
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        pretendPacketReceived(ra);
+        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // 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();
+        pretendPacketReceived(ra);
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // 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;
+        ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
+                mNetworkQuirkMetrics, mDependencies);
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         // 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)
@@ -2843,13 +2851,11 @@
         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();
+            pretendPacketReceived(ra);
+            consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         }
     }
 
@@ -2857,20 +2863,20 @@
     // 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,
+        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
                 mNetworkQuirkMetrics, mDependencies);
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // 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();
+        pretendPacketReceived(ra);
+        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // repeated RA is dropped
         assertDrop(program, ra);
@@ -2886,20 +2892,20 @@
     // 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,
+        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
                 mNetworkQuirkMetrics, mDependencies);
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // 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();
+        pretendPacketReceived(ra);
+        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // repeated RA is dropped
         assertDrop(program, ra);
@@ -2922,18 +2928,18 @@
     // 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,
+        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
                 mNetworkQuirkMetrics, mDependencies);
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Create an initial RA and build an APF program
         byte[] ra = new RaPacketBuilder(0 /* router lifetime */).build();
 
-        apfFilter.pretendPacketReceived(ra);
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        pretendPacketReceived(ra);
+        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // repeated RA is dropped
         assertDrop(program, ra);
@@ -2947,23 +2953,22 @@
         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,
+        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
                 mNetworkQuirkMetrics, mDependencies);
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Create an initial RA and build an APF program
         byte[] ra = new RaPacketBuilder(100 /* router lifetime */).build();
 
-        apfFilter.pretendPacketReceived(ra);
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        pretendPacketReceived(ra);
+        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // repeated RA is dropped
         assertDrop(program, ra);
@@ -2989,18 +2994,18 @@
     // 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,
+        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
                 mNetworkQuirkMetrics, mDependencies);
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Create an initial RA and build an APF program
         byte[] ra = new RaPacketBuilder(200 /* router lifetime */).build();
 
-        apfFilter.pretendPacketReceived(ra);
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        pretendPacketReceived(ra);
+        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // repeated RA is dropped
         assertDrop(program, ra);
@@ -3022,18 +3027,18 @@
     // 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,
+        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
                 mNetworkQuirkMetrics, mDependencies);
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Create an initial RA and build an APF program
         byte[] ra = new RaPacketBuilder(1800 /* router lifetime */).build();
 
-        apfFilter.pretendPacketReceived(ra);
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        pretendPacketReceived(ra);
+        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // repeated RA is dropped
         assertDrop(program, ra);
@@ -3061,17 +3066,17 @@
 
     @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,
+        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
                 mNetworkQuirkMetrics, mDependencies);
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Create an initial RA and build an APF program
         byte[] ra = new RaPacketBuilder(1800 /* router lifetime */).build();
-        apfFilter.pretendPacketReceived(ra);
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        pretendPacketReceived(ra);
+        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // repeated RA is dropped.
         assertDrop(program, ra);
@@ -3079,38 +3084,38 @@
         // 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();
+        pretendPacketReceived(ra);
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         assertDrop(program, ra);
 
         ra = new RaPacketBuilder(180 /* router lifetime */).build();
         assertPass(program, ra);
-        apfFilter.pretendPacketReceived(ra);
-        program = ipClientCallback.assertProgramUpdateAndGet();
+        pretendPacketReceived(ra);
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         assertDrop(program, ra);
 
         ra = new RaPacketBuilder(0 /* router lifetime */).build();
         assertPass(program, ra);
-        apfFilter.pretendPacketReceived(ra);
-        program = ipClientCallback.assertProgramUpdateAndGet();
+        pretendPacketReceived(ra);
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         assertDrop(program, ra);
 
         ra = new RaPacketBuilder(180 /* router lifetime */).build();
         assertPass(program, ra);
-        apfFilter.pretendPacketReceived(ra);
-        program = ipClientCallback.assertProgramUpdateAndGet();
+        pretendPacketReceived(ra);
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         assertDrop(program, ra);
 
         ra = new RaPacketBuilder(599 /* router lifetime */).build();
         assertPass(program, ra);
-        apfFilter.pretendPacketReceived(ra);
-        program = ipClientCallback.assertProgramUpdateAndGet();
+        pretendPacketReceived(ra);
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         assertDrop(program, ra);
 
         ra = new RaPacketBuilder(1800 /* router lifetime */).build();
         assertPass(program, ra);
-        apfFilter.pretendPacketReceived(ra);
-        program = ipClientCallback.assertProgramUpdateAndGet();
+        pretendPacketReceived(ra);
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         assertDrop(program, ra);
     }
 
@@ -3133,18 +3138,12 @@
         assertEquals(want, got);
     }
 
-    private TestAndroidPacketFilter makeTestApfFilter(ApfConfiguration config,
-            MockIpClientCallback ipClientCallback) throws Exception {
-        return new TestApfFilter(mContext, config, ipClientCallback, mNetworkQuirkMetrics,
-                    mDependencies, mClock);
-    }
-
-
     @Test
     public void testInstallPacketFilterFailure() throws Exception {
-        final MockIpClientCallback ipClientCallback = new MockIpClientCallback(false);
+        doReturn(false).when(mIpClientCb).installPacketFilter(any());
         final ApfConfiguration config = getDefaultConfig();
-        final TestAndroidPacketFilter apfFilter = makeTestApfFilter(config, ipClientCallback);
+        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
+                mNetworkQuirkMetrics, mDependencies);
         verify(mNetworkQuirkMetrics).setEvent(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
         verify(mNetworkQuirkMetrics).statsWrite();
         reset(mNetworkQuirkMetrics);
@@ -3158,28 +3157,27 @@
 
     @Test
     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);
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        config.apfVersionSupported = 2;
+        config.apfRamSize = 512;
+        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
+                mNetworkQuirkMetrics, mDependencies);
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         final byte[] ra = buildLargeRa();
-        apfFilter.pretendPacketReceived(ra);
+        pretendPacketReceived(ra);
         // The generated program size will be 529, which is larger than 512
-        program = ipClientCallback.assertProgramUpdateAndGet();
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         verify(mNetworkQuirkMetrics).setEvent(NetworkQuirkEvent.QE_APF_OVER_SIZE_FAILURE);
         verify(mNetworkQuirkMetrics).statsWrite();
     }
 
     @Test
     public void testGenerateApfProgramException() throws Exception {
-        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
         final ApfConfiguration config = getDefaultConfig();
-        final TestAndroidPacketFilter apfFilter;
-        apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mNetworkQuirkMetrics,
-                mDependencies, true /* throwsExceptionWhenGeneratesProgram */);
+        ApfFilter apfFilter = spy(new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
+                mNetworkQuirkMetrics, mDependencies));
         synchronized (apfFilter) {
+            when(apfFilter.emitPrologueLocked()).thenThrow(new IllegalStateException("test"));
             apfFilter.installNewProgramLocked();
         }
         verify(mNetworkQuirkMetrics).setEvent(NetworkQuirkEvent.QE_APF_GENERATE_FILTER_EXCEPTION);
@@ -3188,17 +3186,17 @@
 
     @Test
     public void testApfSessionInfoMetrics() throws Exception {
-        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
         final ApfConfiguration config = getDefaultConfig();
-        final ApfCapabilities capabilities = new ApfCapabilities(4, 4096, ARPHRD_ETHER);
-        config.apfCapabilities = capabilities;
+        config.apfVersionSupported = 4;
+        config.apfRamSize = 4096;
         final long startTimeMs = 12345;
         final long durationTimeMs = config.minMetricsSessionDurationMs;
-        doReturn(startTimeMs).when(mClock).elapsedRealtime();
-        final TestAndroidPacketFilter apfFilter = makeTestApfFilter(config, ipClientCallback);
+        doReturn(startTimeMs).when(mDependencies).elapsedRealtime();
+        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
+                mNetworkQuirkMetrics, mDependencies);
+        byte[] program = consumeInstalledProgram(mIpClientCb, 2 /* installCnt */);
         int maxProgramSize = 0;
         int numProgramUpdated = 0;
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
         maxProgramSize = Math.max(maxProgramSize, program.length);
         numProgramUpdated++;
 
@@ -3215,14 +3213,14 @@
         expectedData[totalPacketsCounterIdx + 3] += 1;
         expectedData[passedIpv6IcmpCounterIdx + 3] += 1;
         assertDataMemoryContentsIgnoreVersion(PASS, program, ra, data, expectedData);
-        apfFilter.pretendPacketReceived(ra);
-        program = ipClientCallback.assertProgramUpdateAndGet();
+        pretendPacketReceived(ra);
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         maxProgramSize = Math.max(maxProgramSize, program.length);
         numProgramUpdated++;
 
         apfFilter.setMulticastFilter(true);
         // setMulticastFilter will trigger program installation.
-        program = ipClientCallback.assertProgramUpdateAndGet();
+        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
         maxProgramSize = Math.max(maxProgramSize, program.length);
         numProgramUpdated++;
 
@@ -3239,7 +3237,7 @@
         apfFilter.setDataSnapshot(data);
 
         // Write metrics data to statsd pipeline when shutdown.
-        doReturn(startTimeMs + durationTimeMs).when(mClock).elapsedRealtime();
+        doReturn(startTimeMs + durationTimeMs).when(mDependencies).elapsedRealtime();
         apfFilter.shutdown();
         verify(mApfSessionInfoMetrics).setVersion(4);
         verify(mApfSessionInfoMetrics).setMemorySize(4096);
@@ -3268,13 +3266,13 @@
 
     @Test
     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);
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
+        doReturn(startTimeMs).when(mDependencies).elapsedRealtime();
+        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
+                mNetworkQuirkMetrics, mDependencies);
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         final int routerLifetime = 1000;
         final int prefixValidLifetime = 200;
@@ -3311,23 +3309,24 @@
 
         // 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.assertProgramUpdateAndGet();
-        apfFilter.pretendPacketReceived(raZeroPioValidLifetime.build());
-        ipClientCallback.assertProgramUpdateAndGet();
-        apfFilter.pretendPacketReceived(raZeroRdnssLifetime.build());
-        ipClientCallback.assertProgramUpdateAndGet();
-        apfFilter.pretendPacketReceived(raZeroRioRouteLifetime.build());
-        ipClientCallback.assertProgramUpdateAndGet();
+        pretendPacketReceived(ra1.build());
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        pretendPacketReceived(ra2.build());
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        pretendPacketReceived(raInvalid.build());
+        Thread.sleep(NO_CALLBACK_TIMEOUT_MS);
+        verify(mIpClientCb, never()).installPacketFilter(any());
+        pretendPacketReceived(raZeroRouterLifetime.build());
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        pretendPacketReceived(raZeroPioValidLifetime.build());
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        pretendPacketReceived(raZeroRdnssLifetime.build());
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        pretendPacketReceived(raZeroRioRouteLifetime.build());
+        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
 
         // Write metrics data to statsd pipeline when shutdown.
-        doReturn(startTimeMs + durationTimeMs).when(mClock).elapsedRealtime();
+        doReturn(startTimeMs + durationTimeMs).when(mDependencies).elapsedRealtime();
         apfFilter.shutdown();
 
         // Verify each metric fields in IpClientRaInfoMetrics.
@@ -3342,26 +3341,25 @@
     }
 
     private void verifyNoMetricsWrittenForShortDuration(boolean isLegacy) 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();
+        doReturn(startTimeMs).when(mDependencies).elapsedRealtime();
+        final ApfFilter apfFilter = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
+                mNetworkQuirkMetrics, mDependencies);
+        doReturn(startTimeMs + durationTimeMs - 1).when(mDependencies).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();
+        doReturn(startTimeMs).when(mDependencies).elapsedRealtime();
+        final ApfFilter apfFilter2 = new ApfFilter(mContext, config, TEST_PARAMS, mIpClientCb,
+                mNetworkQuirkMetrics, mDependencies);
+        doReturn(startTimeMs + durationTimeMs).when(mDependencies).elapsedRealtime();
         apfFilter2.shutdown();
         verify(mApfSessionInfoMetrics).statsWrite();
         verify(mIpClientRaInfoMetrics).statsWrite();
@@ -3388,15 +3386,18 @@
 
     @Test
     public void testApfGeneratorPropagation() throws IllegalInstructionException {
-        ApfV4Generator v4Gen = new ApfV4Generator(APF_VERSION_3);
-        ApfV6Generator v6Gen = new ApfV6Generator(1024);
+        ApfV4Generator v4Gen = new ApfV4Generator(APF_VERSION_3, 1024 /* ramSize */,
+                1024 /* clampSize */);
+        ApfV6Generator v6Gen = new ApfV6Generator(APF_VERSION_6, 1024 /* ramSize */,
+                1024 /* clampSize */);
         assertEquals(4, deriveApfGeneratorVersion(v4Gen));
         assertEquals(6, deriveApfGeneratorVersion(v6Gen));
     }
 
     @Test
     public void testFullApfV4ProgramGenerationIPV6() throws IllegalInstructionException {
-        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_3);
+        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_3, 1024 /* ramSize */,
+                1024 /* clampSize */);
         gen.addLoadImmediate(R1, -4);
         gen.addLoadData(R0, 0);
         gen.addAdd(1);
@@ -3549,7 +3550,8 @@
 
     @Test
     public void testFullApfV4ProgramGenerationIPV4() throws IllegalInstructionException {
-        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_3);
+        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_3, 1024 /* ramSize */,
+                1024 /* clampSize */);
         gen.addLoadImmediate(R1, -4);
         gen.addLoadData(R0, 0);
         gen.addAdd(1);
@@ -3670,7 +3672,7 @@
 
     @Test
     public void testFullApfV4ProgramGenerationNatTKeepAliveV4() throws IllegalInstructionException {
-        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_3, true);
+        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_3, mRamSize, mClampSize, true);
         gen.addLoadImmediate(R1, -4);
         gen.addLoadData(R0, 0);
         gen.addAdd(1);
@@ -3785,7 +3787,8 @@
 
     @Test
     public void testInfiniteLifetimeFullApfV4ProgramGeneration() throws IllegalInstructionException {
-        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_3, true);
+        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_3, 1024 /* ramSize */,
+                1024 /* clampSize */, true);
         gen.addLoadCounter(R0, getCounterEnumFromOffset(-8));
         gen.addAdd(1);
         gen.addStoreData(R0, 0);
diff --git a/tests/unit/src/android/net/apf/ApfTestHelpers.kt b/tests/unit/src/android/net/apf/ApfTestHelpers.kt
new file mode 100644
index 0000000..30d5813
--- /dev/null
+++ b/tests/unit/src/android/net/apf/ApfTestHelpers.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.apf
+
+import android.net.apf.ApfCounterTracker.Counter
+import android.net.apf.ApfCounterTracker.Counter.APF_PROGRAM_ID
+import android.net.apf.ApfCounterTracker.Counter.APF_VERSION
+import android.net.apf.ApfCounterTracker.Counter.TOTAL_PACKETS
+import android.net.apf.ApfTestUtils.DROP
+import android.net.apf.ApfTestUtils.PASS
+import android.net.apf.ApfTestUtils.assertVerdict
+import android.net.apf.BaseApfGenerator.APF_VERSION_6
+import android.net.ip.IpClient
+import com.android.net.module.util.HexDump
+import kotlin.test.assertEquals
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+
+class ApfTestHelpers private constructor() {
+    companion object {
+        const val TIMEOUT_MS: Long = 1000
+        fun verifyProgramRun(
+            version: Int,
+            program: ByteArray,
+            pkt: ByteArray,
+            targetCnt: Counter,
+            cntMap: MutableMap<Counter, Long> = mutableMapOf(),
+            dataRegion: ByteArray = ByteArray(Counter.totalSize()) { 0 },
+            incTotal: Boolean = true,
+            result: Int = if (targetCnt.name.startsWith("PASSED")) PASS else DROP
+        ) {
+            assertVerdict(version, result, program, pkt, dataRegion)
+            cntMap[targetCnt] = cntMap.getOrDefault(targetCnt, 0) + 1
+            if (incTotal) {
+                cntMap[TOTAL_PACKETS] = cntMap.getOrDefault(TOTAL_PACKETS, 0) + 1
+            }
+            val errMsg = "Counter is not increased properly. To debug: \n" +
+                    " apf_run --program ${HexDump.toHexString(program)} " +
+                    "--packet ${HexDump.toHexString(pkt)} " +
+                    "--data ${HexDump.toHexString(dataRegion)} --age 0 " +
+                    "${if (version == APF_VERSION_6) "--v6" else "" } --trace  | less \n"
+            assertEquals(cntMap, decodeCountersIntoMap(dataRegion), errMsg)
+        }
+
+        fun decodeCountersIntoMap(counterBytes: ByteArray): Map<Counter, Long> {
+            val counters = Counter::class.java.enumConstants
+            val ret = HashMap<Counter, Long>()
+            val skippedCounters = setOf(APF_PROGRAM_ID, APF_VERSION)
+            // starting from index 2 to skip the endianness mark
+            if (counters != null) {
+                for (c in listOf(*counters).subList(2, counters.size)) {
+                    if (c in skippedCounters) continue
+                    val value = ApfCounterTracker.getCounterValue(counterBytes, c)
+                    if (value != 0L) {
+                        ret[c] = value
+                    }
+                }
+            }
+            return ret
+        }
+
+        @JvmStatic
+        fun consumeInstalledProgram(
+            ipClientCb: IpClient.IpClientCallbacksWrapper,
+            installCnt: Int
+        ): ByteArray {
+            val programCaptor = ArgumentCaptor.forClass(
+                ByteArray::class.java
+            )
+
+            verify(ipClientCb, timeout(TIMEOUT_MS).times(installCnt)).installPacketFilter(
+                programCaptor.capture()
+            )
+
+            clearInvocations<Any>(ipClientCb)
+            return programCaptor.value
+        }
+    }
+}
diff --git a/tests/unit/src/android/net/apf/ApfTestUtils.java b/tests/unit/src/android/net/apf/ApfTestUtils.java
index 0b3ea65..fc9fdef 100644
--- a/tests/unit/src/android/net/apf/ApfTestUtils.java
+++ b/tests/unit/src/android/net/apf/ApfTestUtils.java
@@ -16,40 +16,11 @@
 package android.net.apf;
 
 import static android.net.apf.ApfJniUtils.apfSimulate;
-import static android.system.OsConstants.AF_UNIX;
-import static android.system.OsConstants.SOCK_STREAM;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-import static org.mockito.Mockito.mock;
 
-import android.content.Context;
-import android.net.LinkAddress;
-import android.net.LinkProperties;
-import android.net.apf.BaseApfGenerator.IllegalInstructionException;
-import android.net.ip.IIpClientCallbacks;
-import android.net.ip.IpClient;
-import android.net.metrics.IpConnectivityLog;
-import android.os.ConditionVariable;
-import android.os.SystemClock;
-import android.system.ErrnoException;
-import android.system.Os;
-import android.text.format.DateUtils;
-
-import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.HexDump;
-import com.android.net.module.util.InterfaceParams;
-import com.android.net.module.util.SharedLog;
-import com.android.networkstack.apishim.NetworkInformationShimImpl;
-import com.android.networkstack.metrics.NetworkQuirkMetrics;
 
-import libcore.io.IoUtils;
-
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.net.InetAddress;
 import java.util.Arrays;
 
 /**
@@ -237,270 +208,4 @@
             throws ApfV4Generator.IllegalInstructionException {
         assertVerdict(apfVersion, DROP, gen, new byte[MIN_PKT_SIZE], 0);
     }
-
-    /**
-     * The Mock ip client callback class.
-     */
-    public static class MockIpClientCallback extends IpClient.IpClientCallbacksWrapper {
-        private final ConditionVariable mGotApfProgram = new ConditionVariable();
-        private byte[] mLastApfProgram;
-        private boolean mInstallPacketFilterReturn = true;
-
-        MockIpClientCallback() {
-            super(mock(IIpClientCallbacks.class), mock(SharedLog.class), mock(SharedLog.class),
-                    NetworkInformationShimImpl.newInstance(), false);
-        }
-
-        MockIpClientCallback(boolean installPacketFilterReturn) {
-            super(mock(IIpClientCallbacks.class), mock(SharedLog.class), mock(SharedLog.class),
-                    NetworkInformationShimImpl.newInstance(), false);
-            mInstallPacketFilterReturn = installPacketFilterReturn;
-        }
-
-        @Override
-        public boolean installPacketFilter(byte[] filter) {
-            mLastApfProgram = filter;
-            mGotApfProgram.open();
-            return mInstallPacketFilterReturn;
-        }
-
-        /**
-         * Reset the apf program and wait for the next update.
-         */
-        public void resetApfProgramWait() {
-            mGotApfProgram.close();
-        }
-
-        /**
-         * Assert the program is update within TIMEOUT_MS and return the program.
-         */
-        public byte[] assertProgramUpdateAndGet() {
-            assertTrue(mGotApfProgram.block(TIMEOUT_MS));
-            return mLastApfProgram;
-        }
-
-        /**
-         * Assert the program is not update within TIMEOUT_MS.
-         */
-        public void assertNoProgramUpdate() {
-            assertFalse(mGotApfProgram.block(TIMEOUT_MS));
-        }
-    }
-
-    /**
-     * The test apf filter class.
-     */
-    public static class TestApfFilter extends ApfFilter implements TestAndroidPacketFilter {
-        public static final byte[] MOCK_MAC_ADDR = {2, 3, 4, 5, 6, 7};
-        private static final byte[] MOCK_IPV4_ADDR = {10, 0, 0, 1};
-
-        private FileDescriptor mWriteSocket;
-        private long mCurrentTimeMs = SystemClock.elapsedRealtime();
-        private final MockIpClientCallback mMockIpClientCb;
-        private final boolean mThrowsExceptionWhenGeneratesProgram;
-
-        public TestApfFilter(Context context, ApfConfiguration config,
-                MockIpClientCallback ipClientCallback, NetworkQuirkMetrics networkQuirkMetrics,
-                Dependencies dependencies) throws Exception {
-            this(context, config, ipClientCallback, networkQuirkMetrics, dependencies,
-                    false /* throwsExceptionWhenGeneratesProgram */, new ApfFilter.Clock());
-        }
-
-        public TestApfFilter(Context context, ApfConfiguration config,
-                MockIpClientCallback ipClientCallback, NetworkQuirkMetrics networkQuirkMetrics,
-                Dependencies dependencies, boolean throwsExceptionWhenGeneratesProgram)
-                throws Exception {
-            this(context, config, ipClientCallback, networkQuirkMetrics, dependencies,
-                    throwsExceptionWhenGeneratesProgram, new ApfFilter.Clock());
-        }
-
-        public TestApfFilter(Context context, ApfConfiguration config,
-                MockIpClientCallback ipClientCallback, NetworkQuirkMetrics networkQuirkMetrics,
-                Dependencies dependencies, ApfFilter.Clock clock) throws Exception {
-            this(context, config, ipClientCallback, networkQuirkMetrics, dependencies,
-                    false /* throwsExceptionWhenGeneratesProgram */, clock);
-        }
-
-        public TestApfFilter(Context context, ApfConfiguration config,
-                MockIpClientCallback ipClientCallback, NetworkQuirkMetrics networkQuirkMetrics,
-                Dependencies dependencies, boolean throwsExceptionWhenGeneratesProgram,
-                ApfFilter.Clock clock) throws Exception {
-            super(context, config, InterfaceParams.getByName("lo"), ipClientCallback,
-                    networkQuirkMetrics, dependencies, clock);
-            mMockIpClientCb = ipClientCallback;
-            mThrowsExceptionWhenGeneratesProgram = throwsExceptionWhenGeneratesProgram;
-        }
-
-        /**
-         * Create a new test ApfFiler.
-         */
-        public static ApfFilter createTestApfFilter(Context context,
-                MockIpClientCallback ipClientCallback, ApfConfiguration config,
-                NetworkQuirkMetrics networkQuirkMetrics, ApfFilter.Dependencies dependencies)
-                throws Exception {
-            LinkAddress link = new LinkAddress(InetAddress.getByAddress(MOCK_IPV4_ADDR), 19);
-            LinkProperties lp = new LinkProperties();
-            lp.addLinkAddress(link);
-            TestApfFilter apfFilter = new TestApfFilter(context, config, ipClientCallback,
-                    networkQuirkMetrics, dependencies);
-            apfFilter.setLinkProperties(lp);
-            return apfFilter;
-        }
-
-        /**
-         * Pretend an RA packet has been received and show it to ApfFilter.
-         */
-        public void pretendPacketReceived(byte[] packet) throws IOException, ErrnoException {
-            mMockIpClientCb.resetApfProgramWait();
-            // ApfFilter's ReceiveThread will be waiting to read this.
-            Os.write(mWriteSocket, packet, 0, packet.length);
-        }
-
-        /**
-         * Simulate current time changes.
-         */
-        public void increaseCurrentTimeSeconds(int delta) {
-            mCurrentTimeMs += delta * DateUtils.SECOND_IN_MILLIS;
-        }
-
-        @Override
-        protected int secondsSinceBoot() {
-            return (int) (mCurrentTimeMs / DateUtils.SECOND_IN_MILLIS);
-        }
-
-        @Override
-        public synchronized void maybeStartFilter() {
-            mHardwareAddress = MOCK_MAC_ADDR;
-            installNewProgramLocked();
-
-            // Create two sockets, "readSocket" and "mWriteSocket" and connect them together.
-            FileDescriptor readSocket = new FileDescriptor();
-            mWriteSocket = new FileDescriptor();
-            try {
-                Os.socketpair(AF_UNIX, SOCK_STREAM, 0, mWriteSocket, readSocket);
-            } catch (ErrnoException e) {
-                fail();
-                return;
-            }
-            // Now pass readSocket to ReceiveThread as if it was setup to read raw RAs.
-            // This allows us to pretend RA packets have been received via pretendPacketReceived().
-            mReceiveThread = new ReceiveThread(readSocket);
-            mReceiveThread.start();
-        }
-
-        @Override
-        public synchronized void shutdown() {
-            super.shutdown();
-            if (mReceiveThread != null) {
-                mReceiveThread.halt();
-                mReceiveThread = null;
-            }
-            IoUtils.closeQuietly(mWriteSocket);
-        }
-
-        @Override
-        @GuardedBy("this")
-        protected ApfV4GeneratorBase<?> emitPrologueLocked() throws IllegalInstructionException {
-            if (mThrowsExceptionWhenGeneratesProgram) {
-                throw new IllegalStateException();
-            }
-            return super.emitPrologueLocked();
-        }
-    }
-
-    /**
-     * The test legacy apf filter class.
-     */
-    public static class TestLegacyApfFilter extends LegacyApfFilter
-            implements TestAndroidPacketFilter {
-        public static final byte[] MOCK_MAC_ADDR = {1, 2, 3, 4, 5, 6};
-        private static final byte[] MOCK_IPV4_ADDR = {10, 0, 0, 1};
-
-        private FileDescriptor mWriteSocket;
-        private final MockIpClientCallback mMockIpClientCb;
-        private final boolean mThrowsExceptionWhenGeneratesProgram;
-
-        public TestLegacyApfFilter(Context context, ApfFilter.ApfConfiguration config,
-                MockIpClientCallback ipClientCallback, IpConnectivityLog ipConnectivityLog,
-                NetworkQuirkMetrics networkQuirkMetrics) throws Exception {
-            this(context, config, ipClientCallback, ipConnectivityLog, networkQuirkMetrics,
-                    new ApfFilter.Dependencies(context),
-                    false /* throwsExceptionWhenGeneratesProgram */, new ApfFilter.Clock());
-        }
-
-        public TestLegacyApfFilter(Context context, ApfFilter.ApfConfiguration config,
-                MockIpClientCallback ipClientCallback, IpConnectivityLog ipConnectivityLog,
-                NetworkQuirkMetrics networkQuirkMetrics, ApfFilter.Dependencies dependencies,
-                boolean throwsExceptionWhenGeneratesProgram) throws Exception {
-            this(context, config, ipClientCallback, ipConnectivityLog, networkQuirkMetrics,
-                    dependencies, throwsExceptionWhenGeneratesProgram, new ApfFilter.Clock());
-        }
-
-        public TestLegacyApfFilter(Context context, ApfFilter.ApfConfiguration config,
-                MockIpClientCallback ipClientCallback, IpConnectivityLog ipConnectivityLog,
-                NetworkQuirkMetrics networkQuirkMetrics, ApfFilter.Dependencies dependencies,
-                ApfFilter.Clock clock) throws Exception {
-            this(context, config, ipClientCallback, ipConnectivityLog, networkQuirkMetrics,
-                    dependencies, false /* throwsExceptionWhenGeneratesProgram */, clock);
-        }
-
-        public TestLegacyApfFilter(Context context, ApfFilter.ApfConfiguration config,
-                MockIpClientCallback ipClientCallback, IpConnectivityLog ipConnectivityLog,
-                NetworkQuirkMetrics networkQuirkMetrics, ApfFilter.Dependencies dependencies,
-                boolean throwsExceptionWhenGeneratesProgram, ApfFilter.Clock clock)
-                throws Exception {
-            super(context, config, InterfaceParams.getByName("lo"), ipClientCallback,
-                    ipConnectivityLog, networkQuirkMetrics, dependencies, clock);
-            mMockIpClientCb = ipClientCallback;
-            mThrowsExceptionWhenGeneratesProgram = throwsExceptionWhenGeneratesProgram;
-        }
-
-        /**
-         * Pretend an RA packet has been received and show it to LegacyApfFilter.
-         */
-        public void pretendPacketReceived(byte[] packet) throws IOException, ErrnoException {
-            mMockIpClientCb.resetApfProgramWait();
-            // ApfFilter's ReceiveThread will be waiting to read this.
-            Os.write(mWriteSocket, packet, 0, packet.length);
-        }
-
-        @Override
-        public synchronized void maybeStartFilter() {
-            mHardwareAddress = MOCK_MAC_ADDR;
-            installNewProgramLocked();
-
-            // Create two sockets, "readSocket" and "mWriteSocket" and connect them together.
-            FileDescriptor readSocket = new FileDescriptor();
-            mWriteSocket = new FileDescriptor();
-            try {
-                Os.socketpair(AF_UNIX, SOCK_STREAM, 0, mWriteSocket, readSocket);
-            } catch (ErrnoException e) {
-                fail();
-                return;
-            }
-            // Now pass readSocket to ReceiveThread as if it was setup to read raw RAs.
-            // This allows us to pretend RA packets have been received via pretendPacketReceived().
-            mReceiveThread = new ReceiveThread(readSocket);
-            mReceiveThread.start();
-        }
-
-        @Override
-        public synchronized void shutdown() {
-            super.shutdown();
-            if (mReceiveThread != null) {
-                mReceiveThread.halt();
-                mReceiveThread = null;
-            }
-            IoUtils.closeQuietly(mWriteSocket);
-        }
-
-        @Override
-        @GuardedBy("this")
-        protected ApfV4Generator emitPrologueLocked() throws IllegalInstructionException {
-            if (mThrowsExceptionWhenGeneratesProgram) {
-                throw new IllegalStateException();
-            }
-            return super.emitPrologueLocked();
-        }
-    }
 }
diff --git a/tests/unit/src/android/net/apf/Bpf2Apf.java b/tests/unit/src/android/net/apf/Bpf2Apf.java
index 49c241e..4dee2f6 100644
--- a/tests/unit/src/android/net/apf/Bpf2Apf.java
+++ b/tests/unit/src/android/net/apf/Bpf2Apf.java
@@ -16,6 +16,7 @@
 
 package android.net.apf;
 
+import static android.net.apf.BaseApfGenerator.APF_VERSION_3;
 import static android.net.apf.BaseApfGenerator.MemorySlot;
 import static android.net.apf.BaseApfGenerator.Register.R0;
 import static android.net.apf.BaseApfGenerator.Register.R1;
@@ -39,6 +40,8 @@
  *                                      android.net.apf.Bpf2Apf
  */
 public class Bpf2Apf {
+    private static int sRamSize = 1024;
+    private static int sClampSize = 1024;
     private static int parseImm(String line, String arg) {
         if (!arg.startsWith("#0x")) {
             throw new IllegalArgumentException("Unhandled instruction: " + line);
@@ -316,7 +319,7 @@
      * program and return it.
      */
     public static byte[] convert(String bpf) throws IllegalInstructionException {
-        ApfV4Generator gen = new ApfV4Generator(3);
+        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_3, sRamSize, sClampSize);
         for (String line : bpf.split("\\n")) convertLine(line, gen);
         return gen.generate();
     }
@@ -329,7 +332,7 @@
         BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
         String line = null;
         StringBuilder responseData = new StringBuilder();
-        ApfV4Generator gen = new ApfV4Generator(3);
+        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_3, sRamSize, sClampSize);
         while ((line = in.readLine()) != null) convertLine(line, gen);
         System.out.write(gen.generate());
     }
diff --git a/tests/unit/src/android/net/apf/LegacyApfTest.java b/tests/unit/src/android/net/apf/LegacyApfTest.java
index cb3fbca..2504604 100644
--- a/tests/unit/src/android/net/apf/LegacyApfTest.java
+++ b/tests/unit/src/android/net/apf/LegacyApfTest.java
@@ -19,15 +19,15 @@
 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.net.apf.ApfTestUtils.TIMEOUT_MS;
+import static android.system.OsConstants.AF_UNIX;
 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 android.system.OsConstants.SOCK_STREAM;
 
 import static com.android.net.module.util.HexDump.hexStringToByteArray;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
@@ -35,7 +35,7 @@
 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.junit.Assert.fail;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
@@ -45,9 +45,7 @@
 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;
@@ -55,14 +53,15 @@
 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.ip.IIpClientCallbacks;
+import android.net.ip.IpClient;
 import android.net.metrics.IpConnectivityLog;
 import android.os.Build;
+import android.os.ConditionVariable;
 import android.os.PowerManager;
 import android.stats.connectivity.NetworkQuirkEvent;
 import android.system.ErrnoException;
+import android.system.Os;
 import android.text.format.DateUtils;
 import android.util.ArrayMap;
 import android.util.Log;
@@ -72,8 +71,10 @@
 
 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.InterfaceParams;
 import com.android.net.module.util.NetworkStackConstants;
+import com.android.net.module.util.SharedLog;
+import com.android.networkstack.apishim.NetworkInformationShimImpl;
 import com.android.networkstack.metrics.ApfSessionInfoMetrics;
 import com.android.networkstack.metrics.IpClientRaInfoMetrics;
 import com.android.networkstack.metrics.NetworkQuirkMetrics;
@@ -82,6 +83,7 @@
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRunner;
 
+import libcore.io.IoUtils;
 import libcore.io.Streams;
 
 import org.junit.After;
@@ -97,6 +99,7 @@
 
 import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.FileDescriptor;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -209,9 +212,6 @@
     }
 
     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;
@@ -237,7 +237,8 @@
 
     private static ApfConfiguration getDefaultConfig() {
         ApfFilter.ApfConfiguration config = new ApfConfiguration();
-        config.apfCapabilities = MOCK_APF_CAPABILITIES;
+        config.apfVersionSupported = 2;
+        config.apfRamSize = 4096;
         config.multicastFilter = ALLOW_MULTICAST;
         config.ieee802_3Filter = ALLOW_802_3_FRAMES;
         config.ethTypeBlackList = new int[0];
@@ -316,12 +317,12 @@
         lp.addLinkAddress(link);
 
         ApfConfiguration config = getDefaultConfig();
-        ApfCapabilities MOCK_APF_PCAP_CAPABILITIES = new ApfCapabilities(4, 1700, ARPHRD_ETHER);
-        config.apfCapabilities = MOCK_APF_PCAP_CAPABILITIES;
+        config.apfVersionSupported = 4;
+        config.apfRamSize = 1700;
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
-                mNetworkQuirkMetrics, mDependencies);
+        TestLegacyApfFilter apfFilter = new TestLegacyApfFilter(mContext, config, ipClientCallback,
+                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
         apfFilter.setLinkProperties(lp);
         byte[] program = ipClientCallback.assertProgramUpdateAndGet();
         byte[] data = new byte[Counter.totalSize()];
@@ -496,23 +497,18 @@
 
         ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
-                mNetworkQuirkMetrics, mDependencies);
+        TestLegacyApfFilter apfFilter = new TestLegacyApfFilter(mContext, config, ipClientCallback,
+                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
         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 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);
+        put(packet, ETH_DEST_ADDR_OFFSET, TestLegacyApfFilter.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());
@@ -540,11 +536,11 @@
         assertDrop(program, packet.array());
 
         // Verify broadcast IPv4 DHCP to us is passed
-        put(packet, DHCP_CLIENT_MAC_OFFSET, TestApfFilter.MOCK_MAC_ADDR);
+        put(packet, DHCP_CLIENT_MAC_OFFSET, TestLegacyApfFilter.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);
+        put(packet, ETH_DEST_ADDR_OFFSET, TestLegacyApfFilter.MOCK_MAC_ADDR);
         assertPass(program, packet.array());
     }
 
@@ -552,8 +548,8 @@
     public void testApfFilterIPv6() throws Exception {
         MockIpClientCallback ipClientCallback = new MockIpClientCallback();
         ApfConfiguration config = getDefaultConfig();
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
-                mNetworkQuirkMetrics, mDependencies);
+        TestLegacyApfFilter apfFilter = new TestLegacyApfFilter(mContext, config, ipClientCallback,
+                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
         byte[] program = ipClientCallback.assertProgramUpdateAndGet();
 
         // Verify empty IPv6 packet is passed
@@ -601,8 +597,8 @@
 
         ApfConfiguration config = getDefaultConfig();
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
-                mNetworkQuirkMetrics, mDependencies);
+        TestLegacyApfFilter apfFilter = new TestLegacyApfFilter(mContext, config, ipClientCallback,
+                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
         apfFilter.setLinkProperties(lp);
 
         byte[] program = ipClientCallback.assertProgramUpdateAndGet();
@@ -627,7 +623,7 @@
 
         // Construct IPv4 broadcast with L2 unicast address packet (b/30231088).
         ByteBuffer bcastv4unicastl2packet = makeIpv4Packet(IPPROTO_UDP);
-        bcastv4unicastl2packet.put(TestApfFilter.MOCK_MAC_ADDR);
+        bcastv4unicastl2packet.put(TestLegacyApfFilter.MOCK_MAC_ADDR);
         bcastv4unicastl2packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP);
         put(bcastv4unicastl2packet, IPV4_DEST_ADDR_OFFSET, broadcastIpv4Addr);
 
@@ -662,8 +658,8 @@
         ipClientCallback.resetApfProgramWait();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mNetworkQuirkMetrics,
-                mDependencies);
+        apfFilter =  new TestLegacyApfFilter(mContext, config, ipClientCallback,
+                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
         apfFilter.setLinkProperties(lp);
         program = ipClientCallback.assertProgramUpdateAndGet();
         assertDrop(program, mcastv4packet.array());
@@ -678,60 +674,22 @@
 
     @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();
+        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();
+        final LegacyApfFilter apfFilter = new TestLegacyApfFilter(mContext, configuration,
+                ipClientCallback, mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
 
         // 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);
+        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...
+        apfFilter.setDozeMode(true);
         assertPass(ipClientCallback.assertProgramUpdateAndGet(), packet.array());
 
         // ...but when the multicast filter is also enabled, drop the multicast pings to save power.
@@ -747,14 +705,10 @@
 
         // 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));
-        }
+        apfFilter.setDozeMode(false);
         assertPass(ipClientCallback.assertProgramUpdateAndGet(), packet.array());
+
+        apfFilter.shutdown();
     }
 
     @Test
@@ -762,8 +716,8 @@
     public void testApfFilter802_3() throws Exception {
         MockIpClientCallback ipClientCallback = new MockIpClientCallback();
         ApfConfiguration config = getDefaultConfig();
-        ApfFilter apfFilter = TestApfFilter.createTestApfFilter(mContext, ipClientCallback, config,
-                mNetworkQuirkMetrics, mDependencies);
+        TestLegacyApfFilter apfFilter =  new TestLegacyApfFilter(mContext, config, ipClientCallback,
+                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
         byte[] program = ipClientCallback.assertProgramUpdateAndGet();
 
         // Verify empty packet of 100 zero bytes is passed
@@ -782,8 +736,8 @@
         // Now turn on the filter
         ipClientCallback.resetApfProgramWait();
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        apfFilter = TestApfFilter.createTestApfFilter(mContext, ipClientCallback, config,
-                mNetworkQuirkMetrics, mDependencies);
+        apfFilter =  new TestLegacyApfFilter(mContext, config, ipClientCallback,
+                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
         program = ipClientCallback.assertProgramUpdateAndGet();
 
         // Verify that IEEE802.3 frame is dropped
@@ -809,8 +763,8 @@
 
         MockIpClientCallback ipClientCallback = new MockIpClientCallback();
         ApfConfiguration config = getDefaultConfig();
-        ApfFilter apfFilter = TestApfFilter.createTestApfFilter(mContext, ipClientCallback, config,
-                mNetworkQuirkMetrics, mDependencies);
+        TestLegacyApfFilter apfFilter =  new TestLegacyApfFilter(mContext, config, ipClientCallback,
+                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
         byte[] program = ipClientCallback.assertProgramUpdateAndGet();
 
         // Verify empty packet of 100 zero bytes is passed
@@ -829,8 +783,8 @@
         // Now add IPv4 to the black list
         ipClientCallback.resetApfProgramWait();
         config.ethTypeBlackList = ipv4BlackList;
-        apfFilter = TestApfFilter.createTestApfFilter(mContext, ipClientCallback, config,
-                mNetworkQuirkMetrics, mDependencies);
+        apfFilter =  new TestLegacyApfFilter(mContext, config, ipClientCallback,
+                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
         program = ipClientCallback.assertProgramUpdateAndGet();
 
         // Verify that IPv4 frame will be dropped
@@ -844,8 +798,8 @@
         // 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);
+        apfFilter =  new TestLegacyApfFilter(mContext, config, ipClientCallback,
+                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
         program = ipClientCallback.assertProgramUpdateAndGet();
 
         // Verify that IPv4 frame will be dropped
@@ -857,7 +811,8 @@
         assertDrop(program, packet.array());
     }
 
-    private byte[] getProgram(MockIpClientCallback cb, ApfFilter filter, LinkProperties lp) {
+    private byte[] getProgram(MockIpClientCallback cb, TestLegacyApfFilter filter,
+            LinkProperties lp) {
         cb.resetApfProgramWait();
         filter.setLinkProperties(lp);
         return cb.assertProgramUpdateAndGet();
@@ -867,7 +822,7 @@
         // 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));
+        assertVerdict(DROP, 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));
@@ -890,8 +845,8 @@
         ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
-                mNetworkQuirkMetrics, mDependencies);
+        TestLegacyApfFilter apfFilter =  new TestLegacyApfFilter(mContext, config, ipClientCallback,
+                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
 
         // Verify initially ARP request filter is off, and GARP filter is on.
         verifyArpFilter(ipClientCallback.assertProgramUpdateAndGet(), PASS);
@@ -949,8 +904,8 @@
         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);
+        TestLegacyApfFilter apfFilter =  new TestLegacyApfFilter(mContext, config, cb,
+                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
         byte[] program;
         final int srcPort = 12345;
         final int dstPort = 54321;
@@ -1141,8 +1096,8 @@
         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);
+        TestLegacyApfFilter apfFilter =  new TestLegacyApfFilter(mContext, config, cb,
+                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
         byte[] program;
         final int srcPort = 1024;
         final int dstPort = 4500;
@@ -1424,16 +1379,18 @@
 
     // 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 {
+    private void verifyRaLifetime(TestLegacyApfFilter 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 {
+    private void assertInvalidRa(TestLegacyApfFilter apfFilter,
+            MockIpClientCallback ipClientCallback, ByteBuffer packet)
+            throws IOException, ErrnoException {
         apfFilter.pretendPacketReceived(packet.array());
         ipClientCallback.assertNoProgramUpdate();
     }
@@ -1444,8 +1401,8 @@
         ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, ipClientCallback,
-                mNetworkQuirkMetrics, mDependencies);
+        TestLegacyApfFilter apfFilter = new TestLegacyApfFilter(mContext, config, ipClientCallback,
+                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
         byte[] program = ipClientCallback.assertProgramUpdateAndGet();
 
         final int ROUTER_LIFETIME = 1000;
@@ -1534,8 +1491,8 @@
         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);
+        final TestLegacyApfFilter apfFilter = new TestLegacyApfFilter(mContext, config,
+                ipClientCallback, mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
         byte[] program = ipClientCallback.assertProgramUpdateAndGet();
         final int RA_REACHABLE_TIME = 1800;
         final int RA_RETRANSMISSION_TIMER = 1234;
@@ -1568,47 +1525,6 @@
         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.
@@ -1644,14 +1560,14 @@
         ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb, mNetworkQuirkMetrics,
-                mDependencies);
+        final TestLegacyApfFilter apfFilter = new TestLegacyApfFilter(mContext, config,
+                cb, mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
         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 (LegacyApfFilter.InvalidRaException e) {
             } catch (Exception e) {
                 throw new Exception("bad packet: " + HexDump.toHexString(packet), e);
             }
@@ -1666,8 +1582,8 @@
         ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestApfFilter apfFilter = new TestApfFilter(mContext, config, cb, mNetworkQuirkMetrics,
-                mDependencies);
+        final TestLegacyApfFilter apfFilter = new TestLegacyApfFilter(mContext, config,
+                cb, mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
         for (int i = 0; i < 1000; i++) {
             byte[] packet = new byte[r.nextInt(maxRandomPacketSize + 1)];
             r.nextBytes(packet);
@@ -1680,35 +1596,12 @@
     }
 
     @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;
+        TestLegacyApfFilter 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)
@@ -1754,10 +1647,10 @@
         //     Reserved
         //     Advertisement Interval: 600000
         final String packetStringFmt = "33330000000128C68E23672C86DD60054C6B00603AFFFE800000000000002AC68EFFFE23672CFF02000000000000000000000000000186000ACD40C01B580000000000000000010128C68E23672C05010000000005DC030440C0%s000000002401FA000480F00000000000000000001903000000001B582401FA000480F000000000000000000107010000000927C0";
-        final List<String> lifetimes = List.of("FFFFFFFF", "00000000", "00000001", "00001B58");
+        final List<String> lifetimes = List.of("FFFFFFFF", "00000001", "00001B58");
         for (String lifetime : lifetimes) {
-            apfFilter = new TestApfFilter(mContext, config, ipClientCallback, mNetworkQuirkMetrics,
-                    mDependencies);
+            apfFilter = new TestLegacyApfFilter(mContext, config, ipClientCallback,
+                    mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
             final byte[] ra = hexStringToByteArray(
                     String.format(packetStringFmt, lifetime + lifetime));
             // feed the RA into APF and generate the filter, the filter shouldn't crash.
@@ -1792,8 +1685,8 @@
     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;
+        config.apfVersionSupported = 2;
+        config.apfRamSize = 512;
         final TestAndroidPacketFilter apfFilter = makeTestApfFilter(config, ipClientCallback);
         byte[] program = ipClientCallback.assertProgramUpdateAndGet();
         final byte[] ra = buildLargeRa();
@@ -1823,8 +1716,8 @@
     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;
+        config.apfVersionSupported = 4;
+        config.apfRamSize = 4096;
         final long startTimeMs = 12345;
         final long durationTimeMs = config.minMetricsSessionDurationMs;
         doReturn(startTimeMs).when(mClock).elapsedRealtime();
@@ -1996,11 +1889,157 @@
         // durationTimeMs.
         ApfFilter.Clock clock = mock(ApfFilter.Clock.class);
         doReturn(startTimeMs).when(clock).elapsedRealtime();
-        final TestAndroidPacketFilter apfFilter2 = new TestApfFilter(mContext, config,
-                ipClientCallback, mNetworkQuirkMetrics, mDependencies, clock);
+        final TestAndroidPacketFilter apfFilter2 = new TestLegacyApfFilter(mContext, config,
+                ipClientCallback, mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, clock);
         doReturn(startTimeMs + durationTimeMs).when(clock).elapsedRealtime();
         apfFilter2.shutdown();
         verify(mApfSessionInfoMetrics).statsWrite();
         verify(mIpClientRaInfoMetrics).statsWrite();
     }
+
+    /**
+     * The Mock ip client callback class.
+     */
+    private static class MockIpClientCallback extends IpClient.IpClientCallbacksWrapper {
+        private final ConditionVariable mGotApfProgram = new ConditionVariable();
+        private byte[] mLastApfProgram;
+        private boolean mInstallPacketFilterReturn = true;
+
+        MockIpClientCallback() {
+            super(mock(IIpClientCallbacks.class), mock(SharedLog.class), mock(SharedLog.class),
+                    NetworkInformationShimImpl.newInstance(), false);
+        }
+
+        MockIpClientCallback(boolean installPacketFilterReturn) {
+            super(mock(IIpClientCallbacks.class), mock(SharedLog.class), mock(SharedLog.class),
+                    NetworkInformationShimImpl.newInstance(), false);
+            mInstallPacketFilterReturn = installPacketFilterReturn;
+        }
+
+        @Override
+        public boolean installPacketFilter(byte[] filter) {
+            mLastApfProgram = filter;
+            mGotApfProgram.open();
+            return mInstallPacketFilterReturn;
+        }
+
+        /**
+         * Reset the apf program and wait for the next update.
+         */
+        public void resetApfProgramWait() {
+            mGotApfProgram.close();
+        }
+
+        /**
+         * Assert the program is update within TIMEOUT_MS and return the program.
+         */
+        public byte[] assertProgramUpdateAndGet() {
+            assertTrue(mGotApfProgram.block(TIMEOUT_MS));
+            return mLastApfProgram;
+        }
+
+        /**
+         * Assert the program is not update within TIMEOUT_MS.
+         */
+        public void assertNoProgramUpdate() {
+            assertFalse(mGotApfProgram.block(TIMEOUT_MS));
+        }
+    }
+
+    /**
+     * The test legacy apf filter class.
+     */
+    private static class TestLegacyApfFilter extends LegacyApfFilter
+            implements TestAndroidPacketFilter {
+        public static final byte[] MOCK_MAC_ADDR = {1, 2, 3, 4, 5, 6};
+        private static final byte[] MOCK_IPV4_ADDR = {10, 0, 0, 1};
+
+        private FileDescriptor mWriteSocket;
+        private final MockIpClientCallback mMockIpClientCb;
+        private final boolean mThrowsExceptionWhenGeneratesProgram;
+
+        TestLegacyApfFilter(Context context, ApfFilter.ApfConfiguration config,
+                MockIpClientCallback ipClientCallback, IpConnectivityLog ipConnectivityLog,
+                NetworkQuirkMetrics networkQuirkMetrics) throws Exception {
+            this(context, config, ipClientCallback, ipConnectivityLog, networkQuirkMetrics,
+                    new ApfFilter.Dependencies(context),
+                    false /* throwsExceptionWhenGeneratesProgram */, new ApfFilter.Clock());
+        }
+
+        TestLegacyApfFilter(Context context, ApfFilter.ApfConfiguration config,
+                MockIpClientCallback ipClientCallback, IpConnectivityLog ipConnectivityLog,
+                NetworkQuirkMetrics networkQuirkMetrics, ApfFilter.Dependencies dependencies,
+                boolean throwsExceptionWhenGeneratesProgram) throws Exception {
+            this(context, config, ipClientCallback, ipConnectivityLog, networkQuirkMetrics,
+                    dependencies, throwsExceptionWhenGeneratesProgram, new ApfFilter.Clock());
+        }
+
+        TestLegacyApfFilter(Context context, ApfFilter.ApfConfiguration config,
+                MockIpClientCallback ipClientCallback, IpConnectivityLog ipConnectivityLog,
+                NetworkQuirkMetrics networkQuirkMetrics, ApfFilter.Dependencies dependencies,
+                ApfFilter.Clock clock) throws Exception {
+            this(context, config, ipClientCallback, ipConnectivityLog, networkQuirkMetrics,
+                    dependencies, false /* throwsExceptionWhenGeneratesProgram */, clock);
+        }
+
+        TestLegacyApfFilter(Context context, ApfFilter.ApfConfiguration config,
+                MockIpClientCallback ipClientCallback, IpConnectivityLog ipConnectivityLog,
+                NetworkQuirkMetrics networkQuirkMetrics, ApfFilter.Dependencies dependencies,
+                boolean throwsExceptionWhenGeneratesProgram, ApfFilter.Clock clock)
+                throws Exception {
+            super(context, config, InterfaceParams.getByName("lo"), ipClientCallback,
+                    ipConnectivityLog, networkQuirkMetrics, dependencies, clock);
+            mMockIpClientCb = ipClientCallback;
+            mThrowsExceptionWhenGeneratesProgram = throwsExceptionWhenGeneratesProgram;
+        }
+
+        /**
+         * Pretend an RA packet has been received and show it to LegacyApfFilter.
+         */
+        public void pretendPacketReceived(byte[] packet) throws IOException, ErrnoException {
+            mMockIpClientCb.resetApfProgramWait();
+            // ApfFilter's ReceiveThread will be waiting to read this.
+            Os.write(mWriteSocket, packet, 0, packet.length);
+        }
+
+        @Override
+        public synchronized void maybeStartFilter() {
+            mHardwareAddress = MOCK_MAC_ADDR;
+            installNewProgramLocked();
+
+            // Create two sockets, "readSocket" and "mWriteSocket" and connect them together.
+            FileDescriptor readSocket = new FileDescriptor();
+            mWriteSocket = new FileDescriptor();
+            try {
+                Os.socketpair(AF_UNIX, SOCK_STREAM, 0, mWriteSocket, readSocket);
+            } catch (ErrnoException e) {
+                fail();
+                return;
+            }
+            // Now pass readSocket to ReceiveThread as if it was setup to read raw RAs.
+            // This allows us to pretend RA packets have been received via pretendPacketReceived().
+            mReceiveThread = new ReceiveThread(readSocket);
+            mReceiveThread.start();
+        }
+
+        @Override
+        public synchronized void shutdown() {
+            super.shutdown();
+            if (mReceiveThread != null) {
+                mReceiveThread.halt();
+                mReceiveThread = null;
+            }
+            IoUtils.closeQuietly(mWriteSocket);
+        }
+
+        @Override
+        @GuardedBy("this")
+        protected ApfV4Generator emitPrologueLocked() throws
+                BaseApfGenerator.IllegalInstructionException {
+            if (mThrowsExceptionWhenGeneratesProgram) {
+                throw new IllegalStateException();
+            }
+            return super.emitPrologueLocked();
+        }
+    }
 }
diff --git a/tests/unit/src/android/net/ip/IpClientTest.java b/tests/unit/src/android/net/ip/IpClientTest.java
index 00982c7..71f7ebf 100644
--- a/tests/unit/src/android/net/ip/IpClientTest.java
+++ b/tests/unit/src/android/net/ip/IpClientTest.java
@@ -16,11 +16,17 @@
 
 package android.net.ip;
 
+import static android.net.apf.BaseApfGenerator.APF_VERSION_6;
 import static android.net.ip.IpClientLinkObserver.CONFIG_SOCKET_RECV_BUFSIZE;
 import static android.net.ip.IpClientLinkObserver.SOCKET_RECV_BUFSIZE;
+import static android.system.OsConstants.AF_UNSPEC;
+import static android.system.OsConstants.ARPHRD_ETHER;
+import static android.system.OsConstants.IFA_F_PERMANENT;
+import static android.system.OsConstants.IFA_F_TENTATIVE;
 import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
 
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
+import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWLINK;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTPROT_KERNEL;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTM_DELROUTE;
 import static com.android.net.module.util.netlink.NetlinkConstants.RTM_NEWADDR;
@@ -44,9 +50,11 @@
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
@@ -67,6 +75,7 @@
 import android.net.MacAddress;
 import android.net.NetworkStackIpMemoryStore;
 import android.net.RouteInfo;
+import android.net.apf.AndroidPacketFilter;
 import android.net.apf.ApfCapabilities;
 import android.net.apf.ApfFilter.ApfConfiguration;
 import android.net.ip.IpClientLinkObserver.IpClientNetlinkMonitor;
@@ -87,8 +96,10 @@
 import com.android.net.module.util.InterfaceParams;
 import com.android.net.module.util.netlink.NduseroptMessage;
 import com.android.net.module.util.netlink.RtNetlinkAddressMessage;
+import com.android.net.module.util.netlink.RtNetlinkLinkMessage;
 import com.android.net.module.util.netlink.RtNetlinkRouteMessage;
 import com.android.net.module.util.netlink.StructIfaddrMsg;
+import com.android.net.module.util.netlink.StructIfinfoMsg;
 import com.android.net.module.util.netlink.StructNdOptRdnss;
 import com.android.net.module.util.netlink.StructNlMsgHdr;
 import com.android.net.module.util.netlink.StructRtMsg;
@@ -105,6 +116,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -135,6 +147,8 @@
     private static final String INVALID = "INVALID";
     private static final String TEST_IFNAME = "test_wlan0";
     private static final int TEST_IFINDEX = 1001;
+    private static final String TEST_CLAT_IFNAME = "v4-" + TEST_IFNAME;
+    private static final int TEST_CLAT_IFINDEX = 1002;
     // See RFC 7042#section-2.1.2 for EUI-48 documentation values.
     private static final MacAddress TEST_MAC = MacAddress.fromString("00:00:5E:00:53:01");
     private static final int TEST_TIMEOUT_MS = 30_000;
@@ -176,6 +190,7 @@
     @Mock private FileDescriptor mFd;
     @Mock private PrintWriter mWriter;
     @Mock private IpClientNetlinkMonitor mNetlinkMonitor;
+    @Mock private AndroidPacketFilter mApfFilter;
 
     private InterfaceParams mIfParams;
     private INetlinkMessageProcessor mNetlinkMessageProcessor;
@@ -296,6 +311,21 @@
                 (byte) 0 /* icmp_code */, option, null /* srcaddr */);
     }
 
+    private static RtNetlinkLinkMessage buildRtmLinkMessage(short type, int ifindex,
+            String ifaceName) {
+        final StructNlMsgHdr nlmsghdr =
+                makeNetlinkMessageHeader(type, (short) (NLM_F_REQUEST | NLM_F_ACK));
+        final StructIfinfoMsg ifInfoMsg =
+                new StructIfinfoMsg(
+                        (short) AF_UNSPEC,
+                        ARPHRD_ETHER,
+                        ifindex,
+                        0 /* flags */,
+                        0xffffffffL /* change */);
+
+        return new RtNetlinkLinkMessage(nlmsghdr, 0 /* mtu */,  ifInfoMsg, TEST_MAC, ifaceName);
+    }
+
     private void onInterfaceAddressUpdated(final LinkAddress la, int flags) {
         final RtNetlinkAddressMessage msg =
                 buildRtmAddressMessage(RTM_NEWADDR, la, TEST_IFINDEX, flags);
@@ -317,6 +347,12 @@
         mNetlinkMessageProcessor.processNetlinkMessage(msg, TEST_UNUSED_REAL_TIME /* whenMs */);
     }
 
+    private void onInterfaceAdded(int ifaceIndex, String ifaceName) {
+        final RtNetlinkLinkMessage msg = buildRtmLinkMessage(RTM_NEWLINK, ifaceIndex, ifaceName);
+        mNetlinkMessageProcessor.processNetlinkMessage(msg, TEST_UNUSED_REAL_TIME /* whenMs */);
+    }
+
+
     @Test
     public void testNullInterfaceNameMostDefinitelyThrows() throws Exception {
         setTestInterfaceParams(null);
@@ -806,16 +842,21 @@
                         conf(links(TEST_LOCAL_ADDRESSES), prefixes(TEST_PREFIXES), ips()));
         if (isApfSupported) {
             config.withApfCapabilities(new ApfCapabilities(4 /* version */,
-                    4096 /* maxProgramSize */, 4 /* format */));
+                    4096 /* maxProgramSize */, ARPHRD_ETHER));
         }
 
         ipc.startProvisioning(config.build());
         final ArgumentCaptor<ApfConfiguration> configCaptor = ArgumentCaptor.forClass(
                 ApfConfiguration.class);
-        verify(mDependencies, timeout(TEST_TIMEOUT_MS)).maybeCreateApfFilter(
-                any(), configCaptor.capture(), any(), any(), any(), anyBoolean());
+        if (isApfSupported) {
+            verify(mDependencies, timeout(TEST_TIMEOUT_MS)).maybeCreateApfFilter(
+                    any(), configCaptor.capture(), any(), any(), any(), anyBoolean());
+        } else {
+            verify(mDependencies, never()).maybeCreateApfFilter(
+                    any(), configCaptor.capture(), any(), any(), any(), anyBoolean());
+        }
 
-        return configCaptor.getValue();
+        return isApfSupported ? configCaptor.getValue() : null;
     }
 
     @Test @IgnoreAfter(Build.VERSION_CODES.R)
@@ -872,11 +913,10 @@
         final IpClient ipc = makeIpClient(TEST_IFNAME);
         final ApfConfiguration config = verifyApfFilterCreatedOnStart(ipc,
                 false /* isApfSupported */);
-        assertNull(config.apfCapabilities);
-        clearInvocations(mDependencies);
+        assertNull(config);
 
         ipc.updateApfCapabilities(new ApfCapabilities(4 /* version */, 4096 /* maxProgramSize */,
-                4 /* format */));
+                ARPHRD_ETHER));
         HandlerUtils.waitForIdle(ipc.getHandler(), TEST_TIMEOUT_MS);
 
         final ArgumentCaptor<ApfConfiguration> configCaptor = ArgumentCaptor.forClass(
@@ -885,10 +925,8 @@
                 any(), configCaptor.capture(), any(), any(), any(), anyBoolean());
         final ApfConfiguration actual = configCaptor.getValue();
         assertNotNull(actual);
-        int expectedApfVersion = SdkLevel.isAtLeastS() ? 4 : 3;
-        assertEquals(expectedApfVersion, actual.apfCapabilities.apfVersionSupported);
-        assertEquals(4096, actual.apfCapabilities.maximumApfProgramSize);
-        assertEquals(4, actual.apfCapabilities.apfPacketFormat);
+        assertEquals(SdkLevel.isAtLeastS() ? 4 : 3, actual.apfVersionSupported);
+        assertEquals(4096, actual.apfRamSize);
 
         verifyShutdown(ipc);
     }
@@ -897,8 +935,9 @@
     public void testDumpApfFilter_withNoException() throws Exception {
         final IpClient ipc = makeIpClient(TEST_IFNAME);
         final ApfConfiguration config = verifyApfFilterCreatedOnStart(ipc,
-                false /* isApfSupported */);
-        assertNull(config.apfCapabilities);
+                true /* isApfSupported */);
+        assertEquals(SdkLevel.isAtLeastS() ? 4 : 3, config.apfVersionSupported);
+        assertEquals(4096, config.apfRamSize);
         clearInvocations(mDependencies);
         ipc.dump(mFd, mWriter, null /* args */);
         verifyShutdown(ipc);
@@ -909,11 +948,12 @@
         final IpClient ipc = makeIpClient(TEST_IFNAME);
         final ApfConfiguration config = verifyApfFilterCreatedOnStart(ipc,
                 true /* isApfSupported */);
-        assertNotNull(config.apfCapabilities);
+        assertEquals(SdkLevel.isAtLeastS() ? 4 : 3, config.apfVersionSupported);
+        assertEquals(4096, config.apfRamSize);
         clearInvocations(mDependencies);
 
         final ApfCapabilities newApfCapabilities = new ApfCapabilities(4 /* version */,
-                8192 /* maxProgramSize */, 4 /* format */);
+                8192 /* maxProgramSize */, ARPHRD_ETHER);
         ipc.updateApfCapabilities(newApfCapabilities);
         HandlerUtils.waitForIdle(ipc.getHandler(), TEST_TIMEOUT_MS);
         verify(mDependencies, never()).maybeCreateApfFilter(any(), any(), any(), any(), any(),
@@ -926,7 +966,8 @@
         final IpClient ipc = makeIpClient(TEST_IFNAME);
         final ApfConfiguration config = verifyApfFilterCreatedOnStart(ipc,
                 true /* isApfSupported */);
-        assertNotNull(config.apfCapabilities);
+        assertEquals(SdkLevel.isAtLeastS() ? 4 : 3, config.apfVersionSupported);
+        assertEquals(4096, config.apfRamSize);
         clearInvocations(mDependencies);
 
         ipc.updateApfCapabilities(null /* apfCapabilities */);
@@ -936,6 +977,105 @@
         verifyShutdown(ipc);
     }
 
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void testVendorNdOffloadDisabledWhenApfV6Supported() throws Exception {
+        when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(), anyBoolean()))
+                .thenReturn(mApfFilter);
+        when(mApfFilter.supportNdOffload()).thenReturn(true);
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIPv4()
+                .withoutIpReachabilityMonitor()
+                .withApfCapabilities(new ApfCapabilities(APF_VERSION_6,
+                        4096 /* maxProgramSize */, ARPHRD_ETHER))
+                .build();
+        ipc.startProvisioning(config);
+        final InOrder inOrder = inOrder(mCb);
+        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS).times(1)).setNeighborDiscoveryOffload(true);
+        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS).times(1)).setNeighborDiscoveryOffload(false);
+
+        // update clat
+        onInterfaceAdded(TEST_CLAT_IFINDEX, TEST_CLAT_IFNAME);
+        verifyShutdown(ipc);
+        inOrder.verify(mCb, never()).setNeighborDiscoveryOffload(anyBoolean());
+        clearInvocations(mApfFilter);
+        clearInvocations(mCb);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void testVendorNdOffloadEnabledWhenApfV6NotSupported() throws Exception {
+        when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(), anyBoolean()))
+                .thenReturn(mApfFilter);
+        when(mApfFilter.supportNdOffload()).thenReturn(false);
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIPv4()
+                .withoutIpReachabilityMonitor()
+                .withApfCapabilities(new ApfCapabilities(APF_VERSION_6,
+                        4096 /* maxProgramSize */, ARPHRD_ETHER))
+                .build();
+        ipc.startProvisioning(config);
+        verify(mCb, timeout(TEST_TIMEOUT_MS).times(1)).setNeighborDiscoveryOffload(true);
+
+        // update clat
+        onInterfaceAdded(TEST_CLAT_IFINDEX, TEST_CLAT_IFNAME);
+        verifyShutdown(ipc);
+        verify(mCb, times(1)).setNeighborDiscoveryOffload(true);
+        clearInvocations(mApfFilter);
+        clearInvocations(mCb);
+    }
+
+    @Test
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    public void testVendorNdOffloadDisabledWhenApfCapabilitiesUpdated() throws Exception {
+        when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(), anyBoolean()))
+                .thenReturn(mApfFilter);
+        when(mApfFilter.supportNdOffload()).thenReturn(true);
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIPv4()
+                .withoutIpReachabilityMonitor()
+                .build();
+        ipc.startProvisioning(config);
+        ipc.updateApfCapabilities(
+                new ApfCapabilities(APF_VERSION_6, 4096 /* maxProgramSize */, ARPHRD_ETHER));
+        HandlerUtils.waitForIdle(ipc.getHandler(), TEST_TIMEOUT_MS);
+        final InOrder inOrder = inOrder(mCb);
+        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS).times(1)).setNeighborDiscoveryOffload(true);
+        inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS).times(1)).setNeighborDiscoveryOffload(false);
+        verifyShutdown(ipc);
+        inOrder.verify(mCb, never()).setNeighborDiscoveryOffload(anyBoolean());
+        clearInvocations(mApfFilter);
+        clearInvocations(mCb);
+    }
+
+    @Test
+    public void testLinkPropertiesUpdate_callSetLinkPropertiesOnApfFilter() throws Exception {
+        when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(), anyBoolean()))
+                .thenReturn(mApfFilter);
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        verifyApfFilterCreatedOnStart(ipc, true /* isApfSupported */);
+        onInterfaceAddressUpdated(
+                new LinkAddress(TEST_GLOBAL_ADDRESS, IFA_F_TENTATIVE, RT_SCOPE_UNIVERSE),
+                IFA_F_TENTATIVE);
+        // mApfFilter.setLinkProperties() is called both in IpClient#handleLinkPropertiesUpdate()
+        // and IpClient#setLinkProperties().
+        verify(mApfFilter, timeout(TEST_TIMEOUT_MS).times(2)).setLinkProperties(any());
+        // LinkAddress flag change will trigger mApfFilter.setLinkProperties()
+        onInterfaceAddressUpdated(
+                new LinkAddress(TEST_GLOBAL_ADDRESS, IFA_F_PERMANENT, RT_SCOPE_UNIVERSE),
+                IFA_F_PERMANENT);
+        // mApfFilter.setLinkProperties() is called only in IpClient#handleLinkPropertiesUpdate().
+        // IpClient#setLinkProperties() is not called because Objects.equals(newLp,
+        // mLinkProperties) returns true and IpClient#handleLinkPropertiesUpdate() is terminated.
+        verify(mApfFilter, timeout(TEST_TIMEOUT_MS).times(3)).setLinkProperties(any());
+        clearInvocations(mDependencies);
+        clearInvocations(mApfFilter);
+        verifyShutdown(ipc);
+    }
+
     private ScanResultInfo makeScanResultInfo(final String ssid, final String bssid) {
         final ByteBuffer payload = ByteBuffer.allocate(14 /* oui + type + data */);
         final byte[] data = new byte[10];
diff --git a/tests/unit/src/android/net/util/RawSocketUtilsTest.kt b/tests/unit/src/android/net/util/RawSocketUtilsTest.kt
new file mode 100644
index 0000000..45bee54
--- /dev/null
+++ b/tests/unit/src/android/net/util/RawSocketUtilsTest.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.util
+
+import android.content.Context
+import android.net.TetheringManager
+import android.system.Os
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.net.module.util.HexDump
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRunner
+import java.io.FileDescriptor
+import java.net.NetworkInterface
+import kotlin.test.assertEquals
+import kotlin.test.assertFailsWith
+import org.junit.After
+import org.junit.Before
+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.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.framework
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoSession
+import org.mockito.quality.Strictness
+
+@RunWith(DevSdkIgnoreRunner::class)
+class RawSocketUtilsTest {
+    @get:Rule
+    val ignoreRule = DevSdkIgnoreRule()
+    companion object {
+        private const val TEST_IFINDEX = 123
+        private const val TEST_IFACENAME = "wlan0"
+        private const val TEST_SRC_MAC = "FFFFFFFFFFFF"
+        private const val TEST_DST_MAC = "1234567890AB"
+        private const val TEST_INVALID_PACKET_IN_HEX = "DEADBEEF"
+        private const val TEST_PACKET_TYPE_IN_HEX = "88A4"
+        private const val TEST_VALID_PACKET_IN_HEX =
+                TEST_DST_MAC + TEST_SRC_MAC + TEST_PACKET_TYPE_IN_HEX
+    }
+    @Mock
+    private lateinit var mockContext: Context
+    @Mock
+    private lateinit var mockTetheringManager: TetheringManager
+    @Mock
+    private lateinit var mockNetworkInterface: NetworkInterface
+
+    // For mocking static methods.
+    private lateinit var mockitoSession: MockitoSession
+
+    @Before
+    fun setup() {
+        mockitoSession = ExtendedMockito.mockitoSession()
+                .mockStatic(Os::class.java)
+                .mockStatic(NetworkInterface::class.java)
+                .mockStatic(SocketUtils::class.java)
+                .initMocks(this)
+                .strictness(Strictness.LENIENT)
+                .startMocking()
+        doReturn(mockTetheringManager).`when`(mockContext)
+                .getSystemService(eq(TetheringManager::class.java))
+        `when`(NetworkInterface.getByName(any())).thenReturn(mockNetworkInterface)
+        doReturn(TEST_IFINDEX).`when`(mockNetworkInterface).index
+    }
+
+    @After
+    fun teardown() {
+        mockitoSession.finishMocking()
+        // Clear mocks to prevent from stubs holding instances and cause memory leaks.
+        framework().clearInlineMocks()
+    }
+
+    @Test
+    fun sendRawPacketDownStream_invalidTetheredInterface() {
+        doAnswer {
+            val callback = it.arguments[1] as TetheringManager.TetheringEventCallback
+            callback.onTetheredInterfacesChanged(listOf("eth0"))
+        }.`when`(mockTetheringManager).registerTetheringEventCallback(any(), any())
+        assertFailsWith<SecurityException> {
+            RawSocketUtils.sendRawPacketDownStream(
+                mockContext,
+                TEST_IFACENAME,
+                TEST_INVALID_PACKET_IN_HEX
+            )
+        }
+    }
+
+    @Test
+    fun sendRawPacketDownStream_invalidPacket() {
+        doAnswer {
+            val callback = it.arguments[1] as TetheringManager.TetheringEventCallback
+            callback.onTetheredInterfacesChanged(listOf(TEST_IFACENAME))
+        }.`when`(mockTetheringManager).registerTetheringEventCallback(any(), any())
+
+        assertFailsWith<ArrayIndexOutOfBoundsException> {
+            RawSocketUtils.sendRawPacketDownStream(
+                    mockContext,
+                    TEST_IFACENAME,
+                    TEST_INVALID_PACKET_IN_HEX
+            )
+        }
+    }
+
+    @Test
+    fun sendRawPacketDownStream_validPacket() {
+        doAnswer {
+            val callback = it.arguments[1] as TetheringManager.TetheringEventCallback
+            callback.onTetheredInterfacesChanged(listOf(TEST_IFACENAME))
+        }.`when`(mockTetheringManager).registerTetheringEventCallback(any(), any())
+
+        RawSocketUtils.sendRawPacketDownStream(
+            mockContext,
+            TEST_IFACENAME,
+            TEST_VALID_PACKET_IN_HEX
+        )
+
+        // Verify interactions with mocked static methods.
+        val fileDescriptorCaptor = ArgumentCaptor.forClass(FileDescriptor::class.java)
+        val packetDataCaptor = ArgumentCaptor.forClass(ByteArray::class.java)
+        val packetDataLengthCaptor = ArgumentCaptor.forClass(Int::class.java)
+        ExtendedMockito.verify {
+            Os.sendto(
+                fileDescriptorCaptor.capture(),
+                packetDataCaptor.capture(),
+                eq(0),
+                packetDataLengthCaptor.capture(),
+                eq(0),
+                any()
+            )
+        }
+        assertEquals(TEST_VALID_PACKET_IN_HEX, HexDump.toHexString(packetDataCaptor.value))
+        assertEquals(TEST_VALID_PACKET_IN_HEX.length / 2, packetDataLengthCaptor.value)
+        // TODO: Verify ifindex and packetType once the members of PacketSocketAddress
+        //  can be accessed.
+        ExtendedMockito.verify { SocketUtils.closeSocket(eq(fileDescriptorCaptor.value)) }
+    }
+}
diff --git a/tests/unit/src/com/android/networkstack/metrics/ApfSessionInfoMetricsTest.java b/tests/unit/src/com/android/networkstack/metrics/ApfSessionInfoMetricsTest.java
index 8dc3d92..69464cf 100644
--- a/tests/unit/src/com/android/networkstack/metrics/ApfSessionInfoMetricsTest.java
+++ b/tests/unit/src/com/android/networkstack/metrics/ApfSessionInfoMetricsTest.java
@@ -130,7 +130,7 @@
                 CounterName.CN_DROPPED_IPV6_NON_ICMP_MULTICAST);
         verifyCounterName(Counter.DROPPED_802_3_FRAME, CounterName.CN_DROPPED_802_3_FRAME);
         verifyCounterName(Counter.DROPPED_ETHERTYPE_NOT_ALLOWED,
-                CounterName.CN_DROPPED_ETHERTYPE_DENYLISTED);
+                CounterName.CN_DROPPED_ETHERTYPE_NOT_ALLOWED);
         verifyCounterName(Counter.DROPPED_ARP_REPLY_SPA_NO_HOST,
                 CounterName.CN_DROPPED_ARP_REPLY_SPA_NO_HOST);
         verifyCounterName(Counter.DROPPED_IPV4_KEEPALIVE_ACK,
@@ -143,5 +143,29 @@
         verifyCounterName(Counter.DROPPED_IPV4_TCP_PORT7_UNICAST, CounterName.CN_UNKNOWN);
         verifyCounterName(Counter.DROPPED_ARP_NON_IPV4, CounterName.CN_DROPPED_ARP_NON_IPV4);
         verifyCounterName(Counter.DROPPED_ARP_UNKNOWN, CounterName.CN_DROPPED_ARP_UNKNOWN);
+        verifyCounterName(Counter.PASSED_ARP_BROADCAST_REPLY,
+                CounterName.CN_PASSED_ARP_BROADCAST_REPLY);
+        verifyCounterName(Counter.PASSED_ARP_REQUEST, CounterName.CN_PASSED_ARP_REQUEST);
+        verifyCounterName(Counter.PASSED_IPV4_FROM_DHCPV4_SERVER,
+                CounterName.CN_PASSED_IPV4_FROM_DHCPV4_SERVER);
+        verifyCounterName(Counter.PASSED_IPV6_NS_DAD, CounterName.CN_PASSED_IPV6_NS_DAD);
+        verifyCounterName(Counter.PASSED_IPV6_NS_NO_ADDRESS,
+                CounterName.CN_PASSED_IPV6_NS_NO_ADDRESS);
+        verifyCounterName(Counter.PASSED_IPV6_NS_NO_SLLA_OPTION,
+                CounterName.CN_PASSED_IPV6_NS_NO_SLLA_OPTION);
+        verifyCounterName(Counter.PASSED_IPV6_NS_TENTATIVE,
+                CounterName.CN_PASSED_IPV6_NS_TENTATIVE);
+        verifyCounterName(Counter.PASSED_MLD, CounterName.CN_PASSED_MLD);
+        verifyCounterName(Counter.DROPPED_IPV4_NON_DHCP4, CounterName.CN_DROPPED_IPV4_NON_DHCP4);
+        verifyCounterName(Counter.DROPPED_IPV6_NS_INVALID, CounterName.CN_DROPPED_IPV6_NS_INVALID);
+        verifyCounterName(Counter.DROPPED_IPV6_NS_OTHER_HOST,
+                CounterName.CN_DROPPED_IPV6_NS_OTHER_HOST);
+        verifyCounterName(Counter.DROPPED_IPV6_NS_REPLIED_NON_DAD,
+                CounterName.CN_DROPPED_IPV6_NS_REPLIED_NON_DAD);
+        verifyCounterName(Counter.DROPPED_ARP_REQUEST_ANYHOST,
+                CounterName.CN_DROPPED_ARP_REQUEST_ANYHOST);
+        verifyCounterName(Counter.DROPPED_ARP_REQUEST_REPLIED,
+                CounterName.CN_DROPPED_ARP_REQUEST_REPLIED);
+        verifyCounterName(Counter.DROPPED_ARP_V6_ONLY, CounterName.CN_DROPPED_ARP_V6_ONLY);
     }
 }
diff --git a/tests/unit/src/com/android/server/connectivity/DdrTrackerTest.java b/tests/unit/src/com/android/server/connectivity/DdrTrackerTest.java
new file mode 100644
index 0000000..dcc4291
--- /dev/null
+++ b/tests/unit/src/com/android/server/connectivity/DdrTrackerTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
+import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.annotation.NonNull;
+import android.net.shared.PrivateDnsConfig;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.MockitoAnnotations;
+
+import java.net.InetAddress;
+
+@RunWith(JUnit4.class)
+public final class DdrTrackerTest {
+    private static final int OFF_MODE = PRIVATE_DNS_MODE_OFF;
+    private static final int OPPORTUNISTIC_MODE = PRIVATE_DNS_MODE_OPPORTUNISTIC;
+    private static final int STRICT_MODE = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+
+    private DdrTracker mDdrTracker;
+
+    private static class PrivateDnsConfigBuilder {
+        private int mMode = OFF_MODE;
+        private String mHostname = null;
+        private final InetAddress[] mIps = null;
+        private final String mDohName = null;
+        private final InetAddress[] mDohIps = null;
+        private final String mDohPath = null;
+        private final int mDohPort = -1;
+
+        PrivateDnsConfigBuilder setMode(int mode) {
+            mMode = mode;
+            return this;
+        }
+        PrivateDnsConfigBuilder setHostname(String value) {
+            mHostname = value;
+            return this;
+        }
+        PrivateDnsConfig build() {
+            return new PrivateDnsConfig(mMode, mHostname, mIps, mDohName, mDohIps, mDohPath,
+                    mDohPort);
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mDdrTracker = new DdrTracker();
+    }
+
+    private void testNotifyPrivateDnsSettingsChangedHelper(int mode, @NonNull String dnsProvider)
+            throws Exception {
+        final PrivateDnsConfig cfg =
+                new PrivateDnsConfigBuilder().setMode(mode).setHostname(dnsProvider).build();
+
+        assertTrue(mDdrTracker.notifyPrivateDnsSettingsChanged(cfg));
+        assertEquals(mode, mDdrTracker.getPrivateDnsMode());
+        assertEquals(dnsProvider, mDdrTracker.getStrictModeHostname());
+        assertFalse(mDdrTracker.notifyPrivateDnsSettingsChanged(cfg));
+    }
+
+    @Test
+    public void testNotifyPrivateDnsSettingsChanged() throws Exception {
+        // Tests the initial private DNS setting in DdrTracker.
+        assertEquals(OFF_MODE, mDdrTracker.getPrivateDnsMode());
+
+        assertEquals("", mDdrTracker.getStrictModeHostname());
+        assertFalse(mDdrTracker.notifyPrivateDnsSettingsChanged(new PrivateDnsConfigBuilder()
+                .setMode(OFF_MODE).build()));
+
+        testNotifyPrivateDnsSettingsChangedHelper(OPPORTUNISTIC_MODE, "");
+        testNotifyPrivateDnsSettingsChangedHelper(STRICT_MODE, "example1.com");
+        testNotifyPrivateDnsSettingsChangedHelper(STRICT_MODE, "example2.com");
+        testNotifyPrivateDnsSettingsChangedHelper(OFF_MODE, "");
+    }
+}