Merge "Add serial urls probe logic for NetworkMonitor" into main
diff --git a/Android.bp b/Android.bp
index 734797a..5898d04 100644
--- a/Android.bp
+++ b/Android.bp
@@ -35,7 +35,7 @@
 java_defaults {
     name: "NetworkStackReleaseTargetSdk",
     min_sdk_version: "30",
-    target_sdk_version: "35",
+    target_sdk_version: "36",
 }
 
 java_defaults {
@@ -64,7 +64,7 @@
     ],
     apex_available: [
         "com.android.tethering",
-        "//apex_available:platform", // For InProcessNetworkStack
+        "//apex_available:platform",
     ],
     min_sdk_version: "30",
 }
@@ -266,8 +266,7 @@
     ],
 }
 
-// Common defaults for android libraries containing network stack code, used to compile variants of
-// the network stack in the system process and in the network_stack process
+// Common defaults for android libraries containing network stack code
 java_defaults {
     name: "NetworkStackAndroidLibraryDefaults",
     srcs: [
@@ -439,29 +438,6 @@
     },
 }
 
-// Non-updatable network stack running in the system server process for devices not using the module
-android_app {
-    name: "InProcessNetworkStack",
-    defaults: [
-        "NetworkStackAppDefaults",
-        "NetworkStackReleaseApiLevel",
-        "ConnectivityNextEnableDefaults",
-    ],
-    static_libs: ["NetworkStackApiCurrentLib"],
-    certificate: "platform",
-    manifest: "AndroidManifest_InProcess.xml",
-    // InProcessNetworkStack is a replacement for NetworkStack
-    overrides: [
-        "NetworkStack",
-        "NetworkStackNext",
-    ],
-    // The InProcessNetworkStack goes together with the PlatformCaptivePortalLogin, which replaces
-    // the default CaptivePortalLogin.
-    required: [
-        "PlatformCaptivePortalLogin",
-    ],
-}
-
 // Pre-merge the AndroidManifest for NetworkStackNext, so that its manifest can be merged on top
 android_library {
     name: "NetworkStackNextManifestBase",
diff --git a/AndroidManifest_InProcess.xml b/AndroidManifest_InProcess.xml
deleted file mode 100644
index 40d74a5..0000000
--- a/AndroidManifest_InProcess.xml
+++ /dev/null
@@ -1,41 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
- * Copyright (C) 2019 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.
- */
--->
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="com.android.networkstack.inprocess"
-          android:sharedUserId="android.uid.system"
-          android:process="system"
-          coreApp="true">
-    <!--- Defines the MAINLINE_NETWORK_STACK permission used by the networkstack process. -->
-    <permission android:name="android.permission.MAINLINE_NETWORK_STACK"
-                android:protectionLevel="signature"/>
-    <application>
-        <service android:name="com.android.server.NetworkStackService"
-                 android:process="system"
-                 android:exported="true"
-                 android:permission="android.permission.MAINLINE_NETWORK_STACK">
-            <intent-filter>
-                <action android:name="android.net.INetworkStackConnector.InProcess"/>
-            </intent-filter>
-        </service>
-        <service android:name="com.android.networkstack.ipmemorystore.RegularMaintenanceJobService"
-                 android:process="system"
-                 android:permission="android.permission.BIND_JOB_SERVICE" >
-        </service>
-    </application>
-</manifest>
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index ee06302..27538b6 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,12 +1,10 @@
 [Builtin Hooks]
 bpfmt = true
 clang_format = true
-ktfmt = true
 
 [Builtin Hooks Options]
 clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp,hpp
-ktfmt = --kotlinlang-style
 
 [Hook Scripts]
 checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
-ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py --no-verify-format -f ${PREUPLOAD_FILES}
+ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py --disabled-rules comment-wrapping -f ${PREUPLOAD_FILES}
diff --git a/common/networkstackclient/Android.bp b/common/networkstackclient/Android.bp
index 983f1b7..7131409 100644
--- a/common/networkstackclient/Android.bp
+++ b/common/networkstackclient/Android.bp
@@ -35,7 +35,7 @@
         java: {
             apex_available: [
                 "//apex_available:platform",
-                "com.android.btservices",
+                "com.android.bt",
                 "com.android.wifi",
                 "com.android.tethering",
             ],
@@ -152,7 +152,7 @@
         java: {
             apex_available: [
                 "//apex_available:platform",
-                "com.android.btservices",
+                "com.android.bt",
                 "com.android.wifi",
                 "com.android.tethering",
             ],
@@ -221,6 +221,10 @@
             version: "22",
             imports: ["ipmemorystore-aidl-interfaces-V11"],
         },
+        {
+            version: "23",
+            imports: ["ipmemorystore-aidl-interfaces-V11"],
+        },
 
     ],
     frozen: true,
@@ -232,12 +236,12 @@
     min_sdk_version: "30",
     static_libs: [
         "ipmemorystore-aidl-interfaces-V11-java",
-        "networkstack-aidl-interfaces-V22-java",
+        "networkstack-aidl-interfaces-V23-java",
     ],
     visibility: ["//packages/modules/NetworkStack:__subpackages__"],
     apex_available: [
         "//apex_available:platform",
-        "com.android.btservices",
+        "com.android.bt",
         "com.android.tethering",
         "com.android.wifi",
     ],
@@ -280,7 +284,7 @@
     ],
     apex_available: [
         "//apex_available:platform",
-        "com.android.btservices",
+        "com.android.bt",
         "com.android.tethering",
         "com.android.wifi",
     ],
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/.hash b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/.hash
new file mode 100644
index 0000000..59c0bfa
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/.hash
@@ -0,0 +1 @@
+9dd581b4741329188b6e58107600f38a3eaa9be1
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/DataStallReportParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/DataStallReportParcelable.aidl
new file mode 100644
index 0000000..771deda
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/DataStallReportParcelable.aidl
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2020, 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable DataStallReportParcelable {
+  long timestampMillis = 0;
+  int detectionMethod = 1;
+  int tcpPacketFailRate = 2;
+  int tcpMetricsCollectionPeriodMillis = 3;
+  int dnsConsecutiveTimeouts = 4;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/DhcpResultsParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/DhcpResultsParcelable.aidl
new file mode 100644
index 0000000..31f2194
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/DhcpResultsParcelable.aidl
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2019, 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable DhcpResultsParcelable {
+  android.net.StaticIpConfiguration baseConfiguration;
+  int leaseDuration;
+  int mtu;
+  String serverAddress;
+  String vendorInfo;
+  @nullable String serverHostName;
+  @nullable String captivePortalApiUrl;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/INetworkMonitor.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/INetworkMonitor.aidl
new file mode 100644
index 0000000..fb13c0c
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/INetworkMonitor.aidl
@@ -0,0 +1,60 @@
+/**
+ * Copyright (c) 2018, 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkMonitor {
+  oneway void start();
+  oneway void launchCaptivePortalApp();
+  oneway void notifyCaptivePortalAppFinished(int response);
+  oneway void setAcceptPartialConnectivity();
+  oneway void forceReevaluation(int uid);
+  oneway void notifyPrivateDnsChanged(in android.net.PrivateDnsConfigParcel config);
+  oneway void notifyDnsResponse(int returnCode);
+  oneway void notifyNetworkConnected(in android.net.LinkProperties lp, in android.net.NetworkCapabilities nc);
+  oneway void notifyNetworkDisconnected();
+  oneway void notifyLinkPropertiesChanged(in android.net.LinkProperties lp);
+  oneway void notifyNetworkCapabilitiesChanged(in android.net.NetworkCapabilities nc);
+  oneway void notifyNetworkConnectedParcel(in android.net.networkstack.aidl.NetworkMonitorParameters params);
+  const int NETWORK_TEST_RESULT_VALID = 0;
+  const int NETWORK_TEST_RESULT_INVALID = 1;
+  const int NETWORK_TEST_RESULT_PARTIAL_CONNECTIVITY = 2;
+  const int NETWORK_VALIDATION_RESULT_VALID = 0x01;
+  const int NETWORK_VALIDATION_RESULT_PARTIAL = 0x02;
+  const int NETWORK_VALIDATION_RESULT_SKIPPED = 0x04;
+  const int NETWORK_VALIDATION_PROBE_DNS = 0x04;
+  const int NETWORK_VALIDATION_PROBE_HTTP = 0x08;
+  const int NETWORK_VALIDATION_PROBE_HTTPS = 0x10;
+  const int NETWORK_VALIDATION_PROBE_FALLBACK = 0x20;
+  const int NETWORK_VALIDATION_PROBE_PRIVDNS = 0x40;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/INetworkMonitorCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/INetworkMonitorCallbacks.aidl
new file mode 100644
index 0000000..2a4fb1d
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/INetworkMonitorCallbacks.aidl
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkMonitorCallbacks {
+  void onNetworkMonitorCreated(in android.net.INetworkMonitor networkMonitor) = 0;
+  void notifyNetworkTested(int testResult, @nullable String redirectUrl) = 1;
+  void notifyPrivateDnsConfigResolved(in android.net.PrivateDnsConfigParcel config) = 2;
+  void showProvisioningNotification(String action, String packageName) = 3;
+  void hideProvisioningNotification() = 4;
+  void notifyProbeStatusChanged(int probesCompleted, int probesSucceeded) = 5;
+  void notifyNetworkTestedWithExtras(in android.net.NetworkTestResultParcelable result) = 6;
+  void notifyDataStallSuspected(in android.net.DataStallReportParcelable report) = 7;
+  void notifyCaptivePortalDataChanged(in android.net.CaptivePortalData data) = 8;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/INetworkStackConnector.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/INetworkStackConnector.aidl
new file mode 100644
index 0000000..8120ffc
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/INetworkStackConnector.aidl
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2018, 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkStackConnector {
+  oneway void makeDhcpServer(in String ifName, in android.net.dhcp.DhcpServingParamsParcel params, in android.net.dhcp.IDhcpServerCallbacks cb);
+  oneway void makeNetworkMonitor(in android.net.Network network, String name, in android.net.INetworkMonitorCallbacks cb);
+  oneway void makeIpClient(in String ifName, in android.net.ip.IIpClientCallbacks callbacks);
+  oneway void fetchIpMemoryStore(in android.net.IIpMemoryStoreCallbacks cb);
+  oneway void allowTestUid(int uid, in android.net.INetworkStackStatusCallback cb);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/INetworkStackStatusCallback.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/INetworkStackStatusCallback.aidl
new file mode 100644
index 0000000..0b6b778
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/INetworkStackStatusCallback.aidl
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+/* @hide */
+interface INetworkStackStatusCallback {
+  oneway void onStatusAvailable(int statusCode);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/InformationElementParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/InformationElementParcelable.aidl
new file mode 100644
index 0000000..6103774
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/InformationElementParcelable.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable InformationElementParcelable {
+  int id;
+  byte[] payload;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/InitialConfigurationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/InitialConfigurationParcelable.aidl
new file mode 100644
index 0000000..6a597e6
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/InitialConfigurationParcelable.aidl
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable InitialConfigurationParcelable {
+  android.net.LinkAddress[] ipAddresses;
+  android.net.IpPrefix[] directlyConnectedRoutes;
+  String[] dnsServers;
+  String gateway;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/Layer2InformationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/Layer2InformationParcelable.aidl
new file mode 100644
index 0000000..83796ee
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/Layer2InformationParcelable.aidl
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable Layer2InformationParcelable {
+  String l2Key;
+  String cluster;
+  android.net.MacAddress bssid;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/Layer2PacketParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/Layer2PacketParcelable.aidl
new file mode 100644
index 0000000..4b3fff5
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/Layer2PacketParcelable.aidl
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2019, 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable Layer2PacketParcelable {
+  android.net.MacAddress dstMacAddress;
+  byte[] payload;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/NattKeepalivePacketDataParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/NattKeepalivePacketDataParcelable.aidl
new file mode 100644
index 0000000..18cf954
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/NattKeepalivePacketDataParcelable.aidl
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable NattKeepalivePacketDataParcelable {
+  byte[] srcAddress;
+  int srcPort;
+  byte[] dstAddress;
+  int dstPort;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/NetworkTestResultParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/NetworkTestResultParcelable.aidl
new file mode 100644
index 0000000..4d6d5a2
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/NetworkTestResultParcelable.aidl
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2020, 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable NetworkTestResultParcelable {
+  long timestampMillis;
+  int result;
+  int probesSucceeded;
+  int probesAttempted;
+  String redirectUrl;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/PrivateDnsConfigParcel.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/PrivateDnsConfigParcel.aidl
new file mode 100644
index 0000000..b624ee4
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/PrivateDnsConfigParcel.aidl
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(equals=true, toString=true)
+parcelable PrivateDnsConfigParcel {
+  String hostname;
+  String[] ips;
+  int privateDnsMode = (-1) /* -1 */;
+  String dohName = "";
+  String[] dohIps = {};
+  String dohPath = "";
+  int dohPort = (-1) /* -1 */;
+  boolean ddrEnabled = false;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/ProvisioningConfigurationParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/ProvisioningConfigurationParcelable.aidl
new file mode 100644
index 0000000..0ce91f0
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/ProvisioningConfigurationParcelable.aidl
@@ -0,0 +1,65 @@
+/*
+**
+** Copyright (C) 2019 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.
+*/
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable ProvisioningConfigurationParcelable {
+  /**
+   * @deprecated use ipv4ProvisioningMode instead.
+   */
+  boolean enableIPv4;
+  /**
+   * @deprecated use ipv6ProvisioningMode instead.
+   */
+  boolean enableIPv6;
+  boolean usingMultinetworkPolicyTracker;
+  boolean usingIpReachabilityMonitor;
+  int requestedPreDhcpActionMs;
+  android.net.InitialConfigurationParcelable initialConfig;
+  android.net.StaticIpConfiguration staticIpConfig;
+  android.net.apf.ApfCapabilities apfCapabilities;
+  int provisioningTimeoutMs;
+  int ipv6AddrGenMode;
+  android.net.Network network;
+  String displayName;
+  boolean enablePreconnection;
+  @nullable android.net.ScanResultInfoParcelable scanResultInfo;
+  @nullable android.net.Layer2InformationParcelable layer2Info;
+  @nullable List<android.net.networkstack.aidl.dhcp.DhcpOption> options;
+  int ipv4ProvisioningMode;
+  int ipv6ProvisioningMode;
+  boolean uniqueEui64AddressesOnly;
+  int creatorUid;
+  int hostnameSetting;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/ScanResultInfoParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/ScanResultInfoParcelable.aidl
new file mode 100644
index 0000000..94fc27f
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/ScanResultInfoParcelable.aidl
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable ScanResultInfoParcelable {
+  String ssid;
+  String bssid;
+  android.net.InformationElementParcelable[] informationElements;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/TcpKeepalivePacketDataParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/TcpKeepalivePacketDataParcelable.aidl
new file mode 100644
index 0000000..0e1c21c
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/TcpKeepalivePacketDataParcelable.aidl
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net;
+@JavaDerive(toString=true)
+parcelable TcpKeepalivePacketDataParcelable {
+  byte[] srcAddress;
+  int srcPort;
+  byte[] dstAddress;
+  int dstPort;
+  int seq;
+  int ack;
+  int rcvWnd;
+  int rcvWndScale;
+  int tos;
+  int ttl;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/dhcp/DhcpLeaseParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/dhcp/DhcpLeaseParcelable.aidl
new file mode 100644
index 0000000..3cd8860
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/dhcp/DhcpLeaseParcelable.aidl
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2020, 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+@JavaDerive(toString=true)
+parcelable DhcpLeaseParcelable {
+  byte[] clientId;
+  byte[] hwAddr;
+  int netAddr;
+  int prefixLength;
+  long expTime;
+  String hostname;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/dhcp/DhcpServingParamsParcel.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/dhcp/DhcpServingParamsParcel.aidl
new file mode 100644
index 0000000..7997936
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/dhcp/DhcpServingParamsParcel.aidl
@@ -0,0 +1,49 @@
+/**
+ *
+ * Copyright (C) 2018 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+@JavaDerive(toString=true)
+parcelable DhcpServingParamsParcel {
+  int serverAddr;
+  int serverAddrPrefixLength;
+  int[] defaultRouters;
+  int[] dnsServers;
+  int[] excludedAddrs;
+  long dhcpLeaseTimeSecs;
+  int linkMtu;
+  boolean metered;
+  int singleClientAddr = 0;
+  boolean changePrefixOnDecline = false;
+  int leasesSubnetPrefixLength = 0;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/dhcp/IDhcpEventCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/dhcp/IDhcpEventCallbacks.aidl
new file mode 100644
index 0000000..9312f47
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/dhcp/IDhcpEventCallbacks.aidl
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2020, 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+interface IDhcpEventCallbacks {
+  oneway void onLeasesChanged(in List<android.net.dhcp.DhcpLeaseParcelable> newLeases);
+  oneway void onNewPrefixRequest(in android.net.IpPrefix currentPrefix);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/dhcp/IDhcpServer.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/dhcp/IDhcpServer.aidl
new file mode 100644
index 0000000..1109f35
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/dhcp/IDhcpServer.aidl
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2018, 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+/* @hide */
+interface IDhcpServer {
+  oneway void start(in android.net.INetworkStackStatusCallback cb) = 0;
+  oneway void startWithCallbacks(in android.net.INetworkStackStatusCallback statusCb, in android.net.dhcp.IDhcpEventCallbacks eventCb) = 3;
+  oneway void updateParams(in android.net.dhcp.DhcpServingParamsParcel params, in android.net.INetworkStackStatusCallback cb) = 1;
+  oneway void stop(in android.net.INetworkStackStatusCallback cb) = 2;
+  const int STATUS_UNKNOWN = 0;
+  const int STATUS_SUCCESS = 1;
+  const int STATUS_INVALID_ARGUMENT = 2;
+  const int STATUS_UNKNOWN_ERROR = 3;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/dhcp/IDhcpServerCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/dhcp/IDhcpServerCallbacks.aidl
new file mode 100644
index 0000000..ab8577c
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/dhcp/IDhcpServerCallbacks.aidl
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2018, 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.dhcp;
+/* @hide */
+interface IDhcpServerCallbacks {
+  oneway void onDhcpServerCreated(int statusCode, in android.net.dhcp.IDhcpServer server);
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/ip/IIpClient.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/ip/IIpClient.aidl
new file mode 100644
index 0000000..87de4a6
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/ip/IIpClient.aidl
@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) 2019, 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ip;
+/* @hide */
+interface IIpClient {
+  oneway void completedPreDhcpAction();
+  oneway void confirmConfiguration();
+  oneway void readPacketFilterComplete(in byte[] data);
+  oneway void shutdown();
+  oneway void startProvisioning(in android.net.ProvisioningConfigurationParcelable req);
+  oneway void stop();
+  oneway void setTcpBufferSizes(in String tcpBufferSizes);
+  oneway void setHttpProxy(in android.net.ProxyInfo proxyInfo);
+  oneway void setMulticastFilter(boolean enabled);
+  oneway void addKeepalivePacketFilter(int slot, in android.net.TcpKeepalivePacketDataParcelable pkt);
+  oneway void removeKeepalivePacketFilter(int slot);
+  oneway void setL2KeyAndGroupHint(in String l2Key, in String cluster);
+  oneway void addNattKeepalivePacketFilter(int slot, in android.net.NattKeepalivePacketDataParcelable pkt);
+  oneway void notifyPreconnectionComplete(boolean success);
+  oneway void updateLayer2Information(in android.net.Layer2InformationParcelable info);
+  oneway void updateApfCapabilities(in android.net.apf.ApfCapabilities apfCapabilities);
+  const int PROV_IPV4_DISABLED = 0x00;
+  const int PROV_IPV4_STATIC = 0x01;
+  const int PROV_IPV4_DHCP = 0x02;
+  const int PROV_IPV6_DISABLED = 0x00;
+  const int PROV_IPV6_SLAAC = 0x01;
+  const int PROV_IPV6_LINKLOCAL = 0x02;
+  const int HOSTNAME_SETTING_UNSET = 0x00;
+  const int HOSTNAME_SETTING_SEND = 0x01;
+  const int HOSTNAME_SETTING_DO_NOT_SEND = 0x02;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/ip/IIpClientCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/ip/IIpClientCallbacks.aidl
new file mode 100644
index 0000000..9d36419
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/ip/IIpClientCallbacks.aidl
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2019, 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.ip;
+/* @hide */
+interface IIpClientCallbacks {
+  oneway void onIpClientCreated(in android.net.ip.IIpClient ipClient);
+  oneway void onPreDhcpAction();
+  oneway void onPostDhcpAction();
+  oneway void onNewDhcpResults(in android.net.DhcpResultsParcelable dhcpResults);
+  oneway void onProvisioningSuccess(in android.net.LinkProperties newLp);
+  oneway void onProvisioningFailure(in android.net.LinkProperties newLp);
+  oneway void onLinkPropertiesChange(in android.net.LinkProperties newLp);
+  oneway void onReachabilityLost(in String logMsg);
+  oneway void onQuit();
+  oneway void installPacketFilter(in byte[] filter);
+  oneway void startReadPacketFilter();
+  oneway void setFallbackMulticastFilter(boolean enabled);
+  oneway void setNeighborDiscoveryOffload(boolean enable);
+  oneway void onPreconnectionStart(in List<android.net.Layer2PacketParcelable> packets);
+  oneway void onReachabilityFailure(in android.net.networkstack.aidl.ip.ReachabilityLossInfoParcelable lossInfo);
+  oneway void setMaxDtimMultiplier(int multiplier);
+  const int DTIM_MULTIPLIER_RESET = 0;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/networkstack/aidl/NetworkMonitorParameters.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/networkstack/aidl/NetworkMonitorParameters.aidl
new file mode 100644
index 0000000..2ab9db0
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/networkstack/aidl/NetworkMonitorParameters.aidl
@@ -0,0 +1,41 @@
+/**
+ *
+ * Copyright (C) 2022 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.networkstack.aidl;
+@JavaDerive(equals=true, toString=true)
+parcelable NetworkMonitorParameters {
+  android.net.NetworkAgentConfig networkAgentConfig;
+  android.net.NetworkCapabilities networkCapabilities;
+  android.net.LinkProperties linkProperties;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/networkstack/aidl/dhcp/DhcpOption.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/networkstack/aidl/dhcp/DhcpOption.aidl
new file mode 100644
index 0000000..eea3e0d
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/networkstack/aidl/dhcp/DhcpOption.aidl
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2020, 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 perNmissions and
+ * limitations under the License.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.networkstack.aidl.dhcp;
+@JavaDerive(toString=true)
+parcelable DhcpOption {
+  byte type;
+  @nullable byte[] value;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/networkstack/aidl/ip/ReachabilityLossInfoParcelable.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/networkstack/aidl/ip/ReachabilityLossInfoParcelable.aidl
new file mode 100644
index 0000000..bb88434
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/networkstack/aidl/ip/ReachabilityLossInfoParcelable.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.networkstack.aidl.ip;
+@JavaDerive(equals=true, toString=true) @JavaOnlyImmutable
+parcelable ReachabilityLossInfoParcelable {
+  String message;
+  android.net.networkstack.aidl.ip.ReachabilityLossReason reason;
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/networkstack/aidl/ip/ReachabilityLossReason.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/networkstack/aidl/ip/ReachabilityLossReason.aidl
new file mode 100644
index 0000000..f9bb3c4
--- /dev/null
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/23/android/net/networkstack/aidl/ip/ReachabilityLossReason.aidl
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+///////////////////////////////////////////////////////////////////////////////
+// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE.                          //
+///////////////////////////////////////////////////////////////////////////////
+
+// This file is a snapshot of an AIDL file. Do not edit it manually. There are
+// two cases:
+// 1). this is a frozen version file - do not edit this in any case.
+// 2). this is a 'current' file. If you make a backwards compatible change to
+//     the interface (from the latest frozen version), the build system will
+//     prompt you to update this file with `m <name>-update-api`.
+//
+// You must not make a backward incompatible change to any AIDL file built
+// with the aidl_interface module type with versions property set. The module
+// type is used to build AIDL files in a way that they can be used across
+// independently updatable components of the system. If a device is shipped
+// with such a backward incompatible change, it has a high risk of breaking
+// later when a module using the interface is updated, e.g., Mainline modules.
+
+package android.net.networkstack.aidl.ip;
+@Backing(type="int")
+enum ReachabilityLossReason {
+  ROAM,
+  CONFIRM,
+  ORGANIC,
+}
diff --git a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkMonitorCallbacks.aidl b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkMonitorCallbacks.aidl
index 36eda8e..2a4fb1d 100644
--- a/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkMonitorCallbacks.aidl
+++ b/common/networkstackclient/aidl_api/networkstack-aidl-interfaces/current/android/net/INetworkMonitorCallbacks.aidl
@@ -34,13 +34,13 @@
 package android.net;
 /* @hide */
 interface INetworkMonitorCallbacks {
-  oneway void onNetworkMonitorCreated(in android.net.INetworkMonitor networkMonitor) = 0;
-  oneway void notifyNetworkTested(int testResult, @nullable String redirectUrl) = 1;
-  oneway void notifyPrivateDnsConfigResolved(in android.net.PrivateDnsConfigParcel config) = 2;
-  oneway void showProvisioningNotification(String action, String packageName) = 3;
-  oneway void hideProvisioningNotification() = 4;
-  oneway void notifyProbeStatusChanged(int probesCompleted, int probesSucceeded) = 5;
-  oneway void notifyNetworkTestedWithExtras(in android.net.NetworkTestResultParcelable result) = 6;
-  oneway void notifyDataStallSuspected(in android.net.DataStallReportParcelable report) = 7;
-  oneway void notifyCaptivePortalDataChanged(in android.net.CaptivePortalData data) = 8;
+  void onNetworkMonitorCreated(in android.net.INetworkMonitor networkMonitor) = 0;
+  void notifyNetworkTested(int testResult, @nullable String redirectUrl) = 1;
+  void notifyPrivateDnsConfigResolved(in android.net.PrivateDnsConfigParcel config) = 2;
+  void showProvisioningNotification(String action, String packageName) = 3;
+  void hideProvisioningNotification() = 4;
+  void notifyProbeStatusChanged(int probesCompleted, int probesSucceeded) = 5;
+  void notifyNetworkTestedWithExtras(in android.net.NetworkTestResultParcelable result) = 6;
+  void notifyDataStallSuspected(in android.net.DataStallReportParcelable report) = 7;
+  void notifyCaptivePortalDataChanged(in android.net.CaptivePortalData data) = 8;
 }
diff --git a/common/networkstackclient/src/android/net/INetworkMonitorCallbacks.aidl b/common/networkstackclient/src/android/net/INetworkMonitorCallbacks.aidl
index b5fd280..280620e 100644
--- a/common/networkstackclient/src/android/net/INetworkMonitorCallbacks.aidl
+++ b/common/networkstackclient/src/android/net/INetworkMonitorCallbacks.aidl
@@ -24,7 +24,7 @@
 import android.os.PersistableBundle;
 
 /** @hide */
-oneway interface INetworkMonitorCallbacks {
+interface INetworkMonitorCallbacks {
     void onNetworkMonitorCreated(in INetworkMonitor networkMonitor) = 0;
 
     // Deprecated. Use notifyNetworkTestedWithExtras() instead.
@@ -36,4 +36,4 @@
     void notifyNetworkTestedWithExtras(in NetworkTestResultParcelable result) = 6;
     void notifyDataStallSuspected(in DataStallReportParcelable report) = 7;
     void notifyCaptivePortalDataChanged(in CaptivePortalData data) = 8;
-}
\ No newline at end of file
+}
diff --git a/common/networkstackclient/src/android/net/networkstack/NetworkStackClientBase.java b/common/networkstackclient/src/android/net/networkstack/NetworkStackClientBase.java
index c2f7ddd..1093426 100644
--- a/common/networkstackclient/src/android/net/networkstack/NetworkStackClientBase.java
+++ b/common/networkstackclient/src/android/net/networkstack/NetworkStackClientBase.java
@@ -81,7 +81,8 @@
      *
      * <p>The INetworkMonitor will be returned asynchronously through the provided callbacks.
      */
-    public void makeNetworkMonitor(Network network, String name, INetworkMonitorCallbacks cb) {
+    public void makeNetworkMonitor(Network network, @Nullable String name,
+            INetworkMonitorCallbacks cb) {
         requestConnector(connector -> {
             try {
                 connector.makeNetworkMonitor(network, name, cb);
diff --git a/jni/network_stack_utils_jni.cpp b/jni/network_stack_utils_jni.cpp
index 6f47d7e..b82f797 100644
--- a/jni/network_stack_utils_jni.cpp
+++ b/jni/network_stack_utils_jni.cpp
@@ -24,6 +24,7 @@
 #include <net/if.h>
 #include <netinet/ether.h>
 #include <netinet/icmp6.h>
+#include <netinet/igmp.h>
 #include <netinet/ip.h>
 #include <netinet/ip6.h>
 #include <netinet/udp.h>
@@ -123,6 +124,115 @@
     }
 }
 
+// fd is a "socket(AF_PACKET, SOCK_RAW, ETH_P_ALL)"
+static void network_stack_units_attachEgressIgmpReportFilter(
+        JNIEnv *env, jclass clazz, jobject javaFd) {
+    static sock_filter filter_code[] = {
+        // Check if skb->pkt_type is PACKET_OUTGOING
+        BPF_LOAD_SKB_PKTTYPE,
+        BPF2_REJECT_IF_NOT_EQUAL(PACKET_OUTGOING),
+
+        // Check if skb->protocol is ETH_P_IP
+        BPF_LOAD_SKB_PROTOCOL,
+        BPF2_REJECT_IF_NOT_EQUAL(ETH_P_IP),
+
+        // Check the protocol is IGMP.
+        BPF_LOAD_IPV4_U8(protocol),
+        BPF2_REJECT_IF_NOT_EQUAL(IPPROTO_IGMP),
+
+        // Check this is not a fragment.
+        BPF_LOAD_IPV4_BE16(frag_off),
+        BPF2_REJECT_IF_ANY_MASKED_BITS_SET(IP_MF | IP_OFFMASK),
+
+        // Get the IP header length.
+        BPF_LOADX_NET_RELATIVE_IPV4_HLEN,
+
+        // Check if IGMPv2/IGMPv3 join/leave message.
+        BPF_LOAD_NETX_RELATIVE_IGMP_TYPE,
+        BPF2_ACCEPT_IF_EQUAL(IGMPV2_HOST_MEMBERSHIP_REPORT),
+        BPF2_ACCEPT_IF_EQUAL(IGMP_HOST_LEAVE_MESSAGE),
+        BPF2_ACCEPT_IF_EQUAL(IGMPV3_HOST_MEMBERSHIP_REPORT),
+        BPF_REJECT,
+    };
+    static const sock_fprog filter = {
+        sizeof(filter_code) / sizeof(filter_code[0]),
+        filter_code,
+    };
+
+    int fd = netjniutils::GetNativeFileDescriptor(env, javaFd);
+    if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) {
+        jniThrowErrnoException(env, "setsockopt(SO_ATTACH_FILTER)", errno);
+    }
+}
+
+// fd is a "socket(AF_PACKET, SOCK_RAW, ETH_P_ALL)"
+static void network_stack_units_attachEgressMulticastReportFilter(
+        JNIEnv *env, jclass clazz, jobject javaFd) {
+    static sock_filter filter_code[] = {
+        // Check if skb->pkt_type is PACKET_OUTGOING
+        BPF_LOAD_SKB_PKTTYPE,
+        BPF2_REJECT_IF_NOT_EQUAL(PACKET_OUTGOING),
+
+        // If IPv4: (otherwise jump to the 'IPv6 ...' below)
+        // Check if skb->protocol is ETH_P_IP
+        BPF_LOAD_SKB_PROTOCOL,
+        // Jump over instructions after this and before IPv6 handling section
+        BPF_JUMP_IF_NOT_EQUAL(ETH_P_IP, 15),
+
+        // Check the protocol is IGMP.
+        BPF_LOAD_IPV4_U8(protocol),
+        BPF2_REJECT_IF_NOT_EQUAL(IPPROTO_IGMP),
+
+        // Check this is not a fragment.
+        BPF_LOAD_IPV4_BE16(frag_off),
+        BPF2_REJECT_IF_ANY_MASKED_BITS_SET(IP_MF | IP_OFFMASK),
+
+        // Get the IP header length.
+        BPF_LOADX_NET_RELATIVE_IPV4_HLEN,
+
+        // Check if IGMPv2/IGMPv3 join/leave message.
+        BPF_LOAD_NETX_RELATIVE_IGMP_TYPE,
+        BPF2_ACCEPT_IF_EQUAL(IGMPV2_HOST_MEMBERSHIP_REPORT),
+        BPF2_ACCEPT_IF_EQUAL(IGMP_HOST_LEAVE_MESSAGE),
+        BPF2_ACCEPT_IF_EQUAL(IGMPV3_HOST_MEMBERSHIP_REPORT),
+        BPF_REJECT,
+
+        // IPv6 ...
+        // Check if skb->protocol is ETH_P_IPV6
+        BPF2_REJECT_IF_NOT_EQUAL(ETH_P_IPV6),
+
+        BPF_LOADX_CONSTANT_IPV6_HLEN,
+
+        // Check IPv6 Next Header is HOPOPTS
+        BPF_LOAD_IPV6_U8(nexthdr),
+        BPF2_REJECT_IF_NOT_EQUAL(IPPROTO_HOPOPTS),
+
+        // Check if HOPOPTS is ICMPv6
+        BPF_LOAD_NETX_RELATIVE_V6EXTHDR_NEXTHDR,
+        BPF2_REJECT_IF_NOT_EQUAL(IPPROTO_ICMPV6),
+
+        // Skip the IPv6 extension header
+        BPF3_LOAD_NETX_RELATIVE_V6EXTHDR_LEN,
+        BPF2_ADD_A_TO_X,
+
+        // Check if MLDv1/MLDv2 report message
+        BPF_LOAD_NETX_RELATIVE_MLD_TYPE,
+        BPF2_ACCEPT_IF_EQUAL(MLD_LISTENER_REPORT),
+        BPF2_ACCEPT_IF_EQUAL(MLD_LISTENER_DONE),
+        BPF2_ACCEPT_IF_EQUAL(MLDV2_LISTENER_REPORT),
+        BPF_REJECT
+    };
+    static const sock_fprog filter = {
+            sizeof(filter_code) / sizeof(filter_code[0]),
+            filter_code,
+    };
+
+    int fd = netjniutils::GetNativeFileDescriptor(env, javaFd);
+    if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) {
+        jniThrowErrnoException(env, "setsockopt(SO_ATTACH_FILTER)", errno);
+    }
+}
+
 // fd is a "socket(AF_PACKET, SOCK_RAW, ETH_P_IPV6)"
 // which guarantees packets already have skb->protocol == htons(ETH_P_IPV6)
 static void network_stack_utils_attachRaFilter(JNIEnv *env, jclass clazz, jobject javaFd) {
@@ -229,6 +339,8 @@
     { "addArpEntry", "([B[BLjava/lang/String;Ljava/io/FileDescriptor;)V", (void*) network_stack_utils_addArpEntry },
     { "attachDhcpFilter", "(Ljava/io/FileDescriptor;)V", (void*) network_stack_utils_attachDhcpFilter },
     { "attachRaFilter", "(Ljava/io/FileDescriptor;)V", (void*) network_stack_utils_attachRaFilter },
+    { "attachEgressIgmpReportFilter", "(Ljava/io/FileDescriptor;)V", (void*) network_stack_units_attachEgressIgmpReportFilter },
+    { "attachEgressMulticastReportFilter", "(Ljava/io/FileDescriptor;)V", (void*) network_stack_units_attachEgressMulticastReportFilter },
     { "attachControlPacketFilter", "(Ljava/io/FileDescriptor;)V", (void*) network_stack_utils_attachControlPacketFilter },
 };
 
diff --git a/proguard.flags b/proguard.flags
index 5a96d5a..96af0c5 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -1,3 +1,6 @@
+# Keep JNI registered methods
+-keepclasseswithmembers,includedescriptorclasses class * { native <methods>; }
+
 -keepclassmembers class com.android.networkstack.android.net.ip.IpClient$IpClientCommands {
     static final int CMD_*;
     static final int EVENT_*;
diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml
index 2267cd4..5e22718 100644
--- a/res/values-iw/strings.xml
+++ b/res/values-iw/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
deleted file mode 100644
index c88587b..0000000
--- a/src/android/net/apf/AndroidPacketFilter.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2023 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.annotation.Nullable;
-import android.net.LinkProperties;
-import android.net.NattKeepalivePacketDataParcelable;
-import android.net.TcpKeepalivePacketDataParcelable;
-
-import com.android.internal.util.IndentingPrintWriter;
-
-/**
- * The interface for AndroidPacketFilter
- */
-public interface AndroidPacketFilter {
-    /**
-     * Update the LinkProperties that will be used by APF.
-     */
-    void setLinkProperties(LinkProperties lp);
-
-    /**
-     * Shutdown the APF.
-     */
-    void shutdown();
-
-    /**
-     * Switch for the multicast filter.
-     * @param isEnabled if  the multicast filter should be enabled or not.
-     */
-    void setMulticastFilter(boolean isEnabled);
-
-    /**
-     * Set the APF data snapshot and return the latest counter snapshot as a String.
-     */
-    String setDataSnapshot(byte[] data);
-
-    /**
-     * Add TCP keepalive ack packet filter.
-     * This will add a filter to drop acks to the keepalive packet passed as an argument.
-     *
-     * @param slot The index used to access the filter.
-     * @param sentKeepalivePacket The attributes of the sent keepalive packet.
-     */
-    void addTcpKeepalivePacketFilter(int slot,
-            TcpKeepalivePacketDataParcelable sentKeepalivePacket);
-
-    /**
-     * Add NAT-T keepalive packet filter.
-     * This will add a filter to drop NAT-T keepalive packet which is passed as an argument.
-     *
-     * @param slot The index used to access the filter.
-     * @param sentKeepalivePacket The attributes of the sent keepalive packet.
-     */
-    void addNattKeepalivePacketFilter(int slot,
-            NattKeepalivePacketDataParcelable sentKeepalivePacket);
-
-    /**
-     * Remove keepalive packet filter.
-     *
-     * @param slot The index used to access the filter.
-     */
-    void removeKeepalivePacketFilter(int slot);
-
-    /**
-     * Dump the status of APF.
-     */
-    void dump(IndentingPrintWriter pw);
-
-    /**
-     * Indicates whether the ApfFilter is currently running / paused for test and debugging
-     * purposes.
-     */
-    boolean isRunning();
-
-    /**
-     * Indicates whether the clat interface is added or removed.
-     */
-    default void updateClatInterfaceState(boolean add) {}
-
-    /** Pause ApfFilter updates for testing purposes. */
-    void pause();
-
-    /** Resume ApfFilter updates for testing purposes. */
-    void resume();
-
-    /** Return hex string of current APF snapshot for testing purposes. */
-    @Nullable String getDataSnapshotHexString();
-
-    /**
-     * Determines whether the APF interpreter advertises support for the data buffer access
-     * opcodes LDDW (LoaD Data Word) and STDW (STore Data Word).
-     */
-    default boolean hasDataAccess(int apfVersionSupported) {
-        return apfVersionSupported > 2;
-    }
-
-    /**
-     * Whether the ApfFilter supports generating ND offload code.
-     */
-    default boolean supportNdOffload() {
-        return false;
-    }
-
-    /**
-     * Return if the ApfFilter should enable mDNS offload.
-     */
-    default boolean shouldEnableMdnsOffload() {
-        return false;
-    }
-}
diff --git a/src/android/net/apf/ApfConstants.java b/src/android/net/apf/ApfConstants.java
index a23e970..9a5af85 100644
--- a/src/android/net/apf/ApfConstants.java
+++ b/src/android/net/apf/ApfConstants.java
@@ -15,6 +15,23 @@
  */
 package android.net.apf;
 
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+
+import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_IGMP_TYPE_V1_REPORT;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_IGMP_TYPE_V2_JOIN_REPORT;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_IGMP_TYPE_V2_LEAVE_REPORT;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_IGMP_TYPE_V3_REPORT;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_OPTION_LEN_ROUTER_ALERT;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_OPTION_TYPE_ROUTER_ALERT;
+
+import android.net.InetAddresses;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Set;
+
 /**
  * The class which declares constants used in ApfFilter and unit tests.
  */
@@ -38,6 +55,51 @@
     public static final int IPV4_DEST_ADDR_OFFSET = ETH_HEADER_LEN + 16;
     public static final int IPV4_ANY_HOST_ADDRESS = 0;
     public static final int IPV4_BROADCAST_ADDRESS = -1; // 255.255.255.255
+    // The IPv4 all hosts destination 224.0.0.1
+    public static final byte[] IPV4_ALL_HOSTS_ADDRESS =
+            InetAddresses.parseNumericAddress("224.0.0.1").getAddress();
+    // The IPv4 all multicast routers destination 224.0.0.22
+    public static final byte[] IPV4_ALL_IGMPV3_MULTICAST_ROUTERS_ADDRESS =
+            InetAddresses.parseNumericAddress("224.0.0.22").getAddress();
+    public static long IPV4_ALL_HOSTS_ADDRESS_IN_LONG = 0xe0000001L; // 224.0.0.1
+    public static final int IPV4_IGMP_TYPE_QUERY = 0x11;
+    public static final Set<Long> IGMP_TYPE_REPORTS = Set.of(
+            (long) IPV4_IGMP_TYPE_V1_REPORT,
+            (long) IPV4_IGMP_TYPE_V2_JOIN_REPORT,
+            (long) IPV4_IGMP_TYPE_V2_LEAVE_REPORT,
+            (long) IPV4_IGMP_TYPE_V3_REPORT);
+    public static final byte[] IPV4_ROUTER_ALERT_OPTION = {
+            (byte) IPV4_OPTION_TYPE_ROUTER_ALERT,   // option type
+            (byte) IPV4_OPTION_LEN_ROUTER_ALERT,    // option length
+            0,  0   // option value
+    };
+    public static final int IPV4_ROUTER_ALERT_OPTION_LEN = 4;
+    public static final int IGMP_CHECKSUM_WITH_ROUTER_ALERT_OFFSET =
+            ETHER_HEADER_LEN + IPV4_HEADER_MIN_LEN + IPV4_ROUTER_ALERT_OPTION_LEN + 2;
+    public static final byte[] IGMPV2_REPORT_FROM_IPV4_OPTION_TO_IGMP_CHECKSUM = {
+            // option type
+            (byte) IPV4_OPTION_TYPE_ROUTER_ALERT,
+            // option length
+            (byte) IPV4_OPTION_LEN_ROUTER_ALERT,
+            // option value
+            0,  0,
+            // IGMP type
+            // Indicating an IGMPv2 Membership Report (Join Group)
+            (byte) IPV4_IGMP_TYPE_V2_JOIN_REPORT,
+            // max response time
+            // Typically used in IGMP queries,but is not significant in IGMPv2 reports.
+            0,
+            // checksum, calculate later
+            0, 0
+    };
+
+    // IGMPv3 group record types
+    // From include/uapi/linux/igmp.h
+    public static final int IGMPV3_MODE_IS_EXCLUDE = 2;
+
+    // MLDv2 group record types
+    // From include/uapi/linux/icmpv6.h
+    public static final int MLD2_MODE_IS_EXCLUDE = 2;
 
     // Traffic class and Flow label are not byte aligned. Luckily we
     // don't care about either value so we'll consider bytes 1-3 of the
@@ -59,10 +121,69 @@
     // The IPv6 solicited nodes multicast address prefix ff02::1:ffXX:X/104
     public static final byte[] IPV6_SOLICITED_NODES_PREFIX =
             { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, (byte) 0xff};
+    public static final byte[] IPV6_MLD_V2_ALL_ROUTERS_MULTICAST_ADDRESS =
+            { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (byte) 0x16 };
 
+    /**
+     * IPv6 Router Alert Option constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc2711
+     */
+    public static final int IPV6_HBH_ROUTER_ALERT_OPTION_TYPE = 5;
+    public static final int IPV6_HBH_ROUTER_ALERT_OPTION_LEN = 2;
+
+    public static final int IPV6_HBH_PADN_OPTION_TYPE = 1;
+
+    /**
+     * IPv6 MLD constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc2710
+     *     - https://tools.ietf.org/html/rfc3810
+     */
+    public static final int IPV6_MLD_MESSAGE_MIN_SIZE = 8;
+    public static final int IPV6_MLD_MIN_SIZE = 24; // including icmp header
+    public static final int IPV6_MLD_TYPE_QUERY = 130;
+    public static final int IPV6_MLD_TYPE_V1_REPORT = 131;
+    public static final int IPV6_MLD_TYPE_V1_DONE = 132;
+    public static final int IPV6_MLD_TYPE_V2_REPORT = 143;
+    public static final int IPV6_MLD_V1_MESSAGE_SIZE = 24;
+    public static final int IPV6_MLD_V2_MULTICAST_ADDRESS_RECORD_SIZE = 20;
+    // kernel reference: net/ipv6/mcast.c#igmp6_send()
+    public static final byte[] IPV6_MLD_HOPOPTS = {
+            (byte) IPPROTO_ICMPV6,   // next header type
+            0,  // next header length
+            (byte) IPV6_HBH_ROUTER_ALERT_OPTION_TYPE, // Router Alert option type
+            (byte) IPV6_HBH_ROUTER_ALERT_OPTION_LEN,  // Router Alert option length
+            0,  0,  // Router Alert option value
+            (byte) IPV6_HBH_PADN_OPTION_TYPE, (byte) 0x00  // PadN type and length
+    };
+
+    public static final Set<Long> IPV6_MLD_TYPE_REPORTS = Set.of(
+            (long) IPV6_MLD_TYPE_V1_REPORT,
+            (long) IPV6_MLD_TYPE_V1_DONE,
+            (long) IPV6_MLD_TYPE_V2_REPORT
+    );
+    public static final int IPV6_EXT_HEADER_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN;
+    public static final int IPV6_MLD_CHECKSUM_OFFSET =
+            ETHER_HEADER_LEN + IPV6_HEADER_LEN + IPV6_MLD_HOPOPTS.length + 2;
+    public static final int IPV6_MLD_TYPE_OFFSET =
+            IPV6_EXT_HEADER_OFFSET + IPV6_MLD_HOPOPTS.length;
+    public static final int IPV6_MLD_MULTICAST_ADDR_OFFSET =
+            IPV6_EXT_HEADER_OFFSET + IPV6_MLD_HOPOPTS.length + 8;
+
+    public static final int ICMP4_TYPE_NO_OPTIONS_OFFSET = ETH_HEADER_LEN + IPV4_HEADER_MIN_LEN;
+    public static final int ICMP4_CHECKSUM_NO_OPTIONS_OFFSET =
+            ETH_HEADER_LEN + IPV4_HEADER_MIN_LEN + 2;
+    public static final int ICMP4_CONTENT_NO_OPTIONS_OFFSET =
+            ETH_HEADER_LEN + IPV4_HEADER_MIN_LEN + 4;
+
+    public static final int ICMP6_ECHO_REQUEST_HEADER_LEN = 8;
     public static final int ICMP6_TYPE_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN;
     public static final int ICMP6_CODE_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN + 1;
     public static final int ICMP6_CHECKSUM_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN + 2;
+    public static final int ICMP6_CONTENT_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN + 4;
     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:
@@ -106,6 +227,8 @@
     // NOTE: this must be added to the IPv4 header length in MemorySlot.IPV4_HEADER_SIZE
     public static final int TCP_UDP_SOURCE_PORT_OFFSET = ETH_HEADER_LEN;
     public static final int TCP_UDP_DESTINATION_PORT_OFFSET = ETH_HEADER_LEN + 2;
+    public static final int IGMP_MAX_RESP_TIME_OFFSET = ETHER_HEADER_LEN + 1;
+    public static final int IGMP_MULTICAST_ADDRESS_OFFSET = ETH_HEADER_LEN + 4;
     public static final int UDP_HEADER_LEN = 8;
 
     public static final int TCP_HEADER_SIZE_OFFSET = 12;
@@ -113,6 +236,24 @@
     public static final int DHCP_SERVER_PORT = 67;
     public static final int DHCP_CLIENT_PORT = 68;
 
+    public static final int DNS_HEADER_LEN = 12;
+    public static final int IPV4_UDP_DESTINATION_PORT_NO_OPTIONS_OFFSET =
+            ETH_HEADER_LEN + IPV4_HEADER_MIN_LEN + 2;
+    public static final int IPV4_UDP_DESTINATION_CHECKSUM_NO_OPTIONS_OFFSET =
+            ETH_HEADER_LEN + IPV4_HEADER_MIN_LEN + 6;
+    public static final int IPV4_UDP_PAYLOAD_NO_OPTIONS_OFFSET =
+            ETH_HEADER_LEN + IPV4_HEADER_MIN_LEN + UDP_HEADER_LEN;
+    public static final int IPV4_DNS_QDCOUNT_NO_OPTIONS_OFFSET =
+            ETH_HEADER_LEN + IPV4_HEADER_MIN_LEN + UDP_HEADER_LEN + 4;
+    public static final int IPV6_UDP_DESTINATION_PORT_OFFSET =
+            ETH_HEADER_LEN + IPV6_HEADER_LEN + 2;
+    public static final int IPV6_UDP_DESTINATION_CHECKSUM_OFFSET =
+            ETH_HEADER_LEN + IPV6_HEADER_LEN + 6;
+    public static final int IPv6_UDP_PAYLOAD_OFFSET =
+            ETH_HEADER_LEN + IPV6_HEADER_LEN + UDP_HEADER_LEN;
+    public static final int IPV6_DNS_QDCOUNT_OFFSET =
+            ETH_HEADER_LEN + IPV6_HEADER_LEN + UDP_HEADER_LEN + 4;
+
     public static final int ARP_HEADER_OFFSET = ETH_HEADER_LEN;
     public static final byte[] ARP_IPV4_HEADER = {
             0, 1, // Hardware type: Ethernet (1)
@@ -139,18 +280,23 @@
             {(byte) 0x01, (byte) 0x00, (byte) 0x5e, (byte) 0x00, (byte) 0x00, (byte) 0xfb};
     public static final byte[] ETH_MULTICAST_MDNS_V6_MAC_ADDRESS =
             {(byte) 0x33, (byte) 0x33, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xfb};
+    public static final byte[] ETH_MULTICAST_IGMP_V3_ALL_MULTICAST_ROUTERS_ADDRESS =
+            { (byte) 0x01, 0, (byte) 0x5e, 0, 0, (byte) 0x16};
+    public static final byte[] ETH_MULTICAST_MLD_V2_ALL_MULTICAST_ROUTERS_ADDRESS =
+            { (byte) 0x33, (byte) 0x33, 0, 0, 0, (byte) 0x16};
     public static final int MDNS_PORT = 5353;
+    public static final byte[] MDNS_PORT_IN_BYTES = ByteBuffer.allocate(2).order(
+            ByteOrder.BIG_ENDIAN).putShort((short) MDNS_PORT).array();
 
+    public static final long MDNS_IPV4_ADDR_IN_LONG = 0xE00000FBL; // 224.0.0.251
+    public static final byte[] MDNS_IPV4_ADDR = InetAddresses.parseNumericAddress(
+            "224.0.0.251").getAddress();
+    public static final byte[] MDNS_IPV6_ADDR = InetAddresses.parseNumericAddress(
+            "FF02::FB").getAddress();
     public static final int ECHO_PORT = 7;
-    public static final int DNS_HEADER_LEN = 12;
-    public static final int DNS_QDCOUNT_OFFSET = 4;
     // NOTE: this must be added to the IPv4 header length in MemorySlot.IPV4_HEADER_SIZE, or the
     // IPv6 header length.
     public static final int DHCP_CLIENT_MAC_OFFSET = ETH_HEADER_LEN + UDP_HEADER_LEN + 28;
-    public static final int MDNS_QDCOUNT_OFFSET =
-            ETH_HEADER_LEN + UDP_HEADER_LEN + DNS_QDCOUNT_OFFSET;
-    public static final int MDNS_QNAME_OFFSET =
-            ETH_HEADER_LEN + UDP_HEADER_LEN + DNS_HEADER_LEN;
 
     /**
      * Fixed byte sequence representing the following part of the ARP reply header:
diff --git a/src/android/net/apf/ApfCounterTracker.java b/src/android/net/apf/ApfCounterTracker.java
index 9700b5b..6b15bee 100644
--- a/src/android/net/apf/ApfCounterTracker.java
+++ b/src/android/net/apf/ApfCounterTracker.java
@@ -26,7 +26,7 @@
 import java.util.Map;
 
 /**
- * Common counter class for {@code ApfFilter} and {@code LegacyApfFilter}.
+ * Counter class for {@code ApfFilter}.
  *
  * @hide
  */
@@ -45,45 +45,44 @@
         PASSED_ALLOCATE_FAILURE, // hardcoded in APFv6 interpreter
         PASSED_TRANSMIT_FAILURE, // hardcoded in APFv6 interpreter
         CORRUPT_DNS_PACKET,      // hardcoded in APFv6 interpreter
+        EXCEPTIONS,              // hardcoded in APFv6.1 interpreter
         FILTER_AGE_SECONDS,
         FILTER_AGE_16384THS,
         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
-        PASSED_ARP_NON_IPV4,
+        PASSED_ARP_BROADCAST_REPLY,  // see also MIN_PASS_COUNTER below.
         PASSED_ARP_REQUEST,
         PASSED_ARP_UNICAST_REPLY,
-        PASSED_ARP_UNKNOWN,
         PASSED_DHCP,
         PASSED_ETHER_OUR_SRC_MAC,
         PASSED_IPV4,
         PASSED_IPV4_FROM_DHCPV4_SERVER,
         PASSED_IPV4_UNICAST,
+        PASSED_IPV6_HOPOPTS,
         PASSED_IPV6_ICMP,
         PASSED_IPV6_NON_ICMP,
-        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,
-        PASSED_MLD,  // see also MAX_PASS_COUNTER below
+        PASSED_MDNS, // see also MAX_PASS_COUNTER below
         DROPPED_ETH_BROADCAST,  // see also MIN_DROP_COUNTER below
+        DROPPED_ETHER_OUR_SRC_MAC,
         DROPPED_RA,
         DROPPED_IPV4_L2_BROADCAST,
         DROPPED_IPV4_BROADCAST_ADDR,
         DROPPED_IPV4_BROADCAST_NET,
+        DROPPED_IPV4_ICMP_INVALID,
         DROPPED_IPV4_MULTICAST,
         DROPPED_IPV4_NON_DHCP4,
+        DROPPED_IPV4_PING_REQUEST_REPLIED,
+        DROPPED_IPV6_ICMP6_ECHO_REQUEST_INVALID,
+        DROPPED_IPV6_ICMP6_ECHO_REQUEST_REPLIED,
         DROPPED_IPV6_ROUTER_SOLICITATION,
+        DROPPED_IPV6_MLD_INVALID,
+        DROPPED_IPV6_MLD_REPORT,
+        DROPPED_IPV6_MLD_V1_GENERAL_QUERY_REPLIED,
+        DROPPED_IPV6_MLD_V2_GENERAL_QUERY_REPLIED,
         DROPPED_IPV6_MULTICAST_NA,
-        DROPPED_IPV6_MULTICAST,
-        DROPPED_IPV6_MULTICAST_PING,
         DROPPED_IPV6_NON_ICMP_MULTICAST,
         DROPPED_IPV6_NS_INVALID,
         DROPPED_IPV6_NS_OTHER_HOST,
@@ -91,17 +90,21 @@
         DROPPED_802_3_FRAME,
         DROPPED_ETHERTYPE_NOT_ALLOWED,
         DROPPED_IPV4_KEEPALIVE_ACK,
-        DROPPED_IPV6_KEEPALIVE_ACK,
         DROPPED_IPV4_NATT_KEEPALIVE,
         DROPPED_MDNS,
+        DROPPED_MDNS_REPLIED,
+        DROPPED_NON_UNICAST_TDLS,
         DROPPED_IPV4_TCP_PORT7_UNICAST,
         DROPPED_ARP_NON_IPV4,
         DROPPED_ARP_OTHER_HOST,
         DROPPED_ARP_REPLY_SPA_NO_HOST,
-        DROPPED_ARP_REQUEST_ANYHOST,
         DROPPED_ARP_REQUEST_REPLIED,
         DROPPED_ARP_UNKNOWN,
         DROPPED_ARP_V6_ONLY,
+        DROPPED_IGMP_V2_GENERAL_QUERY_REPLIED,
+        DROPPED_IGMP_V3_GENERAL_QUERY_REPLIED,
+        DROPPED_IGMP_INVALID,
+        DROPPED_IGMP_REPORT,
         DROPPED_GARP_REPLY;  // see also MAX_DROP_COUNTER below
 
         /**
@@ -109,7 +112,7 @@
          * a given counter.
          */
         public int offset() {
-            return -this.ordinal() * 4;  // Currently, all counters are 32bit long.
+            return -this.value() * 4;  // Currently, all counters are 32bit long.
         }
 
         /**
@@ -139,12 +142,38 @@
             }
             return RESERVED_OOB;
         }
+
+        private void checkCounterRange(Counter lowerBound, Counter upperBound) {
+            if (value() < lowerBound.value() || value() > upperBound.value()) {
+                throw new IllegalArgumentException(
+                        String.format("Counter %s, is not in range [%s, %s]", this,
+                                lowerBound, upperBound));
+            }
+        }
+
+        /**
+         * Return the label such that if we jump to it, the counter will be increased by 1 and
+         * the packet will be passed.
+         */
+        public short getJumpPassLabel() {
+            checkCounterRange(MIN_PASS_COUNTER, MAX_PASS_COUNTER);
+            return (short) (2 * this.value());
+        }
+
+        /**
+         * Return the label such that if we jump to it, the counter will be increased by 1 and
+         * the packet will be dropped.
+         */
+        public short getJumpDropLabel() {
+            checkCounterRange(MIN_DROP_COUNTER, MAX_DROP_COUNTER);
+            return (short) (2 * this.value() + 1);
+        }
     }
 
     public static final Counter MIN_DROP_COUNTER = Counter.DROPPED_ETH_BROADCAST;
     public static final Counter MAX_DROP_COUNTER = Counter.DROPPED_GARP_REPLY;
-    public static final Counter MIN_PASS_COUNTER = Counter.PASSED_ARP;
-    public static final Counter MAX_PASS_COUNTER = Counter.PASSED_MLD;
+    public static final Counter MIN_PASS_COUNTER = Counter.PASSED_ARP_BROADCAST_REPLY;
+    public static final Counter MAX_PASS_COUNTER = Counter.PASSED_MDNS;
 
     private static final String TAG = ApfCounterTracker.class.getSimpleName();
 
diff --git a/src/android/net/apf/ApfFilter.java b/src/android/net/apf/ApfFilter.java
index 08a370d..249a1e5 100644
--- a/src/android/net/apf/ApfFilter.java
+++ b/src/android/net/apf/ApfFilter.java
@@ -27,21 +27,29 @@
 import static android.net.apf.ApfConstants.DHCP_CLIENT_MAC_OFFSET;
 import static android.net.apf.ApfConstants.DHCP_CLIENT_PORT;
 import static android.net.apf.ApfConstants.DHCP_SERVER_PORT;
+import static android.net.apf.ApfConstants.DNS_HEADER_LEN;
 import static android.net.apf.ApfConstants.ECHO_PORT;
 import static android.net.apf.ApfConstants.ETH_DEST_ADDR_OFFSET;
 import static android.net.apf.ApfConstants.ETH_ETHERTYPE_OFFSET;
 import static android.net.apf.ApfConstants.ETH_HEADER_LEN;
+import static android.net.apf.ApfConstants.ETH_MULTICAST_IGMP_V3_ALL_MULTICAST_ROUTERS_ADDRESS;
 import static android.net.apf.ApfConstants.ETH_MULTICAST_MDNS_V4_MAC_ADDRESS;
 import static android.net.apf.ApfConstants.ETH_MULTICAST_MDNS_V6_MAC_ADDRESS;
+import static android.net.apf.ApfConstants.ETH_MULTICAST_MLD_V2_ALL_MULTICAST_ROUTERS_ADDRESS;
 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.ICMP4_CHECKSUM_NO_OPTIONS_OFFSET;
+import static android.net.apf.ApfConstants.ICMP4_CONTENT_NO_OPTIONS_OFFSET;
+import static android.net.apf.ApfConstants.ICMP4_TYPE_NO_OPTIONS_OFFSET;
 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_CONTENT_OFFSET;
 import static android.net.apf.ApfConstants.ICMP6_DNSSL_OPTION_TYPE;
+import static android.net.apf.ApfConstants.ICMP6_ECHO_REQUEST_HEADER_LEN;
 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;
@@ -60,53 +68,144 @@
 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.IGMPV2_REPORT_FROM_IPV4_OPTION_TO_IGMP_CHECKSUM;
+import static android.net.apf.ApfConstants.IGMPV3_MODE_IS_EXCLUDE;
+import static android.net.apf.ApfConstants.IGMP_CHECKSUM_WITH_ROUTER_ALERT_OFFSET;
+import static android.net.apf.ApfConstants.IGMP_MAX_RESP_TIME_OFFSET;
+import static android.net.apf.ApfConstants.IGMP_MULTICAST_ADDRESS_OFFSET;
+import static android.net.apf.ApfConstants.IGMP_TYPE_REPORTS;
 import static android.net.apf.ApfConstants.IPPROTO_HOPOPTS;
+import static android.net.apf.ApfConstants.IPV4_ALL_HOSTS_ADDRESS_IN_LONG;
+import static android.net.apf.ApfConstants.IPV4_ALL_IGMPV3_MULTICAST_ROUTERS_ADDRESS;
 import static android.net.apf.ApfConstants.IPV4_ANY_HOST_ADDRESS;
 import static android.net.apf.ApfConstants.IPV4_BROADCAST_ADDRESS;
 import static android.net.apf.ApfConstants.IPV4_DEST_ADDR_OFFSET;
+import static android.net.apf.ApfConstants.IPV4_DNS_QDCOUNT_NO_OPTIONS_OFFSET;
 import static android.net.apf.ApfConstants.IPV4_FRAGMENT_MORE_FRAGS_MASK;
 import static android.net.apf.ApfConstants.IPV4_FRAGMENT_OFFSET_MASK;
 import static android.net.apf.ApfConstants.IPV4_FRAGMENT_OFFSET_OFFSET;
+import static android.net.apf.ApfConstants.IPV4_IGMP_TYPE_QUERY;
 import static android.net.apf.ApfConstants.IPV4_PROTOCOL_OFFSET;
+import static android.net.apf.ApfConstants.IPV4_SRC_ADDR_OFFSET;
+import static android.net.apf.ApfConstants.IPV4_ROUTER_ALERT_OPTION;
+import static android.net.apf.ApfConstants.IPV4_ROUTER_ALERT_OPTION_LEN;
 import static android.net.apf.ApfConstants.IPV4_TOTAL_LENGTH_OFFSET;
+import static android.net.apf.ApfConstants.IPV4_UDP_DESTINATION_CHECKSUM_NO_OPTIONS_OFFSET;
+import static android.net.apf.ApfConstants.IPV4_UDP_DESTINATION_PORT_NO_OPTIONS_OFFSET;
+import static android.net.apf.ApfConstants.IPV4_UDP_PAYLOAD_NO_OPTIONS_OFFSET;
 import static android.net.apf.ApfConstants.IPV6_ALL_NODES_ADDRESS;
 import static android.net.apf.ApfConstants.IPV6_DEST_ADDR_OFFSET;
+import static android.net.apf.ApfConstants.IPV6_DNS_QDCOUNT_OFFSET;
+import static android.net.apf.ApfConstants.IPV6_EXT_HEADER_OFFSET;
 import static android.net.apf.ApfConstants.IPV6_FLOW_LABEL_LEN;
 import static android.net.apf.ApfConstants.IPV6_FLOW_LABEL_OFFSET;
 import static android.net.apf.ApfConstants.IPV6_HEADER_LEN;
 import static android.net.apf.ApfConstants.IPV6_HOP_LIMIT_OFFSET;
+import static android.net.apf.ApfConstants.IPV6_MLD_CHECKSUM_OFFSET;
+import static android.net.apf.ApfConstants.IPV6_MLD_HOPOPTS;
+import static android.net.apf.ApfConstants.IPV6_MLD_MESSAGE_MIN_SIZE;
+import static android.net.apf.ApfConstants.IPV6_MLD_MIN_SIZE;
+import static android.net.apf.ApfConstants.IPV6_MLD_MULTICAST_ADDR_OFFSET;
+import static android.net.apf.ApfConstants.IPV6_MLD_TYPE_OFFSET;
+import static android.net.apf.ApfConstants.IPV6_MLD_TYPE_QUERY;
+import static android.net.apf.ApfConstants.IPV6_MLD_TYPE_REPORTS;
+import static android.net.apf.ApfConstants.IPV6_MLD_TYPE_V1_REPORT;
+import static android.net.apf.ApfConstants.IPV6_MLD_TYPE_V2_REPORT;
+import static android.net.apf.ApfConstants.IPV6_MLD_V1_MESSAGE_SIZE;
+import static android.net.apf.ApfConstants.IPV6_MLD_V2_ALL_ROUTERS_MULTICAST_ADDRESS;
+import static android.net.apf.ApfConstants.IPV6_MLD_V2_MULTICAST_ADDRESS_RECORD_SIZE;
 import static android.net.apf.ApfConstants.IPV6_NEXT_HEADER_OFFSET;
 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_UDP_DESTINATION_CHECKSUM_OFFSET;
+import static android.net.apf.ApfConstants.IPV6_UDP_DESTINATION_PORT_OFFSET;
 import static android.net.apf.ApfConstants.IPV6_UNSPECIFIED_ADDRESS;
-import static android.net.apf.ApfConstants.MDNS_PORT;
+import static android.net.apf.ApfConstants.MLD2_MODE_IS_EXCLUDE;
 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.APF_PROGRAM_ID;
+import static android.net.apf.ApfCounterTracker.Counter.APF_VERSION;
+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_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_ETHER_OUR_SRC_MAC;
+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_IGMP_INVALID;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IGMP_REPORT;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_ICMP_INVALID;
+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_PING_REQUEST_REPLIED;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_TCP_PORT7_UNICAST;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_ICMP6_ECHO_REQUEST_INVALID;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_ICMP6_ECHO_REQUEST_REPLIED;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MLD_INVALID;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MLD_REPORT;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MLD_V1_GENERAL_QUERY_REPLIED;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MLD_V2_GENERAL_QUERY_REPLIED;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MULTICAST_NA;
+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_NON_UNICAST_TDLS;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_RA;
 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_ARP_BROADCAST_REPLY;
+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_DHCP;
+import static android.net.apf.ApfConstants.IPv6_UDP_PAYLOAD_OFFSET;
+import static android.net.apf.ApfConstants.MDNS_IPV4_ADDR;
+import static android.net.apf.ApfConstants.MDNS_IPV4_ADDR_IN_LONG;
+import static android.net.apf.ApfConstants.MDNS_IPV6_ADDR;
+import static android.net.apf.ApfConstants.MDNS_PORT;
+import static android.net.apf.ApfConstants.UDP_HEADER_LEN;
+import static android.net.apf.ApfConstants.MDNS_PORT_IN_BYTES;
 import static android.net.apf.ApfCounterTracker.Counter.PASSED_ETHER_OUR_SRC_MAC;
-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.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_HOPOPTS;
+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_UNICAST_NON_ICMP;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_MDNS;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_NON_IP_UNICAST;
+import static android.net.apf.ApfCounterTracker.Counter.TOTAL_PACKETS;
 import static android.net.apf.ApfCounterTracker.getCounterValue;
 import static android.net.apf.BaseApfGenerator.MemorySlot;
 import static android.net.apf.BaseApfGenerator.Register.R0;
 import static android.net.apf.BaseApfGenerator.Register.R1;
-import static android.net.nsd.OffloadEngine.OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK;
-import static android.net.nsd.OffloadEngine.OFFLOAD_TYPE_REPLY;
 import static android.net.util.SocketUtils.makePacketSocketAddress;
 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.ETH_P_ALL;
 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.ICMP6_ECHO_REPLY;
+import static android.system.OsConstants.ICMP_ECHO;
+import static android.system.OsConstants.ICMP_ECHOREPLY;
 import static android.system.OsConstants.IFA_F_TENTATIVE;
+import static android.system.OsConstants.IPPROTO_ICMP;
 import static android.system.OsConstants.IPPROTO_ICMPV6;
 import static android.system.OsConstants.IPPROTO_TCP;
 import static android.system.OsConstants.IPPROTO_UDP;
@@ -114,10 +213,13 @@
 import static android.system.OsConstants.SOCK_NONBLOCK;
 import static android.system.OsConstants.SOCK_RAW;
 
+import static com.android.net.module.util.CollectionUtils.concatArrays;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_DST_ADDR_OFFSET;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_SRC_ADDR_OFFSET;
+import static com.android.net.module.util.NetworkStackConstants.ICMP_HEADER_LEN;
 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;
@@ -127,13 +229,22 @@
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_SOLICITATION;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ALL_HOST_MULTICAST;
 import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_HEADER_MIN_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_FLAG_DF;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_IGMP_GROUP_RECORD_SIZE;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_IGMP_MIN_SIZE;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_IGMP_TYPE_V3_REPORT;
+import static com.android.net.module.util.NetworkStackConstants.IPV4_PROTOCOL_IGMP;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_NODES_MULTICAST;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ANY;
 import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
+import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_NODE_LOCAL_ALL_NODES_MULTICAST;
 
 import android.annotation.ChecksSdkIntAtLeast;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.RequiresApi;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -145,11 +256,8 @@
 import android.net.TcpKeepalivePacketDataParcelable;
 import android.net.apf.ApfCounterTracker.Counter;
 import android.net.apf.BaseApfGenerator.IllegalInstructionException;
-import android.net.ip.IpClient.IpClientCallbacksWrapper;
+import android.net.ip.MulticastReportMonitor;
 import android.net.nsd.NsdManager;
-import android.net.nsd.OffloadEngine;
-import android.net.nsd.OffloadServiceInfo;
-import android.os.Build;
 import android.os.Handler;
 import android.os.PowerManager;
 import android.os.SystemClock;
@@ -202,7 +310,23 @@
  *
  * @hide
  */
-public class ApfFilter implements AndroidPacketFilter {
+public class ApfFilter {
+
+    /**
+     * Defines the communication API between the ApfFilter and the APF interpreter
+     * residing within the Wi-Fi/Ethernet firmware.
+     */
+    public interface IApfController {
+        /**
+         * Install the APF program to firmware.
+         */
+        boolean installPacketFilter(@NonNull byte[] filter, @NonNull String filterConfig);
+
+        /**
+         * Read the APF RAM from firmware.
+         */
+        void readPacketFilterRam(@NonNull String event);
+    }
 
     // Helper class for specifying functional filter parameters.
     public static class ApfConfiguration {
@@ -216,9 +340,13 @@
         public int acceptRaMinLft;
         public long minMetricsSessionDurationMs;
         public boolean hasClatInterface;
-        public boolean shouldHandleArpOffload;
-        public boolean shouldHandleNdOffload;
-        public boolean shouldHandleMdnsOffload;
+        public boolean handleArpOffload;
+        public boolean handleNdOffload;
+        public boolean handleMdnsOffload;
+        public boolean handleIgmpOffload;
+        public boolean handleMldOffload;
+        public boolean handleIpv4PingOffload;
+        public boolean handleIpv6PingOffload;
     }
 
 
@@ -243,13 +371,11 @@
     }
 
     private static final String TAG = "ApfFilter";
-    private static final boolean DBG = true;
-    private static final boolean VDBG = false;
 
     private final int mApfRamSize;
     private final int mMaximumApfProgramSize;
     private final int mInstallableProgramSizeClamp;
-    private final IpClientCallbacksWrapper mIpClientCallback;
+    private final IApfController mApfController;
     private final InterfaceParams mInterfaceParams;
     private final TokenBucket mTokenBucket;
 
@@ -283,17 +409,27 @@
     // 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 mShouldHandleArpOffload;
-    private final boolean mShouldHandleNdOffload;
-    private final boolean mShouldHandleMdnsOffload;
+    private final boolean mHandleArpOffload;
+    private final boolean mHandleNdOffload;
+    private final boolean mHandleMdnsOffload;
+    private final boolean mHandleIgmpOffload;
+    private final boolean mHandleMldOffload;
+    private final boolean mHandleIpv4PingOffload;
+    private final boolean mHandleIpv6PingOffload;
 
     private final NetworkQuirkMetrics mNetworkQuirkMetrics;
     private final IpClientRaInfoMetrics mIpClientRaInfoMetrics;
     private final ApfSessionInfoMetrics mApfSessionInfoMetrics;
     private final NsdManager mNsdManager;
-    @VisibleForTesting
-    final List<OffloadServiceInfo> mOffloadServiceInfos = new ArrayList<>();
-    private OffloadEngine mOffloadEngine;
+    private final MulticastReportMonitor mMulticastReportMonitor;
+    private final ApfMdnsOffloadEngine mApfMdnsOffloadEngine;
+    private final List<MdnsOffloadRule> mOffloadRules = new ArrayList<>();
+    // The number of mDNS rules requiring APF to transmit a reply and drop the query packet. A
+    // value of -1 means all mDNS query packets should be passed; no mDNS query packets will trigger
+    // the transmit and reply logic.
+    private int mNumOfMdnsRuleToOffload = -1;
+
+    private int mOverEstimatedProgramSize = 0;
 
     private static boolean isDeviceIdleModeChangedAction(Intent intent) {
         return ACTION_DEVICE_IDLE_MODE_CHANGED.equals(intent.getAction());
@@ -350,6 +486,25 @@
     // Our tentative IPv6 addresses
     private Set<Inet6Address> mIPv6TentativeAddresses = new ArraySet<>();
 
+    // Our link-local IPv6 address
+    private Inet6Address mIPv6LinkLocalAddress;
+
+    // Our joined IPv4 multicast addresses
+    @VisibleForTesting
+    final Set<Inet4Address> mIPv4MulticastAddresses = new ArraySet<>();
+
+    // Our joined IPv4 multicast address exclude all all host multicast (224.0.0.1)
+    @VisibleForTesting
+    final Set<Inet4Address> mIPv4McastAddrsExcludeAllHost = new ArraySet<>();
+
+    // Our joined IPv6 multicast addresses
+    @VisibleForTesting
+    final Set<Inet6Address> mIPv6MulticastAddresses = new ArraySet<>();
+
+    // Our joined IPv6 multicast address exclude ff02::1, ff01::1
+    @VisibleForTesting
+    final Set<Inet6Address> mIPv6McastAddrsExcludeAllHost = new ArraySet<>();
+
     // Whether CLAT is enabled.
     private boolean mHasClat;
 
@@ -360,44 +515,10 @@
 
     private final Dependencies mDependencies;
 
-    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
-    private void registerOffloadEngine() {
-        if (mOffloadEngine != null) {
-            Log.wtf(TAG,
-                    "registerOffloadEngine called twice without calling unregisterOffloadEngine");
-            return;
-        }
-        mOffloadEngine = new OffloadEngine() {
-            @Override
-            public void onOffloadServiceUpdated(@NonNull OffloadServiceInfo info) {
-                mOffloadServiceInfos.removeIf(i -> i.getKey().equals(info.getKey()));
-                mOffloadServiceInfos.add(info);
-            }
-
-            @Override
-            public void onOffloadServiceRemoved(@NonNull OffloadServiceInfo info) {
-                mOffloadServiceInfos.removeIf(i -> i.getKey().equals(info.getKey()));
-            }
-        };
-        mNsdManager.registerOffloadEngine(mInterfaceParams.name,
-                OFFLOAD_TYPE_REPLY,
-                OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK,
-                mHandler::post, mOffloadEngine);
-    }
-
-    @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
-    private void unregisterOffloadEngine() {
-        if (mOffloadEngine != null) {
-            mNsdManager.unregisterOffloadEngine(mOffloadEngine);
-            mOffloadServiceInfos.clear();
-            mOffloadEngine = null;
-        }
-    }
-
     public ApfFilter(Handler handler, Context context, ApfConfiguration config,
-            InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback,
+            InterfaceParams ifParams, IApfController apfController,
             NetworkQuirkMetrics networkQuirkMetrics) {
-        this(handler, context, config, ifParams, ipClientCallback, networkQuirkMetrics,
+        this(handler, context, config, ifParams, apfController, networkQuirkMetrics,
                 new Dependencies(context));
     }
 
@@ -406,16 +527,13 @@
         // 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);
-            }
+            installPacketFilter(new byte[mApfRamSize], getApfConfigMessage() + " (cleanup)");
         }
     }
 
     @VisibleForTesting
     public ApfFilter(Handler handler, Context context, ApfConfiguration config,
-            InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback,
+            InterfaceParams ifParams, IApfController apfController,
             NetworkQuirkMetrics networkQuirkMetrics, Dependencies dependencies) {
         mHandler = handler;
         mApfVersionSupported = config.apfVersionSupported;
@@ -430,16 +548,20 @@
         if (maximumApfProgramSize > mInstallableProgramSizeClamp) {
             maximumApfProgramSize = mInstallableProgramSizeClamp;
         }
-        mMaximumApfProgramSize = maximumApfProgramSize;
-        mIpClientCallback = ipClientCallback;
+        mMaximumApfProgramSize = Math.max(0, maximumApfProgramSize);
+        mApfController = apfController;
         mInterfaceParams = ifParams;
         mMulticastFilter = config.multicastFilter;
         mDrop802_3Frames = config.ieee802_3Filter;
         mMinRdnssLifetimeSec = config.minRdnssLifetimeSec;
         mAcceptRaMinLft = config.acceptRaMinLft;
-        mShouldHandleArpOffload = config.shouldHandleArpOffload;
-        mShouldHandleNdOffload = config.shouldHandleNdOffload;
-        mShouldHandleMdnsOffload = config.shouldHandleMdnsOffload;
+        mHandleArpOffload = config.handleArpOffload;
+        mHandleNdOffload = config.handleNdOffload;
+        mHandleMdnsOffload = config.handleMdnsOffload;
+        mHandleIgmpOffload = config.handleIgmpOffload;
+        mHandleMldOffload = config.handleMldOffload;
+        mHandleIpv4PingOffload = config.handleIpv4PingOffload;
+        mHandleIpv6PingOffload = config.handleIpv6PingOffload;
         mDependencies = dependencies;
         mNetworkQuirkMetrics = networkQuirkMetrics;
         mIpClientRaInfoMetrics = dependencies.getIpClientRaInfoMetrics();
@@ -474,13 +596,38 @@
             Log.wtf(TAG, "Failed to start RaPacketReader");
         }
 
+        mMulticastReportMonitor = createMulticastReportMonitor();
+        if (mMulticastReportMonitor != null) {
+            mMulticastReportMonitor.start();
+        }
+
         // Listen for doze-mode transition changes to enable/disable the IPv6 multicast filter.
         mDependencies.addDeviceIdleReceiver(mDeviceIdleReceiver);
 
         mNsdManager = context.getSystemService(NsdManager.class);
-        if (shouldEnableMdnsOffload()) {
-            registerOffloadEngine();
+        if (enableOffloadEngineRegistration()) {
+            mApfMdnsOffloadEngine = new ApfMdnsOffloadEngine(mInterfaceParams.name, mHandler,
+                    mNsdManager,
+                    allRules -> {
+                        mOffloadRules.clear();
+                        mOffloadRules.addAll(allRules);
+                        installNewProgram();
+                    });
+            mApfMdnsOffloadEngine.registerOffloadEngine();
+        } else {
+            mApfMdnsOffloadEngine = null;
         }
+
+        mIPv4MulticastAddresses.addAll(
+                mDependencies.getIPv4MulticastAddresses(mInterfaceParams.name));
+        mIPv4McastAddrsExcludeAllHost.addAll(mIPv4MulticastAddresses);
+        mIPv4McastAddrsExcludeAllHost.remove((IPV4_ADDR_ALL_HOST_MULTICAST));
+
+        mIPv6MulticastAddresses.addAll(
+                mDependencies.getIPv6MulticastAddresses(mInterfaceParams.name));
+        mIPv6McastAddrsExcludeAllHost.addAll(mIPv6MulticastAddresses);
+        mIPv6McastAddrsExcludeAllHost.remove(IPV6_ADDR_ALL_NODES_MULTICAST);
+        mIPv6McastAddrsExcludeAllHost.remove(IPV6_ADDR_NODE_LOCAL_ALL_NODES_MULTICAST);
     }
 
     /**
@@ -512,6 +659,42 @@
         }
 
         /**
+         * Create a socket to read egress IGMPv2/v3 reports.
+         */
+        @Nullable
+        public FileDescriptor createEgressIgmpReportsReaderSocket(int ifIndex) {
+            FileDescriptor socket;
+            try {
+                socket = Os.socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, 0);
+                NetworkStackUtils.attachEgressIgmpReportFilter(socket);
+                Os.bind(socket, makePacketSocketAddress(ETH_P_ALL, ifIndex));
+            } catch (SocketException | ErrnoException e) {
+                Log.wtf(TAG, "Error starting filter", e);
+                return null;
+            }
+
+            return socket;
+        }
+
+        /**
+         * Create a socket to read egress IGMPv2/v3, MLDv1/v2 reports.
+         */
+        @Nullable
+        public FileDescriptor createEgressMulticastReportsReaderSocket(int ifIndex) {
+            FileDescriptor socket;
+            try {
+                socket = Os.socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, 0);
+                NetworkStackUtils.attachEgressMulticastReportFilter(socket);
+                Os.bind(socket, makePacketSocketAddress(ETH_P_ALL, ifIndex));
+            } catch (SocketException | ErrnoException e) {
+                Log.wtf(TAG, "Error starting filter", e);
+                return null;
+            }
+
+            return socket;
+        }
+
+        /**
          * Get elapsedRealtime.
          */
         public long elapsedRealtime() {
@@ -552,7 +735,7 @@
          * This method is designed to be overridden in test classes to collect created ApfFilter
          * instances.
          */
-        public void onApfFilterCreated(@NonNull AndroidPacketFilter apfFilter) {
+        public void onApfFilterCreated(@NonNull ApfFilter apfFilter) {
         }
 
         /**
@@ -603,9 +786,41 @@
         public int getNdTrafficClass(@NonNull String ifname) {
             return ProcfsParsingUtils.getNdTrafficClass(ifname);
         }
+
+        /**
+         * Returns the default TTL value for IPv4 packets from '/proc/sys/net/ipv4/ip_default_ttl'.
+         */
+        public int getIpv4DefaultTtl() {
+            return ProcfsParsingUtils.getIpv4DefaultTtl();
+        }
+
+        /**
+         * Returns the default HopLimit value for IPv6 packets.
+         */
+        public int getIpv6DefaultHopLimit(@NonNull String ifname) {
+            return ProcfsParsingUtils.getIpv6DefaultHopLimit(ifname);
+        }
+
+        /**
+         * Loads the existing IPv4 multicast addresses from the file
+         * `/proc/net/igmp`.
+         */
+        public List<Inet4Address> getIPv4MulticastAddresses(@NonNull String ifname) {
+            return ProcfsParsingUtils.getIPv4MulticastAddresses(ifname);
+        }
+
+        /**
+         * Loads the existing IPv6 multicast addresses from the file `/proc/net/igmp6`.
+         */
+        public List<Inet6Address> getIPv6MulticastAddresses(@NonNull String ifname) {
+            return ProcfsParsingUtils.getIpv6MulticastAddresses(ifname);
+        }
     }
 
-    @Override
+    public IApfController getApfController() {
+        return mApfController;
+    }
+
     public String setDataSnapshot(byte[] data) {
         mDataSnapshot = data;
         if (mIsRunning) {
@@ -614,6 +829,26 @@
         return mApfCounterTracker.getCounters().toString();
     }
 
+    private MulticastReportMonitor createMulticastReportMonitor() {
+        FileDescriptor socketFd = null;
+
+        // Check if MLD report monitor is enabled first, it includes the IGMP report monitor.
+        if (enableMldReportsMonitor()) {
+            socketFd =
+                mDependencies.createEgressMulticastReportsReaderSocket(mInterfaceParams.index);
+        } else if (enableIgmpReportsMonitor()) {
+            socketFd =
+                mDependencies.createEgressIgmpReportsReaderSocket(mInterfaceParams.index);
+        }
+
+        return socketFd != null ? new MulticastReportMonitor(
+                mHandler,
+                mInterfaceParams,
+                this::updateMulticastAddrs,
+                socketFd
+        ) : null;
+    }
+
     private void log(String s) {
         Log.d(TAG, "(" + mInterfaceParams.name + "): " + s);
     }
@@ -698,6 +933,7 @@
             this.min = min;
         }
 
+        @Override
         public String toString() {
             if (type == Type.LIFETIME) {
                 return String.format("%s: (%d, %d) %d %d", type, start, length, lifetime, min);
@@ -719,11 +955,11 @@
         // Router lifetime in packet
         private final int mRouterLifetime;
         // Minimum valid lifetime of PIOs in packet, Long.MAX_VALUE means not seen.
-        private long mMinPioValidLifetime = Long.MAX_VALUE;
+        private final long mMinPioValidLifetime;
         // Minimum route lifetime of RIOs in packet, Long.MAX_VALUE means not seen.
-        private long mMinRioRouteLifetime = Long.MAX_VALUE;
+        private final long mMinRioRouteLifetime;
         // Minimum lifetime of RDNSSs in packet, Long.MAX_VALUE means not seen.
-        private long mMinRdnssLifetime = Long.MAX_VALUE;
+        private final long mMinRdnssLifetime;
         // The time in seconds in which some of the information contained in this RA expires.
         private final int mExpirationTime;
         // When the packet was last captured, in seconds since Unix Epoch
@@ -809,6 +1045,7 @@
             sb.append("/").append(prefixLen).append(" ");
         }
 
+        @Override
         public String toString() {
             try {
                 StringBuffer sb = new StringBuffer();
@@ -897,9 +1134,15 @@
          * Adds packet sections for an RA option with a 4-byte lifetime 4 bytes into the option
          * @param optionLength the length of the option in bytes
          * @param min the minimum acceptable lifetime
+         * @param isRdnss true iff this is an RDNSS option
          */
-        private long add4ByteLifetimeOption(int optionLength, int min) {
-            addMatchSection(ICMP6_4_BYTE_LIFETIME_OFFSET);
+        private long add4ByteLifetimeOption(int optionLength, int min, boolean isRdnss) {
+            if (isRdnss) {
+                addMatchSection(ICMP6_4_BYTE_LIFETIME_OFFSET - 2);
+                addIgnoreSection(2);  // reserved, but observed non-zero
+            } else {
+                addMatchSection(ICMP6_4_BYTE_LIFETIME_OFFSET);
+            }
             final long lifetime = getUint32(mPacket, mPacket.position());
             addLifetimeSection(ICMP6_4_BYTE_LIFETIME_LEN, lifetime, min);
             addMatchSection(optionLength - ICMP6_4_BYTE_LIFETIME_OFFSET
@@ -907,34 +1150,6 @@
             return lifetime;
         }
 
-        /**
-         * Return the router lifetime of the RA
-         */
-        public int routerLifetime() {
-            return mRouterLifetime;
-        }
-
-        /**
-         * Return the minimum valid lifetime in PIOs
-         */
-        public long minPioValidLifetime() {
-            return mMinPioValidLifetime;
-        }
-
-        /**
-         * Return the minimum route lifetime in RIOs
-         */
-        public long minRioRouteLifetime() {
-            return mMinRioRouteLifetime;
-        }
-
-        /**
-         * Return the minimum lifetime in RDNSSs
-         */
-        public long minRdnssLifetime() {
-            return mMinRdnssLifetime;
-        }
-
         // Note that this parses RA and may throw InvalidRaException (from
         // Buffer.position(int) or due to an invalid-length option) or IndexOutOfBoundsException
         // (from ByteBuffer.get(int) ) if parsing encounters something non-compliant with
@@ -980,6 +1195,10 @@
             // Add remaining fields (reachable time and retransmission timer) to match section.
             addMatchUntil(ICMP6_RA_OPTION_OFFSET);
 
+            long minPioValidLifetime = Long.MAX_VALUE;
+            long minRioRouteLifetime = Long.MAX_VALUE;
+            long minRdnssLifetime = Long.MAX_VALUE;
+
             while (mPacket.hasRemaining()) {
                 final int position = mPacket.position();
                 final int optionType = getUint8(mPacket, position);
@@ -999,8 +1218,8 @@
                         lifetime = getUint32(mPacket, mPacket.position());
                         addLifetimeSection(ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN,
                                 lifetime, mAcceptRaMinLft);
-                        mMinPioValidLifetime = getMinForPositiveValue(
-                                mMinPioValidLifetime, lifetime);
+                        minPioValidLifetime = getMinForPositiveValue(
+                                minPioValidLifetime, lifetime);
                         if (lifetime == 0) mNumZeroLifetimeRas++;
 
                         // Parse preferred lifetime
@@ -1017,15 +1236,15 @@
                     // are processed with the same specialized add4ByteLifetimeOption:
                     case ICMP6_RDNSS_OPTION_TYPE:
                         mRdnssOptionOffsets.add(position);
-                        lifetime = add4ByteLifetimeOption(optionLength, mMinRdnssLifetimeSec);
-                        mMinRdnssLifetime = getMinForPositiveValue(mMinRdnssLifetime, lifetime);
+                        lifetime = add4ByteLifetimeOption(optionLength, mMinRdnssLifetimeSec, true);
+                        minRdnssLifetime = getMinForPositiveValue(minRdnssLifetime, lifetime);
                         if (lifetime == 0) mNumZeroLifetimeRas++;
                         break;
                     case ICMP6_ROUTE_INFO_OPTION_TYPE:
                         mRioOptionOffsets.add(position);
-                        lifetime = add4ByteLifetimeOption(optionLength, mAcceptRaMinLft);
-                        mMinRioRouteLifetime = getMinForPositiveValue(
-                                mMinRioRouteLifetime, lifetime);
+                        lifetime = add4ByteLifetimeOption(optionLength, mAcceptRaMinLft, false);
+                        minRioRouteLifetime = getMinForPositiveValue(
+                                minRioRouteLifetime, lifetime);
                         if (lifetime == 0) mNumZeroLifetimeRas++;
                         break;
                     case ICMP6_SOURCE_LL_ADDRESS_OPTION_TYPE:
@@ -1046,6 +1265,10 @@
                         break;
                 }
             }
+
+            mMinPioValidLifetime = minPioValidLifetime;
+            mMinRioRouteLifetime = minRioRouteLifetime;
+            mMinRdnssLifetime = minRdnssLifetime;
             mExpirationTime = getExpirationTime();
         }
 
@@ -1189,11 +1412,17 @@
             return Math.min(65535, filterLifetime);
         }
 
+        int getRaProgramLengthOverEstimate(int timeSeconds) throws IllegalInstructionException {
+            final ApfV4GeneratorBase<?> gen = createApfGenerator();
+            generateFilter(gen, timeSeconds);
+            return gen.programLengthOverEstimate() - gen.getBaseProgramSize();
+        }
+
         // Append a filter for this RA to {@code gen}. Jump to DROP_LABEL if it should be dropped.
         // Jump to the next filter if packet doesn't match this RA.
         void generateFilter(ApfV4GeneratorBase<?> gen, int timeSeconds)
                 throws IllegalInstructionException {
-            String nextFilterLabel = gen.getUniqueLabel();
+            short nextFilterLabel = gen.getUniqueLabel();
             // Skip if packet is not the right size
             gen.addLoadFromMemory(R0, MemorySlot.PACKET_SIZE);
             gen.addJumpIfR0NotEquals(mPacket.capacity(), nextFilterLabel);
@@ -1211,8 +1440,8 @@
                 } else {
                     switch (section.length) {
                         // length asserted to be either 2 or 4 on PacketSection construction
-                        case 2: gen.addLoad16(R0, section.start); break;
-                        case 4: gen.addLoad32(R0, section.start); break;
+                        case 2: gen.addLoad16intoR0(section.start); break;
+                        case 4: gen.addLoad32intoR0(section.start); break;
                     }
 
                     // WARNING: keep this in sync with matches()!
@@ -1258,7 +1487,7 @@
                         gen.addJumpIfR0Equals(0, nextFilterLabel);
                         gen.addJumpIfR0GreaterThan(section.lifetime, nextFilterLabel);
                     } else {
-                        final String continueLabel = gen.getUniqueLabel();
+                        final short continueLabel = gen.getUniqueLabel();
                         // Case 4a) otherwise
                         //
                         // if lft == 0                  -> PASS
@@ -1276,7 +1505,7 @@
                     }
                 }
             }
-            gen.addCountAndDrop(Counter.DROPPED_RA);
+            gen.addCountAndDrop(DROPPED_RA);
             gen.defineLabel(nextFilterLabel);
         }
     }
@@ -1321,7 +1550,7 @@
 
         NattKeepaliveResponse(final NattKeepalivePacketDataParcelable sentKeepalivePacket) {
             mPacket = new NattKeepaliveResponseData(sentKeepalivePacket);
-            mSrcDstAddr = concatArrays(mPacket.srcAddress, mPacket.dstAddress);
+            mSrcDstAddr = CollectionUtils.concatArrays(mPacket.srcAddress, mPacket.dstAddress);
             mPortFingerprint = generatePortFingerprint(mPacket.srcPort, mPacket.dstPort);
         }
 
@@ -1335,7 +1564,7 @@
 
         @Override
         void generateFilter(ApfV4GeneratorBase<?> gen) throws IllegalInstructionException {
-            final String nextFilterLabel = gen.getUniqueLabel();
+            final short nextFilterLabel = gen.getUniqueLabel();
 
             gen.addLoadImmediate(R0, ETH_HEADER_LEN + IPV4_SRC_ADDR_OFFSET);
             gen.addJumpIfBytesAtR0NotEqual(mSrcDstAddr, nextFilterLabel);
@@ -1345,7 +1574,7 @@
             gen.addLoadFromMemory(R0, MemorySlot.IPV4_HEADER_SIZE);
             gen.addAdd(UDP_HEADER_LEN);
             gen.addSwap();
-            gen.addLoad16(R0, IPV4_TOTAL_LENGTH_OFFSET);
+            gen.addLoad16intoR0(IPV4_TOTAL_LENGTH_OFFSET);
             gen.addNeg(R1);
             gen.addAddR1ToR0();
             gen.addJumpIfR0NotEquals(1, nextFilterLabel);
@@ -1359,10 +1588,11 @@
             gen.addAdd(UDP_HEADER_LEN);
             gen.addJumpIfBytesAtR0NotEqual(mPayload, nextFilterLabel);
 
-            gen.addCountAndDrop(Counter.DROPPED_IPV4_NATT_KEEPALIVE);
+            gen.addCountAndDrop(DROPPED_IPV4_NATT_KEEPALIVE);
             gen.defineLabel(nextFilterLabel);
         }
 
+        @Override
         public String toString() {
             try {
                 return String.format("%s -> %s",
@@ -1418,6 +1648,7 @@
             return fp.array();
         }
 
+        @Override
         public String toString() {
             try {
                 return String.format("%s -> %s , seq=%d, ack=%d",
@@ -1445,12 +1676,13 @@
             this(new TcpKeepaliveAckData(sentKeepalivePacket));
         }
         TcpKeepaliveAckV4(final TcpKeepaliveAckData packet) {
-            super(packet, concatArrays(packet.srcAddress, packet.dstAddress) /* srcDstAddr */);
+            super(packet, CollectionUtils.concatArrays(packet.srcAddress,
+                    packet.dstAddress) /* srcDstAddr */);
         }
 
         @Override
         void generateFilter(ApfV4GeneratorBase<?> gen) throws IllegalInstructionException {
-            final String nextFilterLabel = gen.getUniqueLabel();
+            final short nextFilterLabel = gen.getUniqueLabel();
 
             gen.addLoadImmediate(R0, ETH_HEADER_LEN + IPV4_SRC_ADDR_OFFSET);
             gen.addJumpIfBytesAtR0NotEqual(mSrcDstAddr, nextFilterLabel);
@@ -1460,15 +1692,18 @@
             // Load the IP header size into R1
             gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE);
             // Load the TCP header size into R0 (it's indexed by R1)
-            gen.addLoad8Indexed(R0, ETH_HEADER_LEN + TCP_HEADER_SIZE_OFFSET);
-            // Size offset is in the top nibble, but it must be multiplied by 4, and the two
-            // top bits of the low nibble are guaranteed to be zeroes. Right-shift R0 by 2.
+            gen.addLoad8R1IndexedIntoR0(ETH_HEADER_LEN + TCP_HEADER_SIZE_OFFSET);
+            // Size offset is in the top nibble, bottom nibble is reserved,
+            // but not necessarily zero.  Thus we need to >> 4 then << 2,
+            // achieve this by >> 2 and masking with 0b00111100.
             gen.addRightShift(2);
+            gen.addAnd(0x3C);
             // R0 += R1 -> R0 contains TCP + IP headers length
             gen.addAddR1ToR0();
             // Load IPv4 total length
-            gen.addLoad16(R1, IPV4_TOTAL_LENGTH_OFFSET);
-            gen.addNeg(R0);
+            gen.addSwap();
+            gen.addLoad16intoR0(IPV4_TOTAL_LENGTH_OFFSET);
+            gen.addNeg(R1);
             gen.addAddR1ToR0();
             gen.addJumpIfR0NotEquals(0, nextFilterLabel);
             // Add IPv4 header length
@@ -1477,7 +1712,7 @@
             gen.addAddR1ToR0();
             gen.addJumpIfBytesAtR0NotEqual(mPortSeqAckFingerprint, nextFilterLabel);
 
-            gen.addCountAndDrop(Counter.DROPPED_IPV4_KEEPALIVE_ACK);
+            gen.addCountAndDrop(DROPPED_IPV4_KEEPALIVE_ACK);
             gen.defineLabel(nextFilterLabel);
         }
     }
@@ -1487,7 +1722,8 @@
             this(new TcpKeepaliveAckData(sentKeepalivePacket));
         }
         TcpKeepaliveAckV6(final TcpKeepaliveAckData packet) {
-            super(packet, concatArrays(packet.srcAddress, packet.dstAddress) /* srcDstAddr */);
+            super(packet, CollectionUtils.concatArrays(packet.srcAddress,
+                    packet.dstAddress) /* srcDstAddr */);
         }
 
         @Override
@@ -1500,9 +1736,8 @@
     private static final int MAX_RAS = 10;
 
     private final ArrayList<Ra> mRas = new ArrayList<>();
+    private int mNumFilteredRas = 0;
     private final SparseArray<KeepalivePacket> mKeepalivePackets = new SparseArray<>();
-    // TODO: change the mMdnsAllowList to proper type for APFv6 based mDNS offload
-    private final List<String[]> mMdnsAllowList = new ArrayList<>();
 
     // We don't want to filter an RA for it's whole lifetime as it'll be expired by the time we ever
     // see a refresh.  Using half the lifetime might be a good idea except for the fact that
@@ -1534,13 +1769,6 @@
     // The maximum number of distinct RAs
     private int mMaxDistinctRas = 0;
 
-    private ApfV6Generator tryToConvertToApfV6Generator(ApfV4GeneratorBase<?> gen) {
-        if (gen instanceof ApfV6Generator) {
-            return (ApfV6Generator) gen;
-        }
-        return null;
-    }
-
     /**
      * Generate filter code to process ARP packets. Execution of this code ends in either the
      * DROP_LABEL or PASS_LABEL and does not fall off the end.
@@ -1579,45 +1807,44 @@
 
         // For IPv6 only network, drop all ARP packet.
         if (mHasClat) {
-            gen.addCountAndDrop(Counter.DROPPED_ARP_V6_ONLY);
+            gen.addCountAndDrop(DROPPED_ARP_V6_ONLY);
             return;
         }
 
         // Drop if not ARP IPv4.
-        gen.addLoadImmediate(R0, ARP_HEADER_OFFSET);
-        gen.addCountAndDropIfBytesAtR0NotEqual(ARP_IPV4_HEADER, Counter.DROPPED_ARP_NON_IPV4);
+        gen.addCountAndDropIfBytesAtOffsetNotEqual(ARP_HEADER_OFFSET, ARP_IPV4_HEADER,
+                DROPPED_ARP_NON_IPV4);
 
-        final String checkArpRequest = gen.getUniqueLabel();
+        final short checkArpRequest = gen.getUniqueLabel();
 
-        gen.addLoad16(R0, ARP_OPCODE_OFFSET);
+        gen.addLoad16intoR0(ARP_OPCODE_OFFSET);
         gen.addJumpIfR0Equals(ARP_OPCODE_REQUEST, checkArpRequest); // Skip to arp request check.
         // Drop if unknown ARP opcode.
-        gen.addCountAndDropIfR0NotEquals(ARP_OPCODE_REPLY, Counter.DROPPED_ARP_UNKNOWN);
+        gen.addCountAndDropIfR0NotEquals(ARP_OPCODE_REPLY, DROPPED_ARP_UNKNOWN);
 
         /*----------  Handle ARP Replies. ----------*/
 
         // Drop if ARP reply source IP is 0.0.0.0
-        gen.addLoad32(R0, ARP_SOURCE_IP_ADDRESS_OFFSET);
-        gen.addCountAndDropIfR0Equals(IPV4_ANY_HOST_ADDRESS, Counter.DROPPED_ARP_REPLY_SPA_NO_HOST);
+        gen.addLoad32intoR0(ARP_SOURCE_IP_ADDRESS_OFFSET);
+        gen.addCountAndDropIfR0Equals(IPV4_ANY_HOST_ADDRESS, DROPPED_ARP_REPLY_SPA_NO_HOST);
 
         // Pass if non-broadcast reply.
         // This also accepts multicast arp, but we assume those don't exist.
-        gen.addLoadImmediate(R0, ETH_DEST_ADDR_OFFSET);
-        gen.addCountAndPassIfBytesAtR0NotEqual(ETHER_BROADCAST, Counter.PASSED_ARP_UNICAST_REPLY);
+        gen.addCountAndPassIfBytesAtOffsetNotEqual(ETH_DEST_ADDR_OFFSET, ETHER_BROADCAST,
+                PASSED_ARP_UNICAST_REPLY);
 
         // It is a broadcast reply.
         if (mIPv4Address == null) {
             // When there is no IPv4 address, drop GARP replies (b/29404209).
-            gen.addLoad32(R0, ARP_TARGET_IP_ADDRESS_OFFSET);
-            gen.addCountAndDropIfR0Equals(IPV4_ANY_HOST_ADDRESS, Counter.DROPPED_GARP_REPLY);
+            gen.addLoad32intoR0(ARP_TARGET_IP_ADDRESS_OFFSET);
+            gen.addCountAndDropIfR0Equals(IPV4_ANY_HOST_ADDRESS, DROPPED_GARP_REPLY);
         } else {
             // When there is an IPv4 address, drop broadcast replies with a different target IPv4
             // address.
-            gen.addLoad32(R0, ARP_TARGET_IP_ADDRESS_OFFSET);
-            gen.addCountAndDropIfR0NotEquals(bytesToBEInt(mIPv4Address),
-                    Counter.DROPPED_ARP_OTHER_HOST);
+            gen.addLoad32intoR0(ARP_TARGET_IP_ADDRESS_OFFSET);
+            gen.addCountAndDropIfR0NotEquals(bytesToBEInt(mIPv4Address), DROPPED_ARP_OTHER_HOST);
         }
-        gen.addCountAndPass(Counter.PASSED_ARP_BROADCAST_REPLY);
+        gen.addCountAndPass(PASSED_ARP_BROADCAST_REPLY);
 
         /*----------  Handle ARP Requests. ----------*/
 
@@ -1625,12 +1852,11 @@
         if (mIPv4Address != null) {
             // When there is an IPv4 address, drop unicast/broadcast requests with a different
             // target IPv4 address.
-            gen.addLoad32(R0, ARP_TARGET_IP_ADDRESS_OFFSET);
-            gen.addCountAndDropIfR0NotEquals(bytesToBEInt(mIPv4Address),
-                    Counter.DROPPED_ARP_OTHER_HOST);
+            gen.addLoad32intoR0(ARP_TARGET_IP_ADDRESS_OFFSET);
+            gen.addCountAndDropIfR0NotEquals(bytesToBEInt(mIPv4Address), DROPPED_ARP_OTHER_HOST);
 
-            ApfV6Generator v6Gen = tryToConvertToApfV6Generator(gen);
-            if (v6Gen != null && mShouldHandleArpOffload) {
+            if (enableArpOffload()) {
+                ApfV6GeneratorBase<?> v6Gen = (ApfV6GeneratorBase<?>) gen;
                 // Ethernet requires that all packets be at least 60 bytes long
                 v6Gen.addAllocate(60)
                         .addPacketCopy(ETHER_SRC_ADDR_OFFSET, ETHER_ADDR_LEN)
@@ -1644,12 +1870,156 @@
                         .addAdd(18)
                         .addStoreToMemory(MemorySlot.TX_BUFFER_OUTPUT_POINTER, R0)
                         .addTransmitWithoutChecksum()
-                        .addCountAndDrop(Counter.DROPPED_ARP_REQUEST_REPLIED);
+                        .addCountAndDrop(DROPPED_ARP_REQUEST_REPLIED);
             }
         }
         // If we're not clat, and we don't have an ipv4 address, allow all ARP request to avoid
         // racing against DHCP.
-        gen.addCountAndPass(Counter.PASSED_ARP_REQUEST);
+        gen.addCountAndPass(PASSED_ARP_REQUEST);
+    }
+
+    /**
+     * Generate filter code to reply and drop unicast ICMPv4 echo request.
+     * <p>
+     * On entry, we know it is IPv4 ethertype, but don't know anything else.
+     * R0/R1 have nothing useful in them, and can be clobbered.
+     */
+    private void generateUnicastIpv4PingOffload(ApfV6GeneratorBase<?> gen)
+            throws IllegalInstructionException {
+
+        final short skipIpv4PingFilter = gen.getUniqueLabel();
+        // Check 1) it's not a fragment. 2) it's ICMP.
+        // If condition not match then skip the ping filter logic
+        gen.addJumpIfNotUnfragmentedIPv4Protocol(IPPROTO_ICMP, skipIpv4PingFilter);
+
+        // Only offload unicast Ipv4 ping request for now.
+        // While we could potentially support offloading multicast and broadcast ping requests in
+        // the future, such packets will likely be dropped by multicast filters.
+        // Since the device may have packet forwarding enabled, APF needs to pass any received
+        // unicast IPv4 ping not destined for the device's IP address to the kernel.
+        gen.addJumpIfBytesAtOffsetNotEqual(
+                ETH_DEST_ADDR_OFFSET, mHardwareAddress, skipIpv4PingFilter)
+                .addLoadImmediate(R0, IPV4_DEST_ADDR_OFFSET)
+                .addJumpIfBytesAtR0NotEqual(mIPv4Address, skipIpv4PingFilter);
+
+        // Ignore ping packets with IPv4 options (header size != 20) as they are rare.
+        // Pass them to the kernel to save bytecode space.
+        gen.addLoadFromMemory(R0, MemorySlot.IPV4_HEADER_SIZE)
+                .addJumpIfR0NotEquals(IPV4_HEADER_MIN_LEN, skipIpv4PingFilter);
+
+        // We need to check if the packet is sufficiently large to be a valid ICMP packet.
+        gen.addLoadFromMemory(R0, MemorySlot.PACKET_SIZE)
+                .addCountAndDropIfR0LessThan(
+                        ETHER_HEADER_LEN + IPV4_HEADER_MIN_LEN + ICMP_HEADER_LEN,
+                        DROPPED_IPV4_ICMP_INVALID);
+
+        // If it is not a ICMP echo request, then skip.
+        gen.addLoad8intoR0(ICMP4_TYPE_NO_OPTIONS_OFFSET)
+                .addJumpIfR0NotEquals(ICMP_ECHO, skipIpv4PingFilter);
+
+        final int defaultTtl = mDependencies.getIpv4DefaultTtl();
+        // Construct the ICMP echo reply packet.
+        gen.addLoadFromMemory(R0, MemorySlot.PACKET_SIZE)
+                .addAllocateR0()
+                .addPacketCopy(ETHER_SRC_ADDR_OFFSET, ETHER_ADDR_LEN) // Dst MAC address
+                .addDataCopy(mHardwareAddress) // Src MAC address
+                // Reuse the following fields from the input packet:
+                // 2 bytes: EtherType
+                // 4 bytes: version, IHL, TOS, total length
+                // 4 bytes: identification, flags, fragment offset
+                .addPacketCopy(ETH_ETHERTYPE_OFFSET, 10)
+                // Ttl: default ttl, Protocol: IPPROTO_ICMP, checksum: 0
+                .addWrite32((defaultTtl << 24) | (IPPROTO_ICMP << 16))
+                .addWrite32(mIPv4Address) // Src ip
+                .addPacketCopy(IPV4_SRC_ADDR_OFFSET, IPV4_ADDR_LEN) // Dst ip
+                .addWrite32((ICMP_ECHOREPLY << 24)) // Type: echo reply, code: 0, checksum: 0
+                // Copy identifier, sequence number and ping payload
+                .addSub(ICMP4_CONTENT_NO_OPTIONS_OFFSET)
+                .addLoadImmediate(R1, ICMP4_CONTENT_NO_OPTIONS_OFFSET)
+                .addSwap() // Swaps R0 and R1, so they're the offset and length.
+                .addPacketCopyFromR0LenR1()
+                .addTransmitL4(
+                        ETHER_HEADER_LEN, // ip_ofs
+                        ICMP4_CHECKSUM_NO_OPTIONS_OFFSET, // csum_ofs
+                        ICMP4_TYPE_NO_OPTIONS_OFFSET, // csum_start
+                        0, // partial_sum
+                        false // udp
+                )
+                .addCountAndDrop(DROPPED_IPV4_PING_REQUEST_REPLIED);
+
+        gen.defineLabel(skipIpv4PingFilter);
+    }
+
+    /**
+     * Generates filter code to handle IPv4 mDNS packets.
+     * <p>
+     * On entry, this filter knows it is processing an IPv4 packet. It will then process all IPv4
+     * mDNS packets, either passing or dropping them. IPv4 non-mDNS packets are skipped.
+     *
+     * @param gen the APF generator to generate the filter code
+     * @param labelCheckMdnsQueryPayload the label to jump to for checking the mDNS query payload
+     */
+    private void generateIPv4MdnsFilter(ApfV6GeneratorBase<?> gen,
+            short labelCheckMdnsQueryPayload)
+            throws IllegalInstructionException {
+        final short skipMdnsFilter = gen.getUniqueLabel();
+
+        // If the packet is too short to be a valid IPv4 mDNS packet, the filter is skipped.
+        // For APF performance reasons, we check udp destination port before confirming it is
+        // non-fragmented IPv4 udp packet. We proceed only if the destination port is 5353 (mDNS).
+        // Otherwise, skip filtering.
+        gen.addLoadFromMemory(R0, MemorySlot.PACKET_SIZE)
+                .addJumpIfR0LessThan(
+                        ETH_HEADER_LEN + IPV4_HEADER_MIN_LEN + UDP_HEADER_LEN + DNS_HEADER_LEN,
+                        skipMdnsFilter)
+                .addLoad16intoR0(IPV4_UDP_DESTINATION_PORT_NO_OPTIONS_OFFSET)
+                .addJumpIfR0NotEquals(MDNS_PORT, skipMdnsFilter);
+
+        // If the destination MAC address is not 01:00:5e:00:00:fb (the mDNS multicast MAC
+        // address for IPv4 mDNS packet) or the device's MAC address, skip filtering.
+        // We need to check both the mDNS multicast MAC address and the device's MAC address
+        // because multicast to unicast conversion might have occurred.
+        gen.addJumpIfBytesAtOffsetEqualsNoneOf(
+                ETH_DEST_ADDR_OFFSET,
+                List.of(mHardwareAddress, ETH_MULTICAST_MDNS_V4_MAC_ADDRESS),
+                skipMdnsFilter
+        );
+
+        // Ignore packets with IPv4 options (header size not equal to 20) as they are rare.
+        gen.addLoadFromMemory(R0, MemorySlot.IPV4_HEADER_SIZE)
+                .addJumpIfR0NotEquals(IPV4_HEADER_MIN_LEN, skipMdnsFilter);
+
+        // Skip filtering if the packet is not a non-fragmented IPv4 UDP packet.
+        gen.addJumpIfNotUnfragmentedIPv4Protocol(IPPROTO_UDP, skipMdnsFilter);
+
+        // Skip filtering if the IPv4 destination address is not 224.0.0.251 (the mDNS multicast
+        // address).
+        // Some devices can use unicast queries for mDNS to improve performance and reliability.
+        // These packets are not currently offloaded and will be passed by APF and handled
+        // by NsdService.
+        gen.addLoad32intoR0(IPV4_DEST_ADDR_OFFSET)
+                .addJumpIfR0NotEquals(MDNS_IPV4_ADDR_IN_LONG, skipMdnsFilter);
+
+        // We now know that the packet is an mDNS packet,
+        // i.e., a non-fragmented IPv4 UDP packet destined for port 5353 with the expected
+        // destination MAC and IP addresses.
+
+        // If the packet contains questions, check the query payload. Otherwise, check the
+        // reply payload.
+        gen.addLoad16intoR0(IPV4_DNS_QDCOUNT_NO_OPTIONS_OFFSET)
+                // Set the UDP payload offset in R1 before potentially jumping to the payload
+                // check logic.
+                .addLoadImmediate(R1, IPV4_UDP_PAYLOAD_NO_OPTIONS_OFFSET)
+                .addJumpIfR0NotEquals(0, labelCheckMdnsQueryPayload);
+
+        // TODO: check the reply payload.
+        if (mMulticastFilter) {
+            gen.addCountAndDrop(DROPPED_MDNS);
+        } else {
+            gen.addCountAndPass(PASSED_MDNS);
+        }
+
+        gen.defineLabel(skipMdnsFilter);
     }
 
     /**
@@ -1657,8 +2027,11 @@
      * DROP_LABEL or PASS_LABEL and does not fall off the end.
      * Preconditions:
      *  - Packet being filtered is IPv4
+     *
+     * @param gen the APF generator to generate the filter code
+     * @param labelCheckMdnsQueryPayload the label to jump to for checking the mDNS query payload
      */
-    private void generateIPv4Filter(ApfV4GeneratorBase<?> gen)
+    private void generateIPv4Filter(ApfV4GeneratorBase<?> gen, short labelCheckMdnsQueryPayload)
             throws IllegalInstructionException {
         // Here's a basic summary of what the IPv4 filter program does:
         //
@@ -1669,6 +2042,47 @@
         //     pass
         //   else
         //     drop
+        //
+        // (APFv6+ specific logic)
+        // if it's mDNS:
+        //   if it's a query:
+        //     if mNumOfMdnsRuleToOffload == -1:
+        //       pass
+        //     if the query matches one of the offload rules from idx [0, mNumOfMdnsRuleToOffload):
+        //       transmit mDNS reply and drop
+        //     else if query matches one of the rest of the offload rules:
+        //       pass
+        //     else if filtering multicast (i.e. multicast lock not held):
+        //       drop
+        //     else
+        //       pass
+        //   else:
+        //     if filtering multicast (i.e. multicast lock not held):
+        //       drop
+        //     else
+        //       pass
+        //
+        // (APFv6+ specific logic)
+        // if it's IGMP:
+        //   if payload length is invalid (less than 8 or equal to 9, 10, 11):
+        //     drop
+        //   if the packet is an IGMP report:
+        //     drop
+        //   if the packet is not an IGMP query:
+        //     drop
+        //   if the group_addr is not 0.0.0.0, then it is group specific query:
+        //     pass
+        //   ===== handle IGMPv1/v2/v3 general query =====
+        //   if the IPv4 dst addr is not 224.0.0.1:
+        //     drop
+        //   if the packet length >= 12, then it is IGMPv3:
+        //     transmit IGMPv3 report and drop
+        //   else if the packet length == 8, then it is either IGMPv1 or IGMPv2:
+        //     if the max_res_code == 0, then it is IGMPv1:
+        //       pass
+        //     else it is IGMPv2:
+        //       transmit IGMPv2 reports (one report per group) and drop
+        //
         // if filtering multicast (i.e. multicast lock not held):
         //   if it's DHCP destined to our MAC:
         //     pass
@@ -1678,14 +2092,19 @@
         //     drop
         //   if it's IPv4 broadcast:
         //     drop
+        //
         // if keepalive ack
         //   drop
+        //
+        // (APFv6+ specific logic) if it's unicast IPv4 ICMP echo request to our host:
+        //    transmit echo reply and drop
+        //
         // pass
 
         if (mHasClat) {
             // Check 1) it's not a fragment. 2) it's UDP.
             // Load 16 bit frag flags/offset field, 8 bit ttl, 8 bit protocol
-            gen.addLoad32(R0, IPV4_FRAGMENT_OFFSET_OFFSET);
+            gen.addLoad32intoR0(IPV4_FRAGMENT_OFFSET_OFFSET);
             // Mask out the reserved and don't fragment bits, plus the TTL field.
             // Because:
             //   IPV4_FRAGMENT_OFFSET_MASK = 0x1fff
@@ -1694,52 +2113,58 @@
             // We want the more flag bit and offset to be 0 (ie. not a fragment),
             // so after this masking we end up with just the ip protocol (hopefully UDP).
             gen.addAnd((IPV4_FRAGMENT_MORE_FRAGS_MASK | IPV4_FRAGMENT_OFFSET_MASK) << 16 | 0xFF);
-            gen.addCountAndDropIfR0NotEquals(IPPROTO_UDP, Counter.DROPPED_IPV4_NON_DHCP4);
+            gen.addCountAndDropIfR0NotEquals(IPPROTO_UDP, DROPPED_IPV4_NON_DHCP4);
             // Check it's addressed to DHCP client port.
             gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE);
-            gen.addLoad32Indexed(R0, TCP_UDP_SOURCE_PORT_OFFSET);
+            gen.addLoad32R1IndexedIntoR0(TCP_UDP_SOURCE_PORT_OFFSET);
             gen.addCountAndDropIfR0NotEquals(DHCP_SERVER_PORT << 16 | DHCP_CLIENT_PORT,
-                    Counter.DROPPED_IPV4_NON_DHCP4);
-            gen.addCountAndPass(Counter.PASSED_IPV4_FROM_DHCPV4_SERVER);
+                    DROPPED_IPV4_NON_DHCP4);
+            gen.addCountAndPass(PASSED_IPV4_FROM_DHCPV4_SERVER);
             return;
         }
 
+        if (enableMdns4Offload()) {
+            generateIPv4MdnsFilter((ApfV6GeneratorBase<?>) gen, labelCheckMdnsQueryPayload);
+        }
+
+        if (enableIgmpOffload()) {
+            generateIgmpFilter((ApfV6GeneratorBase<?>) gen);
+        }
+
         if (mMulticastFilter) {
-            final String skipDhcpv4Filter = gen.getUniqueLabel();
+            final short skipDhcpv4Filter = gen.getUniqueLabel();
 
             // Pass DHCP addressed to us.
             // Check 1) it's not a fragment. 2) it's UDP.
-            // Load 16 bit frag flags/offset field, 8 bit ttl, 8 bit protocol
-            gen.addLoad32(R0, IPV4_FRAGMENT_OFFSET_OFFSET);
-            // see above for explanation of this constant
-            gen.addAnd((IPV4_FRAGMENT_MORE_FRAGS_MASK | IPV4_FRAGMENT_OFFSET_MASK) << 16 | 0xFF);
-            gen.addJumpIfR0NotEquals(IPPROTO_UDP, skipDhcpv4Filter);
+            gen.addJumpIfNotUnfragmentedIPv4Protocol(IPPROTO_UDP, skipDhcpv4Filter);
             // Check it's addressed to DHCP client port.
             gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE);
-            gen.addLoad16Indexed(R0, TCP_UDP_DESTINATION_PORT_OFFSET);
+            gen.addLoad16R1IndexedIntoR0(TCP_UDP_DESTINATION_PORT_OFFSET);
             gen.addJumpIfR0NotEquals(DHCP_CLIENT_PORT, skipDhcpv4Filter);
             // Check it's DHCP to our MAC address.
             gen.addLoadImmediate(R0, DHCP_CLIENT_MAC_OFFSET);
             // NOTE: Relies on R1 containing IPv4 header offset.
             gen.addAddR1ToR0();
             gen.addJumpIfBytesAtR0NotEqual(mHardwareAddress, skipDhcpv4Filter);
-            gen.addCountAndPass(Counter.PASSED_DHCP);
+            gen.addCountAndPass(PASSED_DHCP);
 
             // Drop all multicasts/broadcasts.
             gen.defineLabel(skipDhcpv4Filter);
 
             // If IPv4 destination address is in multicast range, drop.
-            gen.addLoad8(R0, IPV4_DEST_ADDR_OFFSET);
-            gen.addAnd(0xf0);
-            gen.addCountAndDropIfR0Equals(0xe0, Counter.DROPPED_IPV4_MULTICAST);
+            gen.addLoad8intoR0(IPV4_DEST_ADDR_OFFSET);
+            // we just loaded a byte, so top 24 bits are zero, thus and'ing
+            // with either one of 0xF0 and 0xFFFFFFF0 accomplishes the same thing,
+            // we thus choose the one which encodes shorter
+            gen.addAnd((gen instanceof ApfV4Generator) ? 0xF0 : 0xFFFFFFF0);
+            gen.addCountAndDropIfR0Equals(0xe0, DROPPED_IPV4_MULTICAST);
 
             // If IPv4 broadcast packet, drop regardless of L2 (b/30231088).
-            gen.addLoad32(R0, IPV4_DEST_ADDR_OFFSET);
-            gen.addCountAndDropIfR0Equals(IPV4_BROADCAST_ADDRESS,
-                    Counter.DROPPED_IPV4_BROADCAST_ADDR);
+            gen.addLoad32intoR0(IPV4_DEST_ADDR_OFFSET);
+            gen.addCountAndDropIfR0Equals(IPV4_BROADCAST_ADDRESS, DROPPED_IPV4_BROADCAST_ADDR);
             if (mIPv4Address != null && mIPv4PrefixLength < 31) {
                 int broadcastAddr = ipv4BroadcastAddress(mIPv4Address, mIPv4PrefixLength);
-                gen.addCountAndDropIfR0Equals(broadcastAddr, Counter.DROPPED_IPV4_BROADCAST_NET);
+                gen.addCountAndDropIfR0Equals(broadcastAddr, DROPPED_IPV4_BROADCAST_NET);
             }
         }
 
@@ -1752,21 +2177,25 @@
         // If TCP unicast on port 7, drop
         generateV4TcpPort7Filter(gen);
 
+        if (enableIpv4PingOffload()) {
+            generateUnicastIpv4PingOffload((ApfV6GeneratorBase<?>) gen);
+        }
+
         if (mMulticastFilter) {
             // Otherwise, this is an IPv4 unicast, pass
             // If L2 broadcast packet, drop.
             // TODO: can we invert this condition to fall through to the common pass case below?
-            gen.addLoadImmediate(R0, ETH_DEST_ADDR_OFFSET);
-            gen.addCountAndPassIfBytesAtR0NotEqual(ETHER_BROADCAST, Counter.PASSED_IPV4_UNICAST);
-            gen.addCountAndDrop(Counter.DROPPED_IPV4_L2_BROADCAST);
+            gen.addCountAndPassIfBytesAtOffsetNotEqual(ETH_DEST_ADDR_OFFSET, ETHER_BROADCAST,
+                    PASSED_IPV4_UNICAST);
+            gen.addCountAndDrop(DROPPED_IPV4_L2_BROADCAST);
         }
 
         // Otherwise, pass
-        gen.addCountAndPass(Counter.PASSED_IPV4);
+        gen.addCountAndPass(PASSED_IPV4);
     }
 
     private void generateKeepaliveFilters(ApfV4GeneratorBase<?> gen, Class<?> filterType, int proto,
-            int offset, String label) throws IllegalInstructionException {
+            int offset, short label) throws IllegalInstructionException {
         final boolean haveKeepaliveResponses = CollectionUtils.any(mKeepalivePackets,
                 filterType::isInstance);
 
@@ -1774,7 +2203,7 @@
         if (!haveKeepaliveResponses) return;
 
         // If not the right proto, skip keepalive filters
-        gen.addLoad8(R0, offset);
+        gen.addLoad8intoR0(offset);
         gen.addJumpIfR0NotEquals(proto, label);
 
         // Drop Keepalive responses
@@ -1883,7 +2312,7 @@
         );
     }
 
-    private void generateNsFilter(ApfV6Generator v6Gen)
+    private void generateNsFilter(ApfV6GeneratorBase<?> v6Gen)
             throws IllegalInstructionException {
         final List<byte[]> allIPv6Addrs = getIpv6Addresses(
                 true /* includeNonTentative */,
@@ -1892,7 +2321,7 @@
         if (allIPv6Addrs.isEmpty()) {
             // If there is no IPv6 link local address, allow all NS packets to avoid racing
             // against RS.
-            v6Gen.addCountAndPass(PASSED_IPV6_NS_NO_ADDRESS);
+            v6Gen.addCountAndPass(PASSED_IPV6_ICMP);
             return;
         }
 
@@ -1900,32 +2329,33 @@
         // used by processes other than clatd. This is because APF cannot reliably detect signal
         // on when IPV6_{JOIN,LEAVE}_ANYCAST is triggered.
         final List<byte[]> allMACs = getKnownMacAddresses();
-        v6Gen.addLoadImmediate(R0, ETH_DEST_ADDR_OFFSET)
-                .addCountAndDropIfBytesAtR0EqualsNoneOf(allMACs, DROPPED_IPV6_NS_OTHER_HOST);
+        v6Gen.addCountAndDropIfBytesAtOffsetEqualsNoneOf(ETH_DEST_ADDR_OFFSET, allMACs,
+                DROPPED_IPV6_NS_OTHER_HOST);
 
         // Dst IPv6 address check:
         final List<byte[]> allSuffixes = getSolicitedNodeMcastAddressSuffix(allIPv6Addrs);
-        final String notIpV6SolicitedNodeMcast = v6Gen.getUniqueLabel();
-        final String endOfIpV6DstCheck = v6Gen.getUniqueLabel();
-        v6Gen.addLoadImmediate(R0, IPV6_DEST_ADDR_OFFSET)
-                .addJumpIfBytesAtR0NotEqual(IPV6_SOLICITED_NODES_PREFIX, notIpV6SolicitedNodeMcast)
-                .addAdd(13)
+        final short notIpV6SolicitedNodeMcast = v6Gen.getUniqueLabel();
+        final short endOfIpV6DstCheck = v6Gen.getUniqueLabel();
+        v6Gen.addJumpIfBytesAtOffsetNotEqual(
+                IPV6_DEST_ADDR_OFFSET, IPV6_SOLICITED_NODES_PREFIX, notIpV6SolicitedNodeMcast)
+                .addLoadImmediate(R0, IPV6_DEST_ADDR_OFFSET + 13)
                 .addCountAndDropIfBytesAtR0EqualsNoneOf(allSuffixes, DROPPED_IPV6_NS_OTHER_HOST)
                 .addJump(endOfIpV6DstCheck)
                 .defineLabel(notIpV6SolicitedNodeMcast)
-                .addCountAndDropIfBytesAtR0EqualsNoneOf(allIPv6Addrs, DROPPED_IPV6_NS_OTHER_HOST)
+                .addCountAndDropIfBytesAtOffsetEqualsNoneOf(
+                        IPV6_DEST_ADDR_OFFSET, allIPv6Addrs, DROPPED_IPV6_NS_OTHER_HOST)
                 .defineLabel(endOfIpV6DstCheck);
 
         // Hop limit not 255, NS requires hop limit to be 255 -> drop
-        v6Gen.addLoad8(R0, IPV6_HOP_LIMIT_OFFSET)
+        v6Gen.addLoad8intoR0(IPV6_HOP_LIMIT_OFFSET)
                 .addCountAndDropIfR0NotEquals(255, DROPPED_IPV6_NS_INVALID);
 
         // payload length < 24 (8 bytes ICMP6 header + 16 bytes target address) -> drop
-        v6Gen.addLoad16(R0, IPV6_PAYLOAD_LEN_OFFSET)
+        v6Gen.addLoad16intoR0(IPV6_PAYLOAD_LEN_OFFSET)
                 .addCountAndDropIfR0LessThan(24, DROPPED_IPV6_NS_INVALID);
 
         // ICMPv6 code not 0 -> drop
-        v6Gen.addLoad8(R0, ICMP6_CODE_OFFSET)
+        v6Gen.addLoad8intoR0(ICMP6_CODE_OFFSET)
                 .addCountAndDropIfR0NotEquals(0, DROPPED_IPV6_NS_INVALID);
 
         // target address (ICMPv6 NS payload)
@@ -1936,10 +2366,10 @@
                 true, /* includeTentative */
                 false /* includeAnycast */
         );
-        v6Gen.addLoadImmediate(R0, ICMP6_NS_TARGET_IP_OFFSET);
+
         if (!tentativeIPv6Addrs.isEmpty()) {
-            v6Gen.addCountAndPassIfBytesAtR0EqualsAnyOf(
-                    tentativeIPv6Addrs, PASSED_IPV6_NS_TENTATIVE);
+            v6Gen.addCountAndPassIfBytesAtOffsetEqualsAnyOf(
+                    ICMP6_NS_TARGET_IP_OFFSET, tentativeIPv6Addrs, PASSED_IPV6_ICMP);
         }
 
         final List<byte[]> nonTentativeIpv6Addrs = getIpv6Addresses(
@@ -1951,19 +2381,19 @@
             v6Gen.addCountAndDrop(DROPPED_IPV6_NS_OTHER_HOST);
             return;
         }
-        v6Gen.addCountAndDropIfBytesAtR0EqualsNoneOf(
-                nonTentativeIpv6Addrs, DROPPED_IPV6_NS_OTHER_HOST);
+        v6Gen.addCountAndDropIfBytesAtOffsetEqualsNoneOf(
+                ICMP6_NS_TARGET_IP_OFFSET, nonTentativeIpv6Addrs, DROPPED_IPV6_NS_OTHER_HOST);
 
         // if source ip is unspecified (::), it's DAD request -> pass
-        v6Gen.addLoadImmediate(R0, IPV6_SRC_ADDR_OFFSET)
-                .addCountAndPassIfBytesAtR0Equal(IPV6_UNSPECIFIED_ADDRESS, PASSED_IPV6_NS_DAD);
+        v6Gen.addCountAndPassIfBytesAtOffsetEqual(
+                IPV6_SRC_ADDR_OFFSET, IPV6_UNSPECIFIED_ADDRESS, PASSED_IPV6_ICMP);
 
         // Only offload NUD/Address resolution packets that have SLLA as the their first option.
         // For option-less NUD packets or NUD/Address resolution packets where
         // 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)
-                .addCountAndPassIfR0LessThan(32, PASSED_IPV6_NS_NO_SLLA_OPTION);
+        v6Gen.addLoad16intoR0(IPV6_PAYLOAD_LEN_OFFSET)
+                .addCountAndPassIfR0LessThan(32, PASSED_IPV6_ICMP);
 
         // if the first option is not SLLA -> pass
         // 0                   1                   2                   3
@@ -1971,20 +2401,159 @@
         // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         // |     Type      |    Length     |Link-Layer Addr  |
         // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
-        v6Gen.addLoad8(R0, ICMP6_NS_OPTION_TYPE_OFFSET)
-                .addCountAndPassIfR0NotEquals(ICMPV6_ND_OPTION_SLLA,
-                        PASSED_IPV6_NS_NO_SLLA_OPTION);
+        v6Gen.addLoad8intoR0(ICMP6_NS_OPTION_TYPE_OFFSET)
+                .addCountAndPassIfR0NotEquals(ICMPV6_ND_OPTION_SLLA, PASSED_IPV6_ICMP);
 
         // Src IPv6 address check:
         // if multicast address (FF::/8) or loopback address (00::/8) -> drop
-        v6Gen.addLoad8(R0, IPV6_SRC_ADDR_OFFSET)
+        v6Gen.addLoad8intoR0(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)
+        v6Gen.addLoad8intoR0(ICMP6_NS_OPTION_TYPE_OFFSET + 2)
                 .addCountAndDropIfR0AnyBitsSet(1, DROPPED_IPV6_NS_INVALID);
         generateNonDadNaTransmit(v6Gen);
-        v6Gen.addCountAndDrop(Counter.DROPPED_IPV6_NS_REPLIED_NON_DAD);
+        v6Gen.addCountAndDrop(DROPPED_IPV6_NS_REPLIED_NON_DAD);
+    }
+
+    /**
+     * Generates filter code to handle IPv6 mDNS packets.
+     * <p>
+     * On entry, this filter knows it is processing an IPv6 packet. It will then process all IPv6
+     * mDNS packets, either passing or dropping them. IPv6 non-mDNS packets are skipped.
+     *
+     * @param gen the APF generator to generate the filter code
+     * @param labelCheckMdnsQueryPayload the label to jump to for checking the mDNS query payload
+     */
+    private void generateIPv6MdnsFilter(ApfV6GeneratorBase<?> gen,
+            short labelCheckMdnsQueryPayload) throws IllegalInstructionException {
+        final short skipMdnsFilter = gen.getUniqueLabel();
+
+        // If the packet is too short to be a valid IPv6 mDNS packet, the filter is skipped.
+        // For APF performance reasons, we check udp destination port before confirming it is IPv6
+        // udp packet. We proceed only if the destination port is 5353 (mDNS). Otherwise, skip
+        // filtering.
+        gen.addLoadFromMemory(R0, MemorySlot.PACKET_SIZE)
+                .addJumpIfR0LessThan(
+                        ETH_HEADER_LEN + IPV6_HEADER_LEN + UDP_HEADER_LEN + DNS_HEADER_LEN,
+                        skipMdnsFilter)
+                .addLoad16intoR0(IPV6_UDP_DESTINATION_PORT_OFFSET)
+                .addJumpIfR0NotEquals(MDNS_PORT, skipMdnsFilter);
+
+        // If the destination MAC address is not 33:33:00:00:00:fb (the mDNS multicast MAC
+        // address for IPv6 mDNS packet) or the device's MAC address, skip filtering.
+        // We need to check both the mDNS multicast MAC address and the device's MAC address
+        // because multicast to unicast conversion might have occurred.
+        gen.addJumpIfBytesAtOffsetEqualsNoneOf(
+                ETH_DEST_ADDR_OFFSET,
+                List.of(mHardwareAddress, ETH_MULTICAST_MDNS_V6_MAC_ADDRESS),
+                skipMdnsFilter
+        );
+
+        // Skip filtering if the packet is not an IPv6 UDP packet.
+        gen.addLoad8intoR0(IPV6_NEXT_HEADER_OFFSET)
+                .addJumpIfR0NotEquals(IPPROTO_UDP, skipMdnsFilter);
+
+        // Skip filtering if the IPv6 destination address is not ff02::fb (the mDNS multicast
+        // IPv6 address).
+        // Some devices can use unicast queries for mDNS to improve performance and reliability.
+        // These packets are not currently offloaded and will be passed by APF and handled
+        // by NsdService.
+        gen.addJumpIfBytesAtOffsetNotEqual(IPV6_DEST_ADDR_OFFSET, MDNS_IPV6_ADDR, skipMdnsFilter);
+
+        // We now know that the packet is an mDNS packet,
+        // i.e., an IPv6 UDP packet destined for port 5353 with the expected destination MAC and IP
+        // addresses.
+
+        // If the packet contains questions, check the query payload. Otherwise, check the
+        // reply payload.
+        gen.addLoad16intoR0(IPV6_DNS_QDCOUNT_OFFSET)
+                // Set the UDP payload offset in R1 before potentially jumping to the payload
+                // check logic.
+                .addLoadImmediate(R1, IPv6_UDP_PAYLOAD_OFFSET)
+                .addJumpIfR0NotEquals(0, labelCheckMdnsQueryPayload);
+
+        // TODO: check the reply payload.
+        if (mMulticastFilter) {
+            gen.addCountAndDrop(DROPPED_MDNS);
+        } else {
+            gen.addCountAndPass(PASSED_MDNS);
+        }
+
+        gen.defineLabel(skipMdnsFilter);
+    }
+
+    /**
+     * Generate filter code to reply and drop unicast ICMPv6 echo request.
+     * <p>
+     * On entry, we know it is IPv6 packet, but don't know anything else.
+     * R0 contains the u8 IPv6 next header.
+     * R1 contains nothing useful in it, and can be clobbered.
+     */
+    private void generateUnicastIpv6PingOffload(ApfV6GeneratorBase<?> gen)
+            throws IllegalInstructionException {
+
+        final short skipPing6Offload = gen.getUniqueLabel();
+        gen.addJumpIfR0NotEquals(IPPROTO_ICMPV6, skipPing6Offload);
+
+        gen.addLoad8intoR0(ICMP6_TYPE_OFFSET)
+                .addJumpIfR0NotEquals(ICMPV6_ECHO_REQUEST_TYPE, skipPing6Offload);
+
+        // Only offload unicast ping6.
+        // While we could potentially support offloading multicast and broadcast ping6 requests in
+        // the future, such packets will likely be dropped by the multicast filter.
+        // Since the device may have packet forwarding enabled, APF needs to pass any received
+        // unicast ping6 not destined for the device's IP address to the kernel.
+        final List<byte[]> nonTentativeIPv6Addrs = getIpv6Addresses(
+                true /* includeNonTentative */,
+                false /* includeTentative */,
+                false /* includeAnycast */);
+        gen.addJumpIfBytesAtOffsetNotEqual(
+                ETHER_DST_ADDR_OFFSET, mHardwareAddress, skipPing6Offload)
+                .addJumpIfBytesAtOffsetEqualsNoneOf(
+                        IPV6_DEST_ADDR_OFFSET, nonTentativeIPv6Addrs, skipPing6Offload);
+
+        // We need to check if the packet is sufficiently large to be a valid ICMPv6 echo packet.
+        gen.addLoadFromMemory(R0, MemorySlot.PACKET_SIZE)
+                .addCountAndDropIfR0LessThan(
+                        ETHER_HEADER_LEN + IPV6_HEADER_LEN + ICMP6_ECHO_REQUEST_HEADER_LEN,
+                        DROPPED_IPV6_ICMP6_ECHO_REQUEST_INVALID);
+
+        int hopLimit = mDependencies.getIpv6DefaultHopLimit(mInterfaceParams.name);
+        // Construct the ICMPv6 echo reply packet.
+        gen.addLoadFromMemory(R0, MemorySlot.PACKET_SIZE)
+                .addAllocateR0()
+                // Eth header
+                .addPacketCopy(ETHER_SRC_ADDR_OFFSET, ETHER_ADDR_LEN) // Dst MAC address
+                .addDataCopy(mHardwareAddress) // Src MAC address
+                // Reuse the following fields from input packet
+                //  2 byte: ethertype
+                //  4 bytes: version, traffic class, flowlabel
+                //  2 bytes: payload length
+                //  1 byte: next header
+                .addPacketCopy(ETH_ETHERTYPE_OFFSET, 9)
+                .addWriteU8(hopLimit)
+                .addPacketCopy(IPV6_DEST_ADDR_OFFSET, IPV6_ADDR_LEN) // Src ip
+                .addPacketCopy(IPV6_SRC_ADDR_OFFSET, IPV6_ADDR_LEN) // Dst ip
+                .addWriteU16((ICMP6_ECHO_REPLY << 8) | 0) // Type: echo reply, code: 0
+                // Checksum: initialized to the IPv6 payload length as a partial checksum. The final
+                // checksum will be calculated by the interpreter.
+                .addPacketCopy(IPV6_PAYLOAD_LEN_OFFSET, 2)
+                // Copy identifier, sequence number and ping payload
+                .addSub(ICMP6_CONTENT_OFFSET)
+                .addLoadImmediate(R1, ICMP6_CONTENT_OFFSET)
+                .addSwap() // Swaps R0 and R1, so they're the offset and length.
+                .addPacketCopyFromR0LenR1()
+                .addTransmitL4(
+                        ETHER_HEADER_LEN, // ip_ofs
+                        ICMP6_CHECKSUM_OFFSET, // csum_ofs
+                        IPV6_SRC_ADDR_OFFSET, // csum_start
+                        IPPROTO_ICMPV6, // partial_sum
+                        false // udp
+                )
+                .addCountAndDrop(DROPPED_IPV6_ICMP6_ECHO_REQUEST_REPLIED);
+
+        gen.defineLabel(skipPing6Offload);
     }
 
     /**
@@ -1992,19 +2561,66 @@
      * DROP_LABEL or PASS_LABEL, or falls off the end for ICMPv6 packets.
      * Preconditions:
      *  - Packet being filtered is IPv6
+     *
+     * @param gen the APF generator to generate the filter code
+     * @param labelCheckMdnsQueryPayload the label to jump to for checking the mDNS query payload
      */
-    private void generateIPv6Filter(ApfV4GeneratorBase<?> gen)
+    private void generateIPv6Filter(ApfV4GeneratorBase<?> gen, short labelCheckMdnsQueryPayload)
             throws IllegalInstructionException {
         // Here's a basic summary of what the IPv6 filter program does:
         //
-        // if there is a hop-by-hop option present (e.g. MLD query)
-        //   pass
+        // if there is a HOPOPTS option present (e.g. MLD query)
+        //   (APFv6+ specific logic)
+        //   if MLD offload is enabled:
+        //     if it is an MLDv1 report/done or MLDv2 report:
+        //       drop
+        //     if the payload length is invalid (25, 26, 27):
+        //       drop
+        //     if the IPv6 src addr is not link-local address:
+        //       drop
+        //     if the IPv6 hop limit is not 1:
+        //       drop
+        //     if it is an multicast address specific query (the MLD multicast address is not "::"):
+        //       pass
+        //     if the IPv6 dst addr is not ff02::1:
+        //       drop
+        //     if it is an MLDv2 general query (payload length is not 24):
+        //       transmit MLDv2 report and drop
+        //     else it is an MLDv1 general query:
+        //       transmit MLDv1 reports (one report per multicast group) and drop
+        //   else
+        //     pass (on APFv2+)
+        //
+        // (APFv6+ specific logic)
+        // if it's mDNS:
+        //   if it's a query:
+        //     if mNumOfMdnsRuleToOffload == -1:
+        //       pass
+        //     if the query matches one of the offload rules from idx [0, mNumOfMdnsRuleToOffload):
+        //       transmit mDNS reply and drop
+        //     else if query matches one of the rest of the offload rules:
+        //       pass
+        //     else if filtering multicast (i.e. multicast lock not held):
+        //       drop
+        //     else
+        //       pass
+        //   else:
+        //     if filtering multicast (i.e. multicast lock not held):
+        //       drop
+        //     else
+        //       pass
+        //
+        // (APFv6+ specific logic) if it's unicast ICMPv6 echo request to our host:
+        //    transmit echo reply and drop
+        //
         // if we're dropping multicast
         //   if it's not ICMPv6 or it's ICMPv6 but we're in doze mode:
         //     if it's multicast:
         //       drop
         //     pass
-        // (APFv6+ specific logic) if it's ICMPv6 NS:
+        //
+        // (APFv6+ specific logic)
+        // if it's ICMPv6 NS:
         //   if there are no IPv6 addresses (including link local address) on the interface:
         //     pass
         //   if MAC dst is none of known {unicast, multicast, broadcast} MAC addresses
@@ -2033,23 +2649,39 @@
         //   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
         //   drop
+        //
         // if keepalive ack
         //   drop
 
-        gen.addLoad8(R0, IPV6_NEXT_HEADER_OFFSET);
+        gen.addLoad8intoR0(IPV6_NEXT_HEADER_OFFSET);
 
-        // MLD packets set the router-alert hop-by-hop option.
-        // TODO: be smarter about not blindly passing every packet with HBH options.
-        gen.addCountAndPassIfR0Equals(IPPROTO_HOPOPTS, Counter.PASSED_MLD);
+        if (enableMldOffload()) {
+            generateMldFilter((ApfV6GeneratorBase<?>) gen);
+            gen.addLoad8intoR0(IPV6_NEXT_HEADER_OFFSET);
+        } else {
+            gen.addCountAndPassIfR0Equals(IPPROTO_HOPOPTS, PASSED_IPV6_HOPOPTS);
+        }
+
+        if (enableMdns6Offload()) {
+            generateIPv6MdnsFilter((ApfV6GeneratorBase<?>) gen, labelCheckMdnsQueryPayload);
+            gen.addLoad8intoR0(IPV6_NEXT_HEADER_OFFSET);
+        }
+
+        if (enableIpv6PingOffload()) {
+            generateUnicastIpv6PingOffload((ApfV6GeneratorBase<?>) gen);
+            gen.addLoad8intoR0(IPV6_NEXT_HEADER_OFFSET);
+        }
 
         // Drop multicast if the multicast filter is enabled.
         if (mMulticastFilter) {
-            final String skipIPv6MulticastFilterLabel = gen.getUniqueLabel();
-            final String dropAllIPv6MulticastsLabel = gen.getUniqueLabel();
+            final short skipIPv6MulticastFilterLabel = gen.getUniqueLabel();
+            final short dropAllIPv6MulticastsLabel = gen.getUniqueLabel();
 
             // While in doze mode, drop ICMPv6 multicast pings, let the others pass.
             // While awake, let all ICMPv6 multicasts through.
@@ -2059,7 +2691,7 @@
 
                 // ICMPv6 but not ECHO? -> Skip the multicast filter.
                 // (ICMPv6 ECHO requests will go through the multicast filter below).
-                gen.addLoad8(R0, ICMP6_TYPE_OFFSET);
+                gen.addLoad8intoR0(ICMP6_TYPE_OFFSET);
                 gen.addJumpIfR0NotEquals(ICMPV6_ECHO_REQUEST_TYPE, skipIPv6MulticastFilterLabel);
             } else {
                 gen.addJumpIfR0Equals(IPPROTO_ICMPV6, skipIPv6MulticastFilterLabel);
@@ -2067,121 +2699,603 @@
 
             // Drop all other packets sent to ff00::/8 (multicast prefix).
             gen.defineLabel(dropAllIPv6MulticastsLabel);
-            gen.addLoad8(R0, IPV6_DEST_ADDR_OFFSET);
-            gen.addCountAndDropIfR0Equals(0xff, Counter.DROPPED_IPV6_NON_ICMP_MULTICAST);
+            gen.addLoad8intoR0(IPV6_DEST_ADDR_OFFSET);
+            gen.addCountAndDropIfR0Equals(0xff, DROPPED_IPV6_NON_ICMP_MULTICAST);
             // If any keepalive filter matches, drop
             generateV6KeepaliveFilters(gen);
             // Not multicast. Pass.
-            gen.addCountAndPass(Counter.PASSED_IPV6_UNICAST_NON_ICMP);
+            gen.addCountAndPass(PASSED_IPV6_UNICAST_NON_ICMP);
             gen.defineLabel(skipIPv6MulticastFilterLabel);
         } else {
             generateV6KeepaliveFilters(gen);
             // If not ICMPv6, pass.
-            gen.addCountAndPassIfR0NotEquals(IPPROTO_ICMPV6, Counter.PASSED_IPV6_NON_ICMP);
+            gen.addCountAndPassIfR0NotEquals(IPPROTO_ICMPV6, PASSED_IPV6_NON_ICMP);
         }
 
         // If we got this far, the packet is ICMPv6.  Drop some specific types.
         // Not ICMPv6 NS -> skip.
-        gen.addLoad8(R0, ICMP6_TYPE_OFFSET); // warning: also used further below.
-        final ApfV6Generator v6Gen = tryToConvertToApfV6Generator(gen);
-        if (v6Gen != null && mShouldHandleNdOffload) {
-            final String skipNsPacketFilter = v6Gen.getUniqueLabel();
-            v6Gen.addJumpIfR0NotEquals(ICMPV6_NEIGHBOR_SOLICITATION, skipNsPacketFilter);
-            generateNsFilter(v6Gen);
+        gen.addLoad8intoR0(ICMP6_TYPE_OFFSET); // warning: also used further below.
+        if (enableNdOffload()) {
+            final short skipNsPacketFilter = gen.getUniqueLabel();
+            gen.addJumpIfR0NotEquals(ICMPV6_NEIGHBOR_SOLICITATION, skipNsPacketFilter);
+            generateNsFilter((ApfV6GeneratorBase<?>) gen);
             // End of NS filter. generateNsFilter() method is terminal, so NS packet will be
             // either dropped or passed inside generateNsFilter().
-            v6Gen.defineLabel(skipNsPacketFilter);
+            gen.defineLabel(skipNsPacketFilter);
         }
 
         // Add unsolicited multicast neighbor announcements filter
-        String skipUnsolicitedMulticastNALabel = gen.getUniqueLabel();
+        short skipUnsolicitedMulticastNALabel = gen.getUniqueLabel();
         // Drop all router solicitations (b/32833400)
-        gen.addCountAndDropIfR0Equals(ICMPV6_ROUTER_SOLICITATION,
-                Counter.DROPPED_IPV6_ROUTER_SOLICITATION);
+        gen.addCountAndDropIfR0Equals(ICMPV6_ROUTER_SOLICITATION, DROPPED_IPV6_ROUTER_SOLICITATION);
         // If not neighbor announcements, skip filter.
         gen.addJumpIfR0NotEquals(ICMPV6_NEIGHBOR_ADVERTISEMENT, skipUnsolicitedMulticastNALabel);
         // Drop all multicast NA to ff02::/120.
         // This is a way to cover ff02::1 and ff02::2 with a single JNEBS.
         // TODO: Drop only if they don't contain the address of on-link neighbours.
         final byte[] unsolicitedNaDropPrefix = Arrays.copyOf(IPV6_ALL_NODES_ADDRESS, 15);
-        gen.addLoadImmediate(R0, IPV6_DEST_ADDR_OFFSET);
-        gen.addJumpIfBytesAtR0NotEqual(unsolicitedNaDropPrefix, skipUnsolicitedMulticastNALabel);
+        gen.addJumpIfBytesAtOffsetNotEqual(
+                IPV6_DEST_ADDR_OFFSET, unsolicitedNaDropPrefix, skipUnsolicitedMulticastNALabel);
 
-        gen.addCountAndDrop(Counter.DROPPED_IPV6_MULTICAST_NA);
+        gen.addCountAndDrop(DROPPED_IPV6_MULTICAST_NA);
         gen.defineLabel(skipUnsolicitedMulticastNALabel);
     }
 
     /**
-     * Generate filter code to process mDNS packets. Execution of this code ends in * DROP_LABEL
-     * or PASS_LABEL if the packet is mDNS packets. Otherwise, skip this check.
+     * Creates the portion of an IGMP packet from the Ethernet source MAC address to the IPv4
+     * Type of Service field.
      */
-    private void generateMdnsFilter(ApfV4GeneratorBase<?> gen)
-            throws IllegalInstructionException {
-        final String skipMdnsv4Filter = gen.getUniqueLabel();
-        final String skipMdnsFilter = gen.getUniqueLabel();
-        final String checkMdnsUdpPort = gen.getUniqueLabel();
+    private byte[] createIgmpPktFromEthSrcToIPv4Tos() {
+        return CollectionUtils.concatArrays(
+                mHardwareAddress,
+                new byte[] {
+                        // etherType: IPv4
+                        (byte) 0x08, 0x00,
+                        // version, IHL
+                        (byte) 0x46,
+                        // Tos: 0xC0 (ref: net/ipv4/igmp.c#igmp_send_report())
+                        (byte) 0xc0}
+        );
+    }
 
-        // Only turn on the filter if multicast filter is on and the qname allowlist is non-empty.
-        if (!mMulticastFilter || mMdnsAllowList.isEmpty()) {
-            return;
+    /**
+     * Creates the portion of an IGMP packet from the IPv4 Identification field to the IPv4
+     * Source Address.
+     */
+    private byte[] createIgmpPktFromIPv4IdToSrc() {
+        final byte[] ipIdToSrc = new byte[] {
+                // identification
+                0, 0,
+                // fragment flag
+                (byte) (IPV4_FLAG_DF >> 8), 0,
+                // TTL
+                (byte) 1,
+                // protocol
+                (byte) IPV4_PROTOCOL_IGMP,
+                // router alert option is { 0x94, 0x04, 0x00, 0x00 }, so we precalculate IPv4
+                // checksum as 0x9404 + 0x0000 = 0x9404
+                (byte) 0x94, (byte) 0x04
+        };
+        return CollectionUtils.concatArrays(
+                ipIdToSrc,
+                mIPv4Address
+        );
+    }
+
+    /**
+     * Creates IGMPv3 Membership Report packet payload (rfc3376#section-7.3.2).
+     */
+    private byte[] createIgmpV3ReportPayload() {
+        final int groupNum = mIPv4McastAddrsExcludeAllHost.size();
+        final byte[] igmpHeader = new byte[] {
+                // IGMP type
+                (byte) IPV4_IGMP_TYPE_V3_REPORT,
+                // reserved
+                0,
+                // checksum, calculate later
+                0, 0,
+                // reserved
+                0, 0,
+                // num group records
+                (byte) ((groupNum >> 8) & 0xff), (byte) (groupNum & 0xff)
+        };
+        final byte[] groupRecordHeader = new byte[] {
+                // record type
+                (byte) IGMPV3_MODE_IS_EXCLUDE,
+                // aux data len,
+                0,
+                // num src
+                0, 0
+        };
+        final byte[] payload =
+                new byte[igmpHeader.length + groupNum * (groupRecordHeader.length + IPV4_ADDR_LEN)];
+        int offset = 0;
+
+        System.arraycopy(igmpHeader, 0, payload, offset, igmpHeader.length);
+        offset += igmpHeader.length;
+        for (Inet4Address mcastAddr: mIPv4McastAddrsExcludeAllHost) {
+            System.arraycopy(groupRecordHeader, 0, payload, offset, groupRecordHeader.length);
+            offset += groupRecordHeader.length;
+            System.arraycopy(mcastAddr.getAddress(), 0, payload, offset, IPV4_ADDR_LEN);
+            offset += IPV4_ADDR_LEN;
         }
 
-        // Here's a basic summary of what the mDNS filter program does:
-        //
-        // A packet is considered as a multicast mDNS packet if it matches all the following
-        // conditions
-        //   1. its destination MAC address matches 01:00:5E:00:00:FB or 33:33:00:00:00:FB, for
-        //   v4 and v6 respectively.
-        //   2. it is an IPv4/IPv6 packet
-        //   3. it is a UDP packet with port 5353
+        return payload;
+    }
 
-        // Check it's L2 mDNS multicast address.
-        gen.addLoadImmediate(R0, ETH_DEST_ADDR_OFFSET);
-        gen.addJumpIfBytesAtR0NotEqual(ETH_MULTICAST_MDNS_V4_MAC_ADDRESS, skipMdnsv4Filter);
+    /**
+     * Generate transmit code to send IGMPv3 report in response to general query packets.
+     */
+    private void generateIgmpV3ReportTransmit(ApfV6GeneratorBase<?> gen,
+            byte[] igmpPktFromEthSrcToIpTos, byte[] igmpPktFromIpIdToSrc)
+            throws IllegalInstructionException {
+        // We place template packet chunks in the data region first to reduce the number of
+        // instructions needed for creating multiple IGMPv2 reports.
+        // The following packet chunks can be used for creating both IGMPv2 and IGMPv3 reports:
+        //   - from Ethernet source to IPv4 Tos: 10 bytes
+        //   - from IPv4 identification to source address: 12 bytes
+        final int igmpV2Ipv4TotalLen =
+                IPV4_HEADER_MIN_LEN + IPV4_ROUTER_ALERT_OPTION_LEN + IPV4_IGMP_MIN_SIZE;
+        final byte[] igmpV3ReportPayload = createIgmpV3ReportPayload();
+        final byte[] igmpReportTemplate = CollectionUtils.concatArrays(
+                ETH_MULTICAST_IGMP_V3_ALL_MULTICAST_ROUTERS_ADDRESS,
+                igmpPktFromEthSrcToIpTos,
+                new byte[] {
+                        (byte) ((igmpV2Ipv4TotalLen >> 8) & 0xff),
+                        (byte) (igmpV2Ipv4TotalLen & 0xff),
+                },
+                igmpPktFromIpIdToSrc,
+                IPV4_ALL_IGMPV3_MULTICAST_ROUTERS_ADDRESS,
+                IPV4_ROUTER_ALERT_OPTION,
+                igmpV3ReportPayload
+        );
+        gen.maybeUpdateDataRegion(igmpReportTemplate);
 
-        // Checks it's IPv4.
-        gen.addLoad16(R0, ETH_ETHERTYPE_OFFSET);
-        gen.addJumpIfR0NotEquals(ETH_P_IP, skipMdnsFilter);
+        final int ipv4TotalLen = IPV4_HEADER_MIN_LEN
+                + IPV4_ROUTER_ALERT_OPTION_LEN
+                + IPV4_IGMP_MIN_SIZE
+                + (mIPv4McastAddrsExcludeAllHost.size() * IPV4_IGMP_GROUP_RECORD_SIZE);
+        final byte[] igmpV3FromEthDstToIpTos = CollectionUtils.concatArrays(
+                ETH_MULTICAST_IGMP_V3_ALL_MULTICAST_ROUTERS_ADDRESS,
+                igmpPktFromEthSrcToIpTos
+        );
+        final byte[] igmpV3PktFromIpIdToEnd = CollectionUtils.concatArrays(
+                igmpPktFromIpIdToSrc,
+                IPV4_ALL_IGMPV3_MULTICAST_ROUTERS_ADDRESS,
+                IPV4_ROUTER_ALERT_OPTION,
+                igmpV3ReportPayload
+        );
+        gen.addAllocate(ETHER_HEADER_LEN + ipv4TotalLen)
+                .addDataCopy(igmpV3FromEthDstToIpTos)
+                .addWriteU16(ipv4TotalLen)
+                .addDataCopy(igmpV3PktFromIpIdToEnd)
+                .addTransmitL4(
+                        // ip_ofs
+                        ETHER_HEADER_LEN,
+                        // csum_ofs
+                        IGMP_CHECKSUM_WITH_ROUTER_ALERT_OFFSET,
+                        // csum_start
+                        ETHER_HEADER_LEN + IPV4_HEADER_MIN_LEN + IPV4_ROUTER_ALERT_OPTION_LEN,
+                        // partial_sum
+                        0,
+                        // udp
+                        false
+                )
+                .addCountAndDrop(Counter.DROPPED_IGMP_V3_GENERAL_QUERY_REPLIED);
+    }
 
-        // Check it's not a fragment.
-        gen.addLoad16(R0, IPV4_FRAGMENT_OFFSET_OFFSET);
-        gen.addJumpIfR0AnyBitsSet(IPV4_FRAGMENT_MORE_FRAGS_MASK | IPV4_FRAGMENT_OFFSET_MASK,
-                skipMdnsFilter);
+    /**
+     * Generate transmit code to send IGMPv2 report in response to general query packets.
+     */
+    private void generateIgmpV2ReportTransmit(ApfV6GeneratorBase<?> gen,
+            byte[] igmpPktFromEthSrcToIpTos, byte[] igmpPktFromIpIdToSrc)
+            throws IllegalInstructionException {
+        final int ipv4TotalLen =
+                IPV4_HEADER_MIN_LEN + IPV4_ROUTER_ALERT_OPTION_LEN + IPV4_IGMP_MIN_SIZE;
+        final byte[] igmpV2PktFromEthSrcToIpSrc =  CollectionUtils.concatArrays(
+                igmpPktFromEthSrcToIpTos,
+                new byte[] {
+                        (byte) ((ipv4TotalLen >> 8) & 0xff), (byte) (ipv4TotalLen & 0xff),
+                },
+                igmpPktFromIpIdToSrc
+        );
+        for (Inet4Address mcastAddr: mIPv4McastAddrsExcludeAllHost) {
+            final MacAddress mcastEther =
+                    NetworkStackUtils.ipv4MulticastToEthernetMulticast(mcastAddr);
+            gen.addAllocate(ETHER_HEADER_LEN + ipv4TotalLen)
+                    .addDataCopy(mcastEther.toByteArray())
+                    .addDataCopy(igmpV2PktFromEthSrcToIpSrc)
+                    .addDataCopy(mcastAddr.getAddress())
+                    .addDataCopy(IGMPV2_REPORT_FROM_IPV4_OPTION_TO_IGMP_CHECKSUM)
+                    .addDataCopy(mcastAddr.getAddress())
+                    .addTransmitL4(
+                            // ip_ofs
+                            ETHER_HEADER_LEN,
+                            // csum_ofs
+                            IGMP_CHECKSUM_WITH_ROUTER_ALERT_OFFSET,
+                            // csum_start
+                            ETHER_HEADER_LEN + IPV4_HEADER_MIN_LEN + IPV4_ROUTER_ALERT_OPTION_LEN,
+                            // partial_sum
+                            0,
+                            // udp
+                            false
+                    );
+        }
 
-        // Checks it's UDP.
-        gen.addLoad8(R0, IPV4_PROTOCOL_OFFSET);
-        gen.addJumpIfR0NotEquals(IPPROTO_UDP, skipMdnsFilter);
+        gen.addCountAndDrop(Counter.DROPPED_IGMP_V2_GENERAL_QUERY_REPLIED);
+    }
 
-        // Set R1 to IPv4 header.
-        gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE);
-        gen.addJump(checkMdnsUdpPort);
+    /**
+     * Generates filter code to handle IGMP packets.
+     * <p>
+     * On entry, this filter know it is processing an IPv4 packet. It will then process all IGMP
+     * packets, either passing or dropping them. Non-IGMP packets are skipped.
+     */
+    private void generateIgmpFilter(ApfV6GeneratorBase<?> v6Gen)
+            throws IllegalInstructionException {
+        final short skipIgmpFilter = v6Gen.getUniqueLabel();
+        final short checkIgmpV1orV2 = v6Gen.getUniqueLabel();
 
-        gen.defineLabel(skipMdnsv4Filter);
+        // Check 1) it's not a fragment. 2) it's IGMP.
+        v6Gen.addJumpIfNotUnfragmentedIPv4Protocol(IPV4_PROTOCOL_IGMP, skipIgmpFilter);
 
-        // Checks it's L2 mDNS multicast address.
-        // Relies on R0 containing the ethernet destination mac address offset.
-        gen.addJumpIfBytesAtR0NotEqual(ETH_MULTICAST_MDNS_V6_MAC_ADDRESS, skipMdnsFilter);
+        // Calculate the IPv4 payload length: (total length - IPv4 header length).
+        // Memory slot 0 is occupied temporarily to store the length.
+        v6Gen.addLoad16intoR0(IPV4_TOTAL_LENGTH_OFFSET)
+                .addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE)
+                .addNeg(R1)
+                .addAddR1ToR0()
+                .addStoreToMemory(MemorySlot.SLOT_0, R0);
 
-        // Checks it's IPv6.
-        gen.addLoad16(R0, ETH_ETHERTYPE_OFFSET);
-        gen.addJumpIfR0NotEquals(ETH_P_IPV6, skipMdnsFilter);
+        // If payload length is less than 8 or equal to 9, 10, 11, it's invalid IGMP packet: drop.
+        v6Gen.addCountAndDropIfR0LessThan(IPV4_IGMP_MIN_SIZE, DROPPED_IGMP_INVALID)
+                .addCountAndDropIfR0IsOneOf(Set.of(9L, 10L, 11L), DROPPED_IGMP_INVALID);
 
-        // Checks it's UDP.
-        gen.addLoad8(R0, IPV6_NEXT_HEADER_OFFSET);
-        gen.addJumpIfR0NotEquals(IPPROTO_UDP, skipMdnsFilter);
+        // If it's an IGMPv1/IGMPv2/IGMPv3 report: drop.
+        // A host normally cancels its own pending report if it observes
+        // an identical report from another host on the network (host suppression).
+        // While dropping reports here technically disrupts this host's suppression behavior,
+        // it is acceptable since other devices on the network will perform the suppression.
+        // If the IGMP type is not one of the reports, it's either a query(type=0x11) or an
+        // invalid packet.
+        v6Gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE)
+                .addLoad8R1IndexedIntoR0(ETHER_HEADER_LEN)
+                .addCountAndDropIfR0IsOneOf(IGMP_TYPE_REPORTS, DROPPED_IGMP_REPORT)
+                .addCountAndDropIfR0NotEquals(IPV4_IGMP_TYPE_QUERY, DROPPED_IGMP_INVALID);
 
-        // Set R1 to IPv6 header.
-        gen.addLoadImmediate(R1, IPV6_HEADER_LEN);
+        // If group address is not 0.0.0.0, it's an IGMPv2/v3 group specific query: pass.
+        // rfc3376#section-6.1 mentions group specific queries are sent when a router receives a
+        // State-Change record indicating a system is leaving a group. Therefore, since the
+        // router only sends group-specific queries after receiving a leave message, it is not
+        // sent out periodically.
+        // Increased APF bytecode size for offloading these queries may not yield significant
+        // power benefits. In this case, letting the kernel handle group-specific queries is
+        // acceptable.
+        v6Gen.addLoad32R1IndexedIntoR0(IGMP_MULTICAST_ADDRESS_OFFSET)
+                .addCountAndPassIfR0NotEquals(0 /* 0.0.0.0 */, PASSED_IPV4);
 
-        // Checks it's mDNS UDP port
-        gen.defineLabel(checkMdnsUdpPort);
-        gen.addLoad16Indexed(R0, TCP_UDP_DESTINATION_PORT_OFFSET);
-        gen.addJumpIfR0NotEquals(MDNS_PORT, skipMdnsFilter);
+        // If we reach here, we know it is an IGMPv1/IGMPv2/IGMPv3 general query.
 
-        // TODO: implement APFv6 mDNS offload
+        // The general query IPv4 destination address must be 224.0.0.1.
+        v6Gen.addLoad32intoR0(IPV4_DEST_ADDR_OFFSET)
+                .addCountAndDropIfR0NotEquals(IPV4_ALL_HOSTS_ADDRESS_IN_LONG,
+                        DROPPED_IGMP_INVALID);
 
-        // end of mDNS filter
-        gen.defineLabel(skipMdnsFilter);
+        // Check payload length, since invalid length already checked,
+        // it should be 8 (IGMPv1 or IGMPv2) or >=12 (IGMPv3)
+        v6Gen.addLoadFromMemory(R0, MemorySlot.SLOT_0)
+                .addJumpIfR0Equals(IPV4_IGMP_MIN_SIZE, checkIgmpV1orV2);
+
+        // ===== IGMPv3 general query =====
+        // To optimize for bytecode size, the IGMPv3 report is constructed first.
+        // Its packet structure is then reused as a template when creating the IGMPv2 report.
+        final byte[] igmpPktFromEthSrcToIpTos = createIgmpPktFromEthSrcToIPv4Tos();
+        final byte[] igmpPktFromIpIdToSrc = createIgmpPktFromIPv4IdToSrc();
+        generateIgmpV3ReportTransmit(v6Gen, igmpPktFromEthSrcToIpTos, igmpPktFromIpIdToSrc);
+
+        // ===== IGMPv1 or IGMPv2 general query =====
+        v6Gen.defineLabel(checkIgmpV1orV2);
+        // Based on rfc3376#section-7.1 If max resp time is 0, it's IGMPv1: pass.
+        // We don't expect many networks are still using IGMPv1, pass it to the kernel to save
+        // bytecode size.
+        // (Note: R1 is still IPV4_HEADER_SIZE)
+        v6Gen.addLoad8R1IndexedIntoR0(IGMP_MAX_RESP_TIME_OFFSET)
+                .addCountAndPassIfR0Equals(0, PASSED_IPV4); // IGMPv1
+
+        // Drop and transmit IGMPv2 reports
+        generateIgmpV2ReportTransmit(v6Gen, igmpPktFromEthSrcToIpTos, igmpPktFromIpIdToSrc);
+
+        v6Gen.defineLabel(skipIgmpFilter);
+    }
+
+    /**
+     * Creates MLDv1 Listener Report packet message (rfc2710#section-3).
+     */
+    private byte[] createMldV1ReportMessage(final Inet6Address mcastAddr) {
+        final byte[] mldv1Header = new byte[] {
+            // MLD type
+            (byte) IPV6_MLD_TYPE_V1_REPORT,
+            // code
+            0,
+            // hop-by-hop option is { 0x3a, 0x00, 0x05, 0x02, 0x00, 0x00, 0x01, 0x00 }
+            // so we precalculate MLD checksum as follows:
+            // 0xffff - (0x3a00 + 0x0502 + 0x0000 + 0x0100) = 0xbffd
+            (byte) 0xbf, (byte) 0xfd,
+            // max response delay
+            0, 0,
+            // reserved
+            0, 0
+        };
+
+        return CollectionUtils.concatArrays(mldv1Header, mcastAddr.getAddress());
+    }
+
+    /**
+     * Creates MLDv2 Listener Report packet payload (rfc3810#section-5.2).
+     */
+    private byte[] createMldV2ReportPayload() {
+        final int mcastAddrsNum = mIPv6McastAddrsExcludeAllHost.size();
+        final byte[] mldHeader = new byte[] {
+            // MLD type
+            (byte) IPV6_MLD_TYPE_V2_REPORT,
+            // code
+            0,
+            // hop-by-hop option is { 0x3a, 0x00, 0x05, 0x02, 0x00, 0x00, 0x01, 0x00 }
+            // so we precalculate MLD checksum as follows:
+            // 0xffff - (0x3a00 + 0x0502 + 0x0000 + 0x0100) = 0xbffd
+            (byte) 0xbf, (byte) 0xfd,
+            // reserved
+            0, 0,
+            // num of multicast address records
+            (byte) ((mcastAddrsNum >> 8) & 0xff), (byte) (mcastAddrsNum & 0xff)
+        };
+
+        final byte[] mcastRecordHeader = new byte[] {
+            // record type
+            (byte) MLD2_MODE_IS_EXCLUDE,
+            // aux data len,
+            0,
+            // num src
+            0, 0
+        };
+
+        final byte[] payload =
+                new byte[
+                    mldHeader.length + mcastAddrsNum * IPV6_MLD_V2_MULTICAST_ADDRESS_RECORD_SIZE
+                ];
+        int offset = 0;
+
+        System.arraycopy(mldHeader, 0, payload, offset, mldHeader.length);
+        offset += mldHeader.length;
+        for (Inet6Address mcastAddr: mIPv6McastAddrsExcludeAllHost) {
+            System.arraycopy(mcastRecordHeader, 0, payload, offset, mcastRecordHeader.length);
+            offset += mcastRecordHeader.length;
+            System.arraycopy(mcastAddr.getAddress(), 0, payload, offset, IPV6_ADDR_LEN);
+            offset += IPV6_ADDR_LEN;
+        }
+
+        return payload;
+    }
+
+    /**
+     * Creates the portion of an MLD packet from the Ethernet source MAC address to the IPv6
+     * VTF field.
+     */
+    private byte[] createMldPktFromEthSrcToIPv6Vtf() {
+        return CollectionUtils.concatArrays(
+            mHardwareAddress,
+            new byte[] {
+                // etherType: IPv6
+                (byte) 0x86, (byte) 0xdd,
+                // version, traffic class, flow label
+                // 0x60000000 (ref: net/ipv6/mcast.c#ip6_mc_hdr())
+                (byte) 0x60, 0, 0, 0}
+        );
+    }
+
+    /**
+     * Creates the portion of an MLD packet from the IPv6 Next Header to the IPv6 Source Address.
+     */
+    private byte[] createMldPktFromIPv6NextHdrToSrc() {
+        final byte[] ipv6FromNextHdrToHoplimit = new byte[] {
+            // Next header: HOPOPTS
+            0,
+            // Hop limit
+            (byte) 1
+        };
+        return CollectionUtils.concatArrays(
+            ipv6FromNextHdrToHoplimit,
+            mIPv6LinkLocalAddress.getAddress()
+        );
+    }
+
+    /**
+     * Generate transmit code to send MLDv1 report in response to general query packets.
+     */
+    private void generateMldV1ReportTransmit(ApfV6GeneratorBase<?> gen,
+            byte[] mldPktFromEthSrcToIpv6Vtf, byte[] mldPktFromIpv6NextHdrToSrc)
+            throws IllegalInstructionException {
+        final int packetSize =
+                ETHER_HEADER_LEN
+                + IPV6_HEADER_LEN
+                + IPV6_MLD_HOPOPTS.length
+                + IPV6_MLD_V1_MESSAGE_SIZE;
+        final int mldV1Ipv6PayloadLength = IPV6_MLD_HOPOPTS.length + IPV6_MLD_V1_MESSAGE_SIZE;
+        final byte[] mldV1PktFromEthSrcToIpv6Src =  CollectionUtils.concatArrays(
+                mldPktFromEthSrcToIpv6Vtf,
+                new byte[] {
+                        (byte) ((mldV1Ipv6PayloadLength >> 8) & 0xff),
+                        (byte) (mldV1Ipv6PayloadLength & 0xff),
+                },
+                mldPktFromIpv6NextHdrToSrc
+        );
+        for (Inet6Address mcastAddr: mIPv6McastAddrsExcludeAllHost) {
+            final MacAddress mcastEther =
+                    NetworkStackUtils.ipv6MulticastToEthernetMulticast(mcastAddr);
+            gen.addAllocate(packetSize)
+                    .addDataCopy(mcastEther.toByteArray())
+                    .addDataCopy(mldV1PktFromEthSrcToIpv6Src)
+                    .addDataCopy(mcastAddr.getAddress())
+                    .addDataCopy(IPV6_MLD_HOPOPTS)
+                    .addDataCopy(createMldV1ReportMessage(mcastAddr))
+                    .addTransmitL4(
+                        // ip_ofs
+                        ETHER_HEADER_LEN,
+                        // csum_ofs
+                        IPV6_MLD_CHECKSUM_OFFSET,
+                        // csum_start
+                        IPV6_SRC_ADDR_OFFSET,
+                        // partial_sum
+                        IPPROTO_ICMPV6 + IPV6_MLD_V1_MESSAGE_SIZE,
+                        // udp
+                        false
+                    );
+        }
+
+        gen.addCountAndDrop(DROPPED_IPV6_MLD_V1_GENERAL_QUERY_REPLIED);
+    }
+
+    /**
+     * Generate transmit code to send MLDv2 report in response to general query packets.
+     */
+    private void generateMldV2ReportTransmit(ApfV6GeneratorBase<?> gen,
+            byte[] mldPktFromEthSrcToIpv6Vtf, byte[] mldPktFromIpv6NextHdrToSrc)
+            throws IllegalInstructionException {
+        final int mldV1Ipv6PayloadLength = IPV6_MLD_HOPOPTS.length + IPV6_MLD_V1_MESSAGE_SIZE;
+        final byte[] encodedMldV1Ipv6PayloadLength = {
+            (byte) ((mldV1Ipv6PayloadLength >> 8) & 0xff), (byte) (mldV1Ipv6PayloadLength & 0xff),
+        };
+        // We place template packet chunks in the data region first to reduce the number of
+        // instructions needed for creating multiple MLDv1 reports.
+        // The following packet chunks can be used for creating both MLDv1 and MLDv2 reports:
+        //   - from Ethernet source to IPv6 VTF: 12 bytes
+        //   - from IPv6 next header to source address: 18 bytes
+        final byte[] mldV2ReportPayload = createMldV2ReportPayload();
+        final byte[] template = CollectionUtils.concatArrays(
+            ETH_MULTICAST_MLD_V2_ALL_MULTICAST_ROUTERS_ADDRESS,
+            mldPktFromEthSrcToIpv6Vtf,
+            encodedMldV1Ipv6PayloadLength,
+            mldPktFromIpv6NextHdrToSrc,
+            IPV6_MLD_V2_ALL_ROUTERS_MULTICAST_ADDRESS,
+            IPV6_MLD_HOPOPTS,
+            mldV2ReportPayload
+        );
+        gen.maybeUpdateDataRegion(template);
+
+        final byte[] mldV2PktFromEthDstToIpv6Vtf = CollectionUtils.concatArrays(
+                ETH_MULTICAST_MLD_V2_ALL_MULTICAST_ROUTERS_ADDRESS,
+                mldPktFromEthSrcToIpv6Vtf
+        );
+        final byte[] mldV2PktFromIpv6NextHdrToEnd = CollectionUtils.concatArrays(
+                mldPktFromIpv6NextHdrToSrc,
+                IPV6_MLD_V2_ALL_ROUTERS_MULTICAST_ADDRESS,
+                IPV6_MLD_HOPOPTS,
+                mldV2ReportPayload
+        );
+        final int mcastAddrsNum = mIPv6McastAddrsExcludeAllHost.size();
+        final int ipv6PayloadLength = IPV6_MLD_HOPOPTS.length
+                + IPV6_MLD_MESSAGE_MIN_SIZE
+                + (mcastAddrsNum * IPV6_MLD_V2_MULTICAST_ADDRESS_RECORD_SIZE);
+        gen.addAllocate(ETHER_HEADER_LEN + IPV6_HEADER_LEN + ipv6PayloadLength)
+            .addDataCopy(mldV2PktFromEthDstToIpv6Vtf)
+            .addWriteU16(ipv6PayloadLength)
+            .addDataCopy(mldV2PktFromIpv6NextHdrToEnd)
+            .addTransmitL4(
+                // ip_ofs
+                ETHER_HEADER_LEN,
+                // csum_ofs
+                IPV6_MLD_CHECKSUM_OFFSET,
+                // csum_start
+                IPV6_SRC_ADDR_OFFSET,
+                // partial_sum
+                IPPROTO_ICMPV6 + (ipv6PayloadLength - IPV6_MLD_HOPOPTS.length),
+                // udp
+                false
+            ).addCountAndDrop(DROPPED_IPV6_MLD_V2_GENERAL_QUERY_REPLIED);
+    }
+
+    /**
+     * Generates filter code to handle MLD packets.
+     * <p>
+     * On entry, this filter knows it is processing an IPv6 packet. It will then process all MLD
+     * packets, either passing or dropping them. Non-MLD packets are skipped.
+     * R0 contains the u8 IPv6 next header.
+     */
+    private void generateMldFilter(ApfV6GeneratorBase<?> gen)
+            throws IllegalInstructionException {
+        final short skipMldFilter = gen.getUniqueLabel();
+        final short checkMldv1 = gen.getUniqueLabel();
+
+        // If next header is not hop-by-hop, then skip
+        gen.addJumpIfR0NotEquals(IPPROTO_HOPOPTS, skipMldFilter);
+
+        final int mldPacketMinSize =
+                ETHER_HEADER_LEN + IPV6_HEADER_LEN + IPV6_MLD_HOPOPTS.length + IPV6_MLD_MIN_SIZE;
+        // If packet is too small to be MLD packet, then skip
+        gen.addLoadFromMemory(R0, MemorySlot.PACKET_SIZE)
+                .addJumpIfR0LessThan(mldPacketMinSize, skipMldFilter)
+                .addSub(ETHER_HEADER_LEN + IPV6_HEADER_LEN + IPV6_MLD_HOPOPTS.length)
+                // Memory slot 0 is occupied temporarily to store the MLD payload length.
+                .addStoreToMemory(MemorySlot.SLOT_0, R0);
+
+        // If the hop-by-hop option is not the one used by MLD, then skip
+        gen.addLoadImmediate(R0, IPV6_EXT_HEADER_OFFSET)
+                .addJumpIfBytesAtR0NotEqual(IPV6_MLD_HOPOPTS, skipMldFilter);
+
+        // If the packet is an MLDv1 report or done, or an MLDv2 report, then drop it.
+        // Else if the packet is not an MLD query packet, then skip.
+        gen.addLoad8intoR0(IPV6_MLD_TYPE_OFFSET)
+                .addCountAndDropIfR0IsOneOf(IPV6_MLD_TYPE_REPORTS, DROPPED_IPV6_MLD_REPORT)
+                .addJumpIfR0NotEquals(IPV6_MLD_TYPE_QUERY, skipMldFilter);
+
+        // If we reach here, we know it is an MLDv1/MLDv2 query.
+
+        // If the payload length is 25, 26, or 27, the MLD packet is invalid and should be dropped.
+        gen.addLoadFromMemory(R0, MemorySlot.SLOT_0)
+                .addCountAndDropIfR0IsOneOf(Set.of(25L, 26L, 27L), DROPPED_IPV6_MLD_INVALID);
+
+        // rfc3810#section-5 and rfc2710#section-3 describe that all MLD messages are sent with a
+        // link-local IPv6 source address, an IPv6 Hop Limit of 1, and an IPv6 Router Alert
+        // option [RTR-ALERT] in a Hop-by-Hop Options header.
+        // rfc3810#section-5.2.13 describes that an MLDv2 Report MUST be sent with a valid
+        // IPv6 link-local source address, or the unspecified address (::), if the sending interface
+        // has not yet acquired a valid link-local address.
+        // Its OK to not check :: here since we also drop MLD reports.
+        // If the source address is a not a link-local address, then drop.
+        gen.addLoad16intoR0(IPV6_SRC_ADDR_OFFSET)
+                .addCountAndDropIfR0NotEquals(0xfe80, DROPPED_IPV6_MLD_INVALID);
+
+        // If hop limit is not 1, then drop.
+        gen.addLoad8intoR0(IPV6_HOP_LIMIT_OFFSET)
+                .addCountAndDropIfR0NotEquals(1, DROPPED_IPV6_MLD_INVALID);
+
+        // If the multicast address is not "::", it is an MLD2 multicast-address-specific query,
+        // then pass.
+        gen.addCountAndPassIfBytesAtOffsetNotEqual(
+                IPV6_MLD_MULTICAST_ADDR_OFFSET, IPV6_ADDR_ANY.getAddress(), PASSED_IPV6_ICMP);
+
+        // If we reach here, we know it is an MLDv1/MLDv2 general query.
+
+        // The general query IPv6 destination address must be ff02::1.
+        gen.addCountAndDropIfBytesAtOffsetNotEqual(
+                        IPV6_DEST_ADDR_OFFSET, IPV6_ALL_NODES_ADDRESS, DROPPED_IPV6_MLD_INVALID);
+
+        // If the MLD payload length is 24, it is an MLDv1 packet, otherwise, it is an MLDv2 packet.
+        gen.addLoadFromMemory(R0, MemorySlot.SLOT_0)
+                .addJumpIfR0Equals(IPV6_MLD_MIN_SIZE, checkMldv1);
+
+        // ===== MLDv2 general query =====
+        // To optimize for bytecode size, the MLDv2 report is constructed first.
+        // Its packet structure is then reused as a template when creating the IGMPv1 report.
+        final byte[] mldPktFromEthSrcToIPv6Vtf = createMldPktFromEthSrcToIPv6Vtf();
+        final byte[] mldPktFromIPv6NextHdrToSrc = createMldPktFromIPv6NextHdrToSrc();
+        generateMldV2ReportTransmit(gen, mldPktFromEthSrcToIPv6Vtf, mldPktFromIPv6NextHdrToSrc);
+
+        gen.defineLabel(checkMldv1);
+        // ===== MLDv1 general query =====
+        generateMldV1ReportTransmit(gen, mldPktFromEthSrcToIPv6Vtf, mldPktFromIPv6NextHdrToSrc);
+
+        gen.defineLabel(skipMldFilter);
     }
 
     /**
@@ -2192,23 +3306,23 @@
      */
     private void generateV4TcpPort7Filter(ApfV4GeneratorBase<?> gen)
             throws IllegalInstructionException {
-        final String skipPort7V4Filter = gen.getUniqueLabel();
+        final short skipPort7V4Filter = gen.getUniqueLabel();
 
         // Check it's TCP.
-        gen.addLoad8(R0, IPV4_PROTOCOL_OFFSET);
+        gen.addLoad8intoR0(IPV4_PROTOCOL_OFFSET);
         gen.addJumpIfR0NotEquals(IPPROTO_TCP, skipPort7V4Filter);
 
         // Check it's not a fragment or is the initial fragment.
-        gen.addLoad16(R0, IPV4_FRAGMENT_OFFSET_OFFSET);
+        gen.addLoad16intoR0(IPV4_FRAGMENT_OFFSET_OFFSET);
         gen.addJumpIfR0AnyBitsSet(IPV4_FRAGMENT_OFFSET_MASK, skipPort7V4Filter);
 
         // Check it's destination port 7.
         gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE);
-        gen.addLoad16Indexed(R0, TCP_UDP_DESTINATION_PORT_OFFSET);
+        gen.addLoad16R1IndexedIntoR0(TCP_UDP_DESTINATION_PORT_OFFSET);
         gen.addJumpIfR0NotEquals(ECHO_PORT, skipPort7V4Filter);
 
         // Drop it.
-        gen.addCountAndDrop(Counter.DROPPED_IPV4_TCP_PORT7_UNICAST);
+        gen.addCountAndDrop(DROPPED_IPV4_TCP_PORT7_UNICAST);
 
         // Skip label.
         gen.defineLabel(skipPort7V4Filter);
@@ -2220,6 +3334,188 @@
                 gen.getUniqueLabel());
     }
 
+    private byte[] createMdns4PktFromEthDstToIPv4Tos(boolean enabled) {
+        if (!enabled) {
+            return null;
+        }
+        return concatArrays(
+                ETH_MULTICAST_MDNS_V4_MAC_ADDRESS,
+                mHardwareAddress,
+                new byte[]{
+                        0x08, 0x00, // ethertype: IPv4
+                        0x45, 0x00, // version, IHL, DSCP, ECN,
+                });
+    }
+
+    private byte[] createMdns6PktFromEthDstToIPv6FlowLabel(boolean enabled) {
+        if (!enabled) {
+            return null;
+        }
+        return concatArrays(
+                ETH_MULTICAST_MDNS_V6_MAC_ADDRESS,
+                mHardwareAddress,
+                new byte[]{
+                        (byte) 0x86, (byte) 0xdd, // ethertype: IPv6
+                        0x60, 0x00, 0x00, 0x00, // version, traffic class, flow label
+                });
+    }
+
+
+    private byte[] createMdns4PktFromIPv4IdToUdpDport(boolean enabled) {
+        if (!enabled) {
+            return null;
+        }
+        return concatArrays(
+                new byte[]{
+                        0x00, 0x00, // identification
+                        (byte) (IPV4_FLAG_DF >> 8), 0, // flags, fragment offset
+                        (byte) 0xff, // set TTL to 255 per rfc6762#section-11
+                        (byte) IPPROTO_UDP,
+                        0x00, 0x00, // checksum, it's a placeholder that will be filled in later.
+                },
+                mIPv4Address,
+                MDNS_IPV4_ADDR,
+                MDNS_PORT_IN_BYTES, // source port
+                MDNS_PORT_IN_BYTES); // destination port
+    }
+
+    private byte[] createMdns6PktFromIPv6NextHdrToUdpDport(boolean enabled) {
+        if (!enabled) {
+            return null;
+        }
+        return concatArrays(
+                new byte[]{
+                        (byte) IPPROTO_UDP,
+                        (byte) 0xff, // set hop limit to 255 per rfc6762#section-11
+                },
+                mIPv6LinkLocalAddress.getAddress(),
+                MDNS_IPV6_ADDR,
+                MDNS_PORT_IN_BYTES, // source port
+                MDNS_PORT_IN_BYTES); // destination port
+    }
+
+    /**
+     * Generates filter code to process an mDNS payload against offload rules.
+     * The generated filter code is guaranteed to process all IPv4 and IPv6 mDNS packets,
+     * ensuring each packet is either passed or dropped.
+     * <p>
+     * The only way to enter the mDNS offload payload check logic is by jumping to the
+     * labelCheckMdnsQueryPayload label.
+     * On entry, the packet is known to be an IPv4/IPv6 mDNS query packet, and register R1
+     * is set to the offset of the beginning of the UDP payload (the DNS header).
+     *
+     * @param gen the APF generator to generate the filter code
+     * @param labelCheckMdnsQueryPayload the label to jump to for checking the mDNS query payload
+     */
+    private void generateMdnsQueryOffload(ApfV6GeneratorBase<?> gen,
+            short labelCheckMdnsQueryPayload, int numOfMdnsRuleToOffload)
+            throws IllegalInstructionException {
+        // The mDNS payload check logic is terminal; the program will always result in either
+        // PASS or DROP.
+        gen.defineLabel(labelCheckMdnsQueryPayload);
+
+        if (numOfMdnsRuleToOffload == -1) {
+            gen.addCountAndPass(PASSED_MDNS);
+            return;
+        }
+
+        // Set R0 to the offset of the beginning of the UDP payload (the DNS header)
+        gen.addSwap();
+
+        final boolean enableMdns4 = enableMdns4Offload();
+        final boolean enableMdns6 = enableMdns6Offload();
+        final byte[] mdns4EthDstToTos = createMdns4PktFromEthDstToIPv4Tos(enableMdns4);
+        final byte[] mdns4IdToUdpDport = createMdns4PktFromIPv4IdToUdpDport(enableMdns4);
+        final byte[] mdns6EthDstToFlowLabel = createMdns6PktFromEthDstToIPv6FlowLabel(enableMdns6);
+        final byte[] mdns6NextHdrToUdpDport = createMdns6PktFromIPv6NextHdrToUdpDport(enableMdns6);
+
+        for (int i = 0; i < mOffloadRules.size(); i++) {
+            final MdnsOffloadRule rule = mOffloadRules.get(i);
+            final short ruleNotMatch = gen.getUniqueLabel();
+            final short ruleMatch = gen.getUniqueLabel();
+            final short offloadIPv6Mdns = gen.getUniqueLabel();
+
+            for (MdnsOffloadRule.Matcher matcher : rule.mMatchers) {
+                try {
+                    gen.addJumpIfPktAtR0ContainDnsQ(matcher.mQnames, matcher.mQtypes, ruleMatch);
+                } catch (IllegalArgumentException e) {
+                    Log.e(TAG, "Failed to generate mDNS offload filter for rule: " + rule, e);
+                }
+            }
+
+            gen.addJump(ruleNotMatch);
+
+            gen.defineLabel(ruleMatch);
+
+            // If there is no offload payload, pass the packet to let NsdService handle it.
+            // If there isn't enough space to offload all rules, packets should be processed
+            // by iterating through the rules, starting with the lowest priority.
+            if (rule.mOffloadPayload == null || i >= numOfMdnsRuleToOffload) {
+                gen.addCountAndPass(PASSED_MDNS);
+            } else {
+                if (enableMdns4 && enableMdns6) {
+                    gen.addLoad16intoR0(ETH_ETHERTYPE_OFFSET)
+                            .addJumpIfR0NotEquals(ETH_P_IP, offloadIPv6Mdns);
+                }
+
+                if (enableMdns4) {
+                    final int udpLength = UDP_HEADER_LEN + rule.mOffloadPayload.length;
+                    final int ipv4TotalLength = IPV4_HEADER_MIN_LEN + udpLength;
+                    final int pktLength = ETH_HEADER_LEN + ipv4TotalLength;
+
+                    gen.addAllocate(pktLength)
+                            .addDataCopy(mdns4EthDstToTos)
+                            .addWriteU16(ipv4TotalLength)
+                            .addDataCopy(mdns4IdToUdpDport)
+                            .addWrite32(udpLength << 16) // udp length and checksum
+                            .addDataCopy(rule.mOffloadPayload)
+                            .addTransmitL4(
+                                    ETH_HEADER_LEN, // ip_ofs
+                                    IPV4_UDP_DESTINATION_CHECKSUM_NO_OPTIONS_OFFSET, // csum_ofs
+                                    IPV4_SRC_ADDR_OFFSET, // csum_start
+                                    IPPROTO_UDP + udpLength, // partial_sum
+                                    true // udp
+                            ).addCountAndDrop(Counter.DROPPED_MDNS_REPLIED);
+                }
+
+                if (enableMdns4 && enableMdns6) {
+                    gen.defineLabel(offloadIPv6Mdns);
+                }
+
+                if (enableMdns6) {
+                    final int udpLength = UDP_HEADER_LEN + rule.mOffloadPayload.length;
+                    final int pktLength = ETH_HEADER_LEN + IPV6_HEADER_LEN + udpLength;
+                    gen.addAllocate(pktLength)
+                            .addDataCopy(mdns6EthDstToFlowLabel)
+                            .addWriteU16(udpLength) // payload length
+                            .addDataCopy(mdns6NextHdrToUdpDport)
+                            .addWrite32(udpLength << 16) //  udp length and checksum
+                            .addDataCopy(rule.mOffloadPayload)
+                            .addTransmitL4(
+                                    ETH_HEADER_LEN, // ip_ofs
+                                    IPV6_UDP_DESTINATION_CHECKSUM_OFFSET, // csum_ofs
+                                    IPV6_SRC_ADDR_OFFSET, // csum_start
+                                    IPPROTO_UDP + udpLength, // partial_sum
+                                    true // udp
+                            ).addCountAndDrop(Counter.DROPPED_MDNS_REPLIED);
+                }
+            }
+
+            gen.defineLabel(ruleNotMatch);
+        }
+
+        // If no offload rules match, we should still respect the multicast filter. During the
+        // transition period, not all apps will use NsdManager for mDNS advertising. If an app
+        // decides to perform mDNS advertising itself, it must acquire a multicast lock, and no
+        // offload rules will be registered for that app. In this case, the APF should pass the
+        // mDNS packet and allow the app to handle the query.
+        if (mMulticastFilter) {
+            gen.addCountAndDrop(DROPPED_MDNS);
+        } else {
+            gen.addCountAndPass(PASSED_MDNS);
+        }
+    }
+
     /**
      * Begin generating an APF program to:
      * <ul>
@@ -2239,46 +3535,47 @@
      * <li>Let execution continue off the end of the program for IPv6 ICMPv6 packets. This allows
      *     insertion of RA filters here, or if there aren't any, just passes the packets.
      * </ul>
+     * @param gen the APF generator to generate the filter code
+     * @param labelCheckMdnsQueryPayload the label to jump to for checking the mDNS query payload
      */
-    private ApfV4GeneratorBase<?> emitPrologue() throws IllegalInstructionException {
-        // This is guaranteed to succeed because of the check in maybeCreate.
-        ApfV4GeneratorBase<?> gen;
-        if (shouldUseApfV6Generator()) {
-            gen = new ApfV6Generator(mApfVersionSupported, mApfRamSize,
-                    mInstallableProgramSizeClamp);
-        } else {
-            gen = new ApfV4Generator(mApfVersionSupported, mApfRamSize,
-                    mInstallableProgramSizeClamp);
-        }
-
+    private void emitPrologue(@NonNull ApfV4GeneratorBase<?> gen, short labelCheckMdnsQueryPayload)
+            throws IllegalInstructionException {
         if (hasDataAccess(mApfVersionSupported)) {
             if (gen instanceof ApfV4Generator) {
                 // Increment TOTAL_PACKETS.
                 // Only needed in APFv4.
                 // In APFv6, the interpreter will increase the counter on packet receive.
-                gen.addIncrementCounter(Counter.TOTAL_PACKETS);
+                gen.addIncrementCounter(TOTAL_PACKETS);
             }
 
             gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_SECONDS);
-            gen.addStoreCounter(Counter.FILTER_AGE_SECONDS, R0);
+            gen.addStoreCounter(FILTER_AGE_SECONDS, R0);
 
             // requires a new enough APFv5+ interpreter, otherwise will be 0
             gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_16384THS);
-            gen.addStoreCounter(Counter.FILTER_AGE_16384THS, R0);
+            gen.addStoreCounter(FILTER_AGE_16384THS, R0);
 
             // requires a new enough APFv5+ interpreter, otherwise will be 0
             gen.addLoadFromMemory(R0, MemorySlot.APF_VERSION);
-            gen.addStoreCounter(Counter.APF_VERSION, R0);
+            gen.addStoreCounter(APF_VERSION, R0);
 
             // store this program's sequential id, for later comparison
             gen.addLoadImmediate(R0, mNumProgramUpdates);
-            gen.addStoreCounter(Counter.APF_PROGRAM_ID, R0);
+            gen.addStoreCounter(APF_PROGRAM_ID, R0);
         }
 
         // Here's a basic summary of what the initial program does:
         //
         // if it is a loopback (src mac is nic's primary mac) packet
-        //    pass
+        //    if 25Q2+:
+        //      drop
+        //    else
+        //      pass
+        // if it's a TDLS packet:
+        //    it is unicast:
+        //      pass
+        //    else
+        //      drop
         // if it's a 802.3 Frame (ethtype < 0x0600):
         //    drop or pass based on configurations
         // if it has a ether-type that belongs to the black list
@@ -2293,18 +3590,33 @@
         //   pass
         // insert IPv6 filter to drop, pass, or fall off the end for ICMPv6 packets
 
-        gen.addLoadImmediate(R0, ETHER_SRC_ADDR_OFFSET);
-        gen.addCountAndPassIfBytesAtR0Equal(mHardwareAddress, PASSED_ETHER_OUR_SRC_MAC);
+        if (NetworkStackUtils.isAtLeast25Q2()) {
+            gen.addCountAndDropIfBytesAtOffsetEqual(ETHER_SRC_ADDR_OFFSET, mHardwareAddress,
+                    DROPPED_ETHER_OUR_SRC_MAC);
+        } else {
+            // TODO: we don't have test coverage for this line
+            gen.addCountAndPassIfBytesAtOffsetEqual(ETHER_SRC_ADDR_OFFSET, mHardwareAddress,
+                    PASSED_ETHER_OUR_SRC_MAC);
+        }
 
-        gen.addLoad16(R0, ETH_ETHERTYPE_OFFSET);
+        gen.addLoad16intoR0(ETH_ETHERTYPE_OFFSET);
         if (SdkLevel.isAtLeastV()) {
+            // Pass unicast TDLS packet but drop non-unicast TDLS packet.
+            short skipTDLScheck = gen.getUniqueLabel();
+            gen.addJumpIfR0NotEquals(0x890DL, skipTDLScheck)
+                    .addCountAndDropIfBytesAtOffsetNotEqual(
+                            ETH_DEST_ADDR_OFFSET, mHardwareAddress, DROPPED_NON_UNICAST_TDLS)
+                    .addCountAndPass(PASSED_NON_IP_UNICAST)
+                    .defineLabel(skipTDLScheck);
+
             // IPv4, ARP, IPv6, EAPOL, WAPI
-            gen.addCountAndDropIfR0IsNoneOf(Set.of(0x0800L, 0x0806L, 0x86DDL, 0x888EL, 0x88B4L),
-                    Counter.DROPPED_ETHERTYPE_NOT_ALLOWED);
+            gen.addCountAndDropIfR0IsNoneOf(
+                    Set.of(0x0800L, 0x0806L, 0x86DDL, 0x888EL, 0x88B4L),
+                    DROPPED_ETHERTYPE_NOT_ALLOWED);
         } else  {
             if (mDrop802_3Frames) {
                 // drop 802.3 frames (ethtype < 0x0600)
-                gen.addCountAndDropIfR0LessThan(ETH_TYPE_MIN, Counter.DROPPED_802_3_FRAME);
+                gen.addCountAndDropIfR0LessThan(ETH_TYPE_MIN, DROPPED_802_3_FRAME);
             }
             // Handle ether-type black list
             if (mEthTypeBlackList.length > 0) {
@@ -2312,58 +3624,188 @@
                 for (int p : mEthTypeBlackList) {
                     deniedEtherTypes.add((long) p);
                 }
-                gen.addCountAndDropIfR0IsOneOf(deniedEtherTypes,
-                        Counter.DROPPED_ETHERTYPE_NOT_ALLOWED);
+                gen.addCountAndDropIfR0IsOneOf(deniedEtherTypes, DROPPED_ETHERTYPE_NOT_ALLOWED);
             }
         }
 
         // Add ARP filters:
-        String skipArpFiltersLabel = gen.getUniqueLabel();
+        short skipArpFiltersLabel = gen.getUniqueLabel();
         gen.addJumpIfR0NotEquals(ETH_P_ARP, skipArpFiltersLabel);
         generateArpFilter(gen);
         gen.defineLabel(skipArpFiltersLabel);
 
-        // Add mDNS filter:
-        generateMdnsFilter(gen);
-        gen.addLoad16(R0, ETH_ETHERTYPE_OFFSET);
+        gen.addLoad16intoR0(ETH_ETHERTYPE_OFFSET);
 
         // Add IPv4 filters:
-        String skipIPv4FiltersLabel = gen.getUniqueLabel();
+        short skipIPv4FiltersLabel = gen.getUniqueLabel();
         gen.addJumpIfR0NotEquals(ETH_P_IP, skipIPv4FiltersLabel);
-        generateIPv4Filter(gen);
+        generateIPv4Filter(gen, labelCheckMdnsQueryPayload);
         gen.defineLabel(skipIPv4FiltersLabel);
 
         // Check for IPv6:
         // NOTE: Relies on R0 containing ethertype. This is safe because if we got here, we did
         // not execute the IPv4 filter, since this filter do not fall through, but either drop or
         // pass.
-        String ipv6FilterLabel = gen.getUniqueLabel();
+        short ipv6FilterLabel = gen.getUniqueLabel();
         gen.addJumpIfR0Equals(ETH_P_IPV6, ipv6FilterLabel);
 
         // Drop non-IP non-ARP broadcasts, pass the rest
-        gen.addLoadImmediate(R0, ETH_DEST_ADDR_OFFSET);
-        gen.addCountAndPassIfBytesAtR0NotEqual(ETHER_BROADCAST, Counter.PASSED_NON_IP_UNICAST);
-        gen.addCountAndDrop(Counter.DROPPED_ETH_BROADCAST);
+        gen.addCountAndPassIfBytesAtOffsetNotEqual(ETH_DEST_ADDR_OFFSET, ETHER_BROADCAST,
+                PASSED_NON_IP_UNICAST);
+        gen.addCountAndDrop(DROPPED_ETH_BROADCAST);
 
         // Add IPv6 filters:
         gen.defineLabel(ipv6FilterLabel);
-        generateIPv6Filter(gen);
-        return gen;
+        generateIPv6Filter(gen, labelCheckMdnsQueryPayload);
     }
 
-    /**
-     * Append packet counting epilogue to the APF program.
-     * <p>
-     * Currently, the epilogue consists of two trampolines which count passed and dropped packets
-     * before jumping to the actual PASS and DROP labels.
-     */
-    private void emitEpilogue(ApfV4GeneratorBase<?> gen) throws IllegalInstructionException {
-        // Execution will reach here if none of the filters match, which will pass the packet to
-        // the application processor.
-        gen.addCountAndPass(Counter.PASSED_IPV6_ICMP);
+    private String getApfConfigMessage() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("{ ");
+        sb.append("mcast: ");
+        sb.append(mMulticastFilter ? "DROP" : "ALLOW");
+        sb.append(", ");
+        sb.append("doze: ");
+        sb.append(mInDozeMode ? "TRUE" : "FALSE");
+        sb.append(", ");
+        sb.append("offloads: ");
+        sb.append("[ ");
+        if (enableArpOffload()) {
+            sb.append("ARP, ");
+        }
+        if (enableNdOffload()) {
+            sb.append("ND, ");
+        }
+        if (enableIgmpOffload()) {
+            sb.append("IGMP, ");
+        }
+        if (enableMldOffload()) {
+            sb.append("MLD, ");
+        }
+        if (enableIpv4PingOffload()) {
+            sb.append("Ping4, ");
+        }
+        if (enableIpv6PingOffload()) {
+            sb.append("Ping6, ");
+        }
+        if (enableMdns4Offload()) {
+            sb.append("Mdns4, ");
+        }
+        if (enableMdns6Offload()) {
+            sb.append("Mdns6, ");
+        }
+        sb.append("] ");
+        sb.append("total RAs: ");
+        sb.append(mRas.size());
+        sb.append(" filtered RAs: ");
+        sb.append(mNumFilteredRas);
+        sb.append(" mDNSs: ");
+        sb.append(mOffloadRules.size());
+        sb.append(" }");
+        return sb.toString();
+    }
 
-        // TODO: merge the addCountTrampoline() into generate() method
-        gen.addCountTrampoline();
+    private void installPacketFilter(byte[] program, String logInfo) {
+        if (!mApfController.installPacketFilter(program, logInfo)) {
+            sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
+        }
+    }
+
+    private ApfV4GeneratorBase<?> createApfGenerator() throws IllegalInstructionException {
+        if (useApfV61Generator()) {
+            return new ApfV61Generator(mApfVersionSupported, mApfRamSize,
+                    mInstallableProgramSizeClamp);
+        } else if (useApfV6Generator()) {
+            return new ApfV6Generator(mApfVersionSupported, mApfRamSize,
+                    mInstallableProgramSizeClamp);
+        } else {
+            return new ApfV4Generator(mApfVersionSupported, mApfRamSize,
+                    mInstallableProgramSizeClamp);
+        }
+    }
+
+    @VisibleForTesting
+    public int getOverEstimatedProgramSize() {
+        return mOverEstimatedProgramSize;
+    }
+
+    private int calcMdnsOffloadProgramSizeOverEstimate(int numOfMdnsRuleToOffload)
+            throws IllegalInstructionException {
+        ApfV6GeneratorBase<?> gen = (ApfV6GeneratorBase<?>) createApfGenerator();
+        // We need to preload data for size estimation because the preloaded data contains mDNS
+        // data chunks. If we don't preload, generateMdnsQueryOffload() will add data to the data
+        // region, resulting in an incorrect estimated size.
+        if (gen instanceof ApfV61GeneratorBase<?>) {
+            preloadData((ApfV61GeneratorBase<?>) gen);
+        }
+        final int programLengthOverEstimateBefore = gen.programLengthOverEstimate();
+        short tmpLabelCheckMdnsQueryPayload = gen.getUniqueLabel();
+        generateMdnsQueryOffload(gen, tmpLabelCheckMdnsQueryPayload,
+                numOfMdnsRuleToOffload);
+        return gen.programLengthOverEstimate() - programLengthOverEstimateBefore;
+    }
+
+    void preloadData(ApfV61GeneratorBase<?> gen) throws IllegalInstructionException {
+        final List<byte[]> preloadedMacAddress = getKnownMacAddresses();
+        final List<byte[]> preloadedIPv6Address = getIpv6Addresses(true /* includeNonTentative */,
+                true /* includeTentative */, true /* includeAnycast */);
+        preloadedIPv6Address.add(IPV6_ADDR_ALL_NODES_MULTICAST.getAddress());
+        preloadedIPv6Address.add(IPV6_ADDR_ANY.getAddress());
+        byte[] mdns6NextHdrToUdpDport = new byte[0];
+        byte[] mdns6EthDstToFlowLabel = new byte[0];
+        byte[] mdns4EthDstToTos = new byte[0];
+        if (enableMdns6Offload()) {
+            mdns6NextHdrToUdpDport = createMdns6PktFromIPv6NextHdrToUdpDport(true);
+            preloadedIPv6Address.removeIf(
+                    addr -> Arrays.equals(addr, mIPv6LinkLocalAddress.getAddress()));
+            mdns6EthDstToFlowLabel = createMdns6PktFromEthDstToIPv6FlowLabel(true);
+            preloadedMacAddress.removeIf(
+                    addr -> Arrays.equals(addr, mHardwareAddress) || Arrays.equals(addr,
+                            ETH_MULTICAST_MDNS_V6_MAC_ADDRESS));
+        }
+
+        if (enableMdns4Offload()) {
+            mdns4EthDstToTos = createMdns4PktFromEthDstToIPv4Tos(true);
+            preloadedMacAddress.removeIf(
+                    addr -> Arrays.equals(addr, mHardwareAddress) || Arrays.equals(addr,
+                            ETH_MULTICAST_MDNS_V4_MAC_ADDRESS));
+        }
+
+        int preloadDataSize = mdns6NextHdrToUdpDport.length + mdns6EthDstToFlowLabel.length
+                + mdns4EthDstToTos.length + preloadedIPv6Address.size() * 16
+                + preloadedMacAddress.size() * 6;
+
+        if (enableArpOffload()) {
+            preloadDataSize += FIXED_ARP_REPLY_HEADER.length;
+        }
+
+        final byte[] preloadData = new byte[preloadDataSize];
+        int offset = 0;
+        System.arraycopy(mdns6NextHdrToUdpDport, 0, preloadData, offset,
+                mdns6NextHdrToUdpDport.length);
+        offset += mdns6NextHdrToUdpDport.length;
+        System.arraycopy(mdns6EthDstToFlowLabel, 0, preloadData, offset,
+                mdns6EthDstToFlowLabel.length);
+        offset += mdns6EthDstToFlowLabel.length;
+        System.arraycopy(mdns4EthDstToTos, 0, preloadData, offset, mdns4EthDstToTos.length);
+        offset += mdns4EthDstToTos.length;
+        for (byte[] addr : preloadedMacAddress) {
+            System.arraycopy(addr, 0, preloadData, offset, 6);
+            offset += 6;
+        }
+        for (byte[] addr : preloadedIPv6Address) {
+            System.arraycopy(addr, 0, preloadData, offset, 16);
+            offset += 16;
+        }
+        if (enableArpOffload()) {
+            System.arraycopy(FIXED_ARP_REPLY_HEADER, 0, preloadData, offset,
+                    FIXED_ARP_REPLY_HEADER.length);
+            offset += FIXED_ARP_REPLY_HEADER.length;
+        }
+
+        if (preloadDataSize > 0) {
+            gen.addPreloadData(preloadData);
+        }
     }
 
     /**
@@ -2375,67 +3817,136 @@
         final byte[] program;
         int programMinLft = Integer.MAX_VALUE;
 
+        // Ensure the entire APF program uses the same time base.
+        final int timeSeconds = secondsSinceBoot();
+        // Every return from this function calls installPacketFilter().
+        mLastTimeInstalledProgram = timeSeconds;
+
+        // Increase the counter before we generate the program.
+        // This keeps the APF_PROGRAM_ID counter in sync with the program.
+        mNumProgramUpdates++;
+
         try {
-            // Ensure the entire APF program uses the same time base.
-            final int timeSeconds = secondsSinceBoot();
-            mLastTimeInstalledProgram = timeSeconds;
-            // Step 1: Determine how many RA filters we can fit in the program.
-            ApfV4GeneratorBase<?> gen = emitPrologue();
+            // Step 1: Determine how many RA filters/mDNS offloads we can fit in the program.
+            ApfV4GeneratorBase<?> gen = createApfGenerator();
+            if (gen instanceof ApfV61GeneratorBase<?>) {
+                preloadData((ApfV61GeneratorBase<?>) gen);
+            }
+            short labelCheckMdnsQueryPayload = gen.getUniqueLabel();
 
-            // The epilogue normally goes after the RA filters, but add it early to include its
-            // length when estimating the total.
-            emitEpilogue(gen);
+            emitPrologue(gen, labelCheckMdnsQueryPayload);
 
-            // Can't fit the program even without any RA filters?
-            if (gen.programLengthOverEstimate() > mMaximumApfProgramSize) {
+            int programLengthOverEstimate = gen.programLengthOverEstimate();
+
+            // The default packet handling normally goes after the RA filters, but add it early to
+            // include its length when estimating the total.
+            programLengthOverEstimate += gen.getDefaultPacketHandlingSizeOverEstimate();
+
+            // Can't fit the program even without any RA filters/Mdns offloads?
+            if (programLengthOverEstimate > mMaximumApfProgramSize) {
                 Log.e(TAG, "Program exceeds maximum size " + mMaximumApfProgramSize);
                 sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_OVER_SIZE_FAILURE);
+                installPacketFilter(new byte[mMaximumApfProgramSize],
+                        getApfConfigMessage() + " (clear memory, reason: program too large)");
                 return;
             }
 
+            // We attempt to fit the mDNS offload rules into the program before fitting RAs. The
+            // strategy is as follows:
+            // 1. If sufficient memory is available, offload all rules.
+            // 2. If memory is insufficient, switch low-priority rules to passthrough mode.
+            // 3. If memory remains insufficient after all rules are switched to passthrough
+            // mode, fail open to pass all mDNS packets.
+            //
+            // We prioritize mDNS offload over RA filters because:
+            // 1. We plan to move away from the multicast lock API, making mDNS offload critical
+            // for the application's proper function. Without it, app developers would likely
+            // still use the multicast lock, which would wake the device for almost all
+            // multicast traffic, leading to serious power problems.
+            // 2. For devices like TVs, reliable mDNS offload is key to meeting EU power regulation
+            // requirement. These devices are usually on home networks with very chatty mDNS
+            // traffic.
+            if (enableMdns4Offload() || enableMdns6Offload()) {
+                final int remainSize = mMaximumApfProgramSize - programLengthOverEstimate;
+                mNumOfMdnsRuleToOffload = mOffloadRules.size();
+                int mDnsProgramLengthOverEstimate = 0;
+                for (; mNumOfMdnsRuleToOffload >= -1; --mNumOfMdnsRuleToOffload) {
+                    mDnsProgramLengthOverEstimate = calcMdnsOffloadProgramSizeOverEstimate(
+                            mNumOfMdnsRuleToOffload);
+                    if (mDnsProgramLengthOverEstimate <= remainSize) {
+                        break;
+                    }
+                }
+
+                // When the size of offload rules is non-zero, at bare minimum, the
+                // program should be pass the mDNS packets. Otherwise, the application use case will
+                // be broken after we migrate away from multicast lock.
+                // If the remaining size is insufficient for mDNS fail-open, we should fail-open
+                // for the entire program.
+                if (mNumOfMdnsRuleToOffload < -1) {
+                    Log.e(TAG, "Program exceeds maximum size (unable to fail-open for mDNS)  "
+                            + mMaximumApfProgramSize);
+                    sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_OVER_SIZE_FAILURE);
+                    installPacketFilter(new byte[mMaximumApfProgramSize], getApfConfigMessage()
+                            + " (clear memory, reason: unable to fail-open for mDNS)");
+                    return;
+                }
+
+                programLengthOverEstimate += mDnsProgramLengthOverEstimate;
+            } else {
+                mNumOfMdnsRuleToOffload = -1;
+            }
+
+
             for (Ra ra : mRas) {
                 // skip filter if it has expired.
                 if (ra.getRemainingFilterLft(timeSeconds) <= 0) continue;
-                ra.generateFilter(gen, timeSeconds);
+                programLengthOverEstimate += ra.getRaProgramLengthOverEstimate(timeSeconds);
                 // Stop if we get too big.
-                if (gen.programLengthOverEstimate() > mMaximumApfProgramSize) {
-                    if (VDBG) Log.d(TAG, "Past maximum program size, skipping RAs");
-                    sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_OVER_SIZE_FAILURE);
+                if (programLengthOverEstimate > mMaximumApfProgramSize) {
+                    Log.i(TAG, "Past maximum program size, skipping RAs");
                     break;
                 }
 
+                ra.generateFilter(gen, timeSeconds);
+                programMinLft = Math.min(programMinLft, ra.getRemainingFilterLft(timeSeconds));
                 rasToFilter.add(ra);
             }
 
-            // Increase the counter before we generate the program.
-            // This keeps the APF_PROGRAM_ID counter in sync with the program.
-            mNumProgramUpdates++;
-
-            // Step 2: Actually generate the program
-            gen = emitPrologue();
-            for (Ra ra : rasToFilter) {
-                ra.generateFilter(gen, timeSeconds);
-                programMinLft = Math.min(programMinLft, ra.getRemainingFilterLft(timeSeconds));
+            gen.addDefaultPacketHandling();
+            if (enableMdns4Offload() || enableMdns6Offload()) {
+                generateMdnsQueryOffload((ApfV6GeneratorBase<?>) gen, labelCheckMdnsQueryPayload,
+                        mNumOfMdnsRuleToOffload);
             }
-            emitEpilogue(gen);
+
+            mNumFilteredRas = rasToFilter.size();
+            mOverEstimatedProgramSize = gen.programLengthOverEstimate();
             program = gen.generate();
         } catch (IllegalInstructionException | IllegalStateException | IllegalArgumentException e) {
             Log.wtf(TAG, "Failed to generate APF program.", e);
             sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_GENERATE_FILTER_EXCEPTION);
+            installPacketFilter(new byte[mMaximumApfProgramSize],
+                    getApfConfigMessage() + String.format(" (clear memory, reason: %s)",
+                            e.getMessage()));
             return;
         }
         if (mIsRunning) {
-            if (!mIpClientCallback.installPacketFilter(program)) {
-                sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
+            if (program.length > mMaximumApfProgramSize) {
+                Log.wtf(TAG, String.format(
+                        "Size estimation logic is wrong: final program size: %d exceeds maximum "
+                                + "size: %d. ",
+                        program.length, mMaximumApfProgramSize));
+                sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_OVER_SIZE_FAILURE);
+                installPacketFilter(new byte[mMaximumApfProgramSize],
+                        getApfConfigMessage() + " (clear memory, reason: wrong size estimation)");
+                return;
             }
+            installPacketFilter(program, getApfConfigMessage());
         }
         mLastInstalledProgramMinLifetime = programMinLft;
         mLastInstalledProgram = program;
         mMaxProgramSize = Math.max(mMaxProgramSize, program.length);
 
-        if (VDBG) {
-            hexDump("Installing filter: ", program, program.length);
-        }
     }
 
     private void hexDump(String msg, byte[] packet, int length) {
@@ -2462,8 +3973,6 @@
      */
     @VisibleForTesting
     public void processRa(byte[] packet, int length) {
-        if (VDBG) hexDump("Read packet = ", packet, length);
-
         final Ra ra;
         try {
             ra = new Ra(packet, length);
@@ -2475,13 +3984,13 @@
 
         // Update info for Metrics
         mLowestRouterLifetimeSeconds = getMinForPositiveValue(
-                mLowestRouterLifetimeSeconds, ra.routerLifetime());
+                mLowestRouterLifetimeSeconds, ra.mRouterLifetime);
         mLowestPioValidLifetimeSeconds = getMinForPositiveValue(
-                mLowestPioValidLifetimeSeconds, ra.minPioValidLifetime());
+                mLowestPioValidLifetimeSeconds, ra.mMinPioValidLifetime);
         mLowestRioRouteLifetimeSeconds = getMinForPositiveValue(
-                mLowestRioRouteLifetimeSeconds, ra.minRioRouteLifetime());
+                mLowestRioRouteLifetimeSeconds, ra.mMinRioRouteLifetime);
         mLowestRdnssLifetimeSeconds = getMinForPositiveValue(
-                mLowestRdnssLifetimeSeconds, ra.minRdnssLifetime());
+                mLowestRdnssLifetimeSeconds, ra.mMinRdnssLifetime);
 
         // Remove all expired RA filters before trying to match the new RA.
         // TODO: matches() still checks that the old RA filter has not expired. Consider removing
@@ -2538,7 +4047,7 @@
      * filtering using APF programs.
      */
     public static ApfFilter maybeCreate(Handler handler, Context context, ApfConfiguration config,
-            InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback,
+            InterfaceParams ifParams, IApfController apfController,
             NetworkQuirkMetrics networkQuirkMetrics) {
         if (context == null || config == null || ifParams == null) return null;
         if (!ApfV4Generator.supportsVersion(config.apfVersionSupported)) {
@@ -2549,7 +4058,7 @@
             return null;
         }
 
-        return new ApfFilter(handler, context, config, ifParams, ipClientCallback,
+        return new ApfFilter(handler, context, config, ifParams, apfController,
                 networkQuirkMetrics);
     }
 
@@ -2590,8 +4099,12 @@
         mRas.clear();
         mDependencies.removeBroadcastReceiver(mDeviceIdleReceiver);
         mIsApfShutdown = true;
-        if (shouldEnableMdnsOffload()) {
-            unregisterOffloadEngine();
+        if (SdkLevel.isAtLeastV() && mApfMdnsOffloadEngine != null) {
+            mApfMdnsOffloadEngine.unregisterOffloadEngine();
+        }
+
+        if (mMulticastReportMonitor != null) {
+            mMulticastReportMonitor.stop();
         }
     }
 
@@ -2667,11 +4180,11 @@
         mIPv4PrefixLength = prefix;
         mIPv6TentativeAddresses = ipv6Addresses.first;
         mIPv6NonTentativeAddresses = ipv6Addresses.second;
+        mIPv6LinkLocalAddress = NetworkStackUtils.selectPreferredIPv6LinkLocalAddress(lp);
 
         installNewProgram();
     }
 
-    @Override
     public void updateClatInterfaceState(boolean add) {
         if (mHasClat == add) {
             return;
@@ -2680,22 +4193,124 @@
         installNewProgram();
     }
 
-    @Override
-    public boolean supportNdOffload() {
-        return shouldUseApfV6Generator() && mShouldHandleNdOffload;
+    private boolean updateIPv6MulticastAddrs() {
+        final Set<Inet6Address> mcastAddrs =
+                new ArraySet<>(mDependencies.getIPv6MulticastAddresses(mInterfaceParams.name));
+
+        if (!mIPv6MulticastAddresses.equals(mcastAddrs)) {
+            mIPv6MulticastAddresses.clear();
+            mIPv6MulticastAddresses.addAll(mcastAddrs);
+
+            mIPv6McastAddrsExcludeAllHost.clear();
+            mIPv6McastAddrsExcludeAllHost.addAll(mIPv6MulticastAddresses);
+            mIPv6McastAddrsExcludeAllHost.remove(IPV6_ADDR_ALL_NODES_MULTICAST);
+            mIPv6McastAddrsExcludeAllHost.remove(IPV6_ADDR_NODE_LOCAL_ALL_NODES_MULTICAST);
+            return true;
+        }
+        return false;
     }
 
-    @ChecksSdkIntAtLeast(api = 35 /* Build.VERSION_CODES.VanillaIceCream */, codename =
-            "VanillaIceCream")
-    @Override
-    public boolean shouldEnableMdnsOffload() {
-        return shouldUseApfV6Generator() && mShouldHandleMdnsOffload;
+    private boolean updateIPv4MulticastAddrs() {
+        final Set<Inet4Address> mcastAddrs =
+                new ArraySet<>(mDependencies.getIPv4MulticastAddresses(mInterfaceParams.name));
+
+        if (!mIPv4MulticastAddresses.equals(mcastAddrs)) {
+            mIPv4MulticastAddresses.clear();
+            mIPv4MulticastAddresses.addAll(mcastAddrs);
+
+            mIPv4McastAddrsExcludeAllHost.clear();
+            mIPv4McastAddrsExcludeAllHost.addAll(mcastAddrs);
+            mIPv4McastAddrsExcludeAllHost.remove(IPV4_ADDR_ALL_HOST_MULTICAST);
+            return true;
+        }
+        return false;
     }
 
-    private boolean shouldUseApfV6Generator() {
+    /**
+     * Updates IPv4/IPv6 multicast addresses.
+     */
+    public void updateMulticastAddrs() {
+        boolean ipv6MulticastUpdated = updateIPv6MulticastAddrs();
+        boolean ipv4MulticastUpdated = updateIPv4MulticastAddrs();
+        if (ipv6MulticastUpdated || ipv4MulticastUpdated) {
+            installNewProgram();
+        }
+    }
+
+    @ChecksSdkIntAtLeast(api = 35 /* Build.VERSION_CODES.VanillaIceCream */)
+    private boolean enableArpOffload() {
+        return mHandleArpOffload && useApfV6Generator() && mIPv4Address != null;
+    }
+
+    @ChecksSdkIntAtLeast(api = 35 /* Build.VERSION_CODES.VanillaIceCream */)
+    public boolean enableNdOffload() {
+        return mHandleNdOffload && useApfV6Generator();
+    }
+
+    @ChecksSdkIntAtLeast(api = 35 /* Build.VERSION_CODES.VanillaIceCream */)
+    private boolean enableOffloadEngineRegistration() {
+        return mHandleMdnsOffload && useApfV6Generator();
+    }
+
+    @ChecksSdkIntAtLeast(api = 35 /* Build.VERSION_CODES.VanillaIceCream */)
+    private boolean enableIgmpReportsMonitor() {
+        return mHandleIgmpOffload && useApfV6Generator();
+    }
+
+    @ChecksSdkIntAtLeast(api = 35 /* Build.VERSION_CODES.VanillaIceCream */)
+    private boolean enableMdns4Offload() {
+        return enableOffloadEngineRegistration() && mIPv4Address != null
+                && !mOffloadRules.isEmpty();
+    }
+
+    @ChecksSdkIntAtLeast(api = 35 /* Build.VERSION_CODES.VanillaIceCream */)
+    private boolean enableMdns6Offload() {
+        return enableOffloadEngineRegistration() && mIPv6LinkLocalAddress != null
+                && !mOffloadRules.isEmpty();
+    }
+
+    @ChecksSdkIntAtLeast(api = 35 /* Build.VERSION_CODES.VanillaIceCream */)
+    private boolean enableIgmpOffload() {
+        // Since the all-hosts multicast address (224.0.0.1) is always present for IPv4
+        // multicast, and IGMP packets are not needed for this address, IGMP offloading is only
+        // necessary if there are additional joined multicast addresses
+        // (mIPv4MulticastAddresses.size() > 1).
+        return enableIgmpReportsMonitor() && mIPv4MulticastAddresses.size() > 1
+                && mIPv4Address != null;
+    }
+
+    @ChecksSdkIntAtLeast(api = 35 /* Build.VERSION_CODES.VanillaIceCream */)
+    private boolean enableIpv4PingOffload() {
+        return mHandleIpv4PingOffload && useApfV6Generator() && mIPv4Address != null;
+    }
+
+    @ChecksSdkIntAtLeast(api = 35 /* Build.VERSION_CODES.VanillaIceCream */)
+    private boolean enableIpv6PingOffload() {
+        return mHandleIpv6PingOffload && useApfV6Generator()
+                && !mIPv6NonTentativeAddresses.isEmpty();
+    }
+
+    @ChecksSdkIntAtLeast(api = 35 /* Build.VERSION_CODES.VanillaIceCream */)
+    private boolean enableMldReportsMonitor() {
+        return mHandleMldOffload && useApfV6Generator();
+    }
+
+    @ChecksSdkIntAtLeast(api = 35 /* Build.VERSION_CODES.VanillaIceCream */)
+    private boolean enableMldOffload() {
+        return enableMldReportsMonitor() && mIPv6LinkLocalAddress != null
+                && !mIPv6McastAddrsExcludeAllHost.isEmpty();
+    }
+
+    @ChecksSdkIntAtLeast(api = 35 /* Build.VERSION_CODES.VanillaIceCream */)
+    private boolean useApfV6Generator() {
         return SdkLevel.isAtLeastV() && ApfV6Generator.supportsVersion(mApfVersionSupported);
     }
 
+    @ChecksSdkIntAtLeast(api = 35 /* Build.VERSION_CODES.VanillaIceCream */)
+    private boolean useApfV61Generator() {
+        return SdkLevel.isAtLeastV() && ApfV61Generator.supportsVersion(mApfVersionSupported);
+    }
+
     /**
      * Add TCP keepalive ack packet filter.
      * This will add a filter to drop acks to the keepalive packet passed as an argument.
@@ -2750,53 +4365,81 @@
         installNewProgram();
     }
 
+    /**
+     * Determines whether the APF interpreter advertises support for the data buffer access
+     * opcodes LDDW (LoaD Data Word) and STDW (STore Data Word).
+     */
+    public boolean hasDataAccess(int apfVersionSupported) {
+        return apfVersionSupported > 2;
+    }
+
     public void dump(IndentingPrintWriter pw) {
-        // TODO: use HandlerUtils.runWithScissors() to dump APF on the handler thread.
         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("Multicast: " + (mMulticastFilter ? "DROP" : "ALLOW"));
+        pw.println("ApfConfig: " + getApfConfigMessage());
         pw.println("Minimum RDNSS lifetime: " + mMinRdnssLifetimeSec);
         pw.println("Interface MAC address: " + MacAddress.fromBytes(mHardwareAddress));
-        pw.println("Multicast MAC addresses: ");
+        pw.println("Multicast MAC addresses:");
         pw.increaseIndent();
         for (byte[] addr : mDependencies.getEtherMulticastAddresses(mInterfaceParams.name)) {
             pw.println(MacAddress.fromBytes(addr));
         }
         pw.decreaseIndent();
+        if (SdkLevel.isAtLeastV()) {
+            pw.print("Hardcoded not denylisted Ethertypes:");
+            pw.println(
+                    " 0800(IPv4) 0806(ARP) 86DD(IPv6) 888E(EAPOL) 88B4(WAPI) 890D(TDLS unicast)");
+        } else {
+            pw.print("Denylisted Ethertypes:");
+            for (int p : mEthTypeBlackList) {
+                pw.print(String.format(" %04x", p));
+            }
+        }
         try {
             pw.println("IPv4 address: " + InetAddress.getByAddress(mIPv4Address).getHostAddress());
-            pw.println("IPv6 non-tentative addresses: ");
-            pw.increaseIndent();
-            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();
-        } catch (UnknownHostException|NullPointerException e) {}
+        } catch (UnknownHostException | NullPointerException e) {
+            pw.println("IPv4 address: None");
+        }
+
+        pw.println("IPv4 multicast addresses:");
+        pw.increaseIndent();
+        final List<Inet4Address> ipv4McastAddrs =
+                ProcfsParsingUtils.getIPv4MulticastAddresses(mInterfaceParams.name);
+        for (Inet4Address addr: ipv4McastAddrs) {
+            pw.println(addr.getHostAddress());
+        }
+        pw.decreaseIndent();
+        pw.println("IPv6 non-tentative addresses:");
+        pw.increaseIndent();
+        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();
 
         if (mLastTimeInstalledProgram == 0) {
             pw.println("No program installed.");
@@ -2808,29 +4451,42 @@
                 "Last program length %d, installed %ds ago, lifetime %ds",
                 mLastInstalledProgram.length, filterAgeSeconds,
                 mLastInstalledProgramMinLifetime));
-        if (SdkLevel.isAtLeastV()) {
-            pw.print("Hardcoded Allowlisted Ethertypes:");
-            pw.println(" 0800(IPv4) 0806(ARP) 86DD(IPv6) 888E(EAPOL) 88B4(WAPI)");
+        pw.println();
+        pw.println("Mdns filters:");
+        pw.increaseIndent();
+        if (mNumOfMdnsRuleToOffload == -1) {
+            pw.println("pass all mDNS packet");
         } else {
-            pw.print("Denylisted Ethertypes:");
-            for (int p : mEthTypeBlackList) {
-                pw.print(String.format(" %04x", p));
+            for (int i = 0; i < mOffloadRules.size(); ++i) {
+                final MdnsOffloadRule rule = mOffloadRules.get(i);
+                if (i >= mNumOfMdnsRuleToOffload) {
+                    pw.println(String.format("passthrough service: %s", rule.mFullServiceName));
+                } else {
+                    pw.println(String.format("offload service: %s, payloadSize: %d",
+                            rule.mFullServiceName,
+                            rule.mOffloadPayload == null ? 0 : rule.mOffloadPayload.length));
+                }
             }
         }
+        pw.decreaseIndent();
         pw.println();
         pw.println("RA filters:");
         pw.increaseIndent();
-        for (Ra ra: mRas) {
+        for (int i = 0; i < mRas.size(); ++i) {
+            if (i < mNumFilteredRas) {
+                pw.println("Filtered: ");
+            } else {
+                pw.println("Ignored: ");
+            }
+            final Ra ra = mRas.get(i);
             pw.println(ra);
             pw.increaseIndent();
             pw.println(String.format(
                     "Last seen %ds ago", secondsSinceBoot() - ra.mLastSeen));
-            if (DBG) {
-                pw.println("Last match:");
-                pw.increaseIndent();
-                pw.println(ra.getLastMatchingPacket());
-                pw.decreaseIndent();
-            }
+            pw.println("Last match:");
+            pw.increaseIndent();
+            pw.println(ra.getLastMatchingPacket());
+            pw.decreaseIndent();
             pw.decreaseIndent();
         }
         pw.decreaseIndent();
@@ -2861,14 +4517,12 @@
         }
         pw.decreaseIndent();
 
-        if (DBG) {
-            pw.println("Last program:");
-            pw.increaseIndent();
-            pw.println(HexDump.toHexString(mLastInstalledProgram, false /* lowercase */));
-            pw.decreaseIndent();
-        }
+        pw.println("Last program:");
+        pw.increaseIndent();
+        pw.println(HexDump.toHexString(mLastInstalledProgram, false /* lowercase */));
+        pw.decreaseIndent();
 
-        pw.println("APF packet counters: ");
+        pw.println("APF packet counters:");
         pw.increaseIndent();
         if (!hasDataAccess(mApfVersionSupported)) {
             pw.println("APF counters not supported");
@@ -2878,9 +4532,9 @@
             try {
                 Counter[] counters = Counter.class.getEnumConstants();
                 long counterFilterAgeSeconds =
-                        getCounterValue(mDataSnapshot, Counter.FILTER_AGE_SECONDS);
+                        getCounterValue(mDataSnapshot, FILTER_AGE_SECONDS);
                 long counterApfProgramId =
-                        getCounterValue(mDataSnapshot, Counter.APF_PROGRAM_ID);
+                        getCounterValue(mDataSnapshot, APF_PROGRAM_ID);
                 for (Counter c : Arrays.asList(counters).subList(1, counters.length)) {
                     long value = getCounterValue(mDataSnapshot, c);
 
@@ -2945,10 +4599,6 @@
             } catch (ArrayIndexOutOfBoundsException e) {
                 pw.println("Uh-oh: " + e);
             }
-            if (VDBG) {
-                pw.println("Raw data dump: ");
-                pw.println(HexDump.dumpHexString(mDataSnapshot));
-            }
         }
         pw.decreaseIndent();
     }
@@ -3010,20 +4660,6 @@
                 + (uint8(bytes[3]));
     }
 
-    private static byte[] concatArrays(final byte[]... arr) {
-        int size = 0;
-        for (byte[] a : arr) {
-            size += a.length;
-        }
-        final byte[] result = new byte[size];
-        int offset = 0;
-        for (byte[] a : arr) {
-            System.arraycopy(a, 0, result, offset, a.length);
-            offset += a.length;
-        }
-        return result;
-    }
-
     private void sendNetworkQuirkMetrics(final NetworkQuirkEvent event) {
         if (mNetworkQuirkMetrics == null) return;
         mNetworkQuirkMetrics.setEvent(event);
diff --git a/src/android/net/apf/ApfMdnsOffloadEngine.java b/src/android/net/apf/ApfMdnsOffloadEngine.java
new file mode 100644
index 0000000..3aee08f
--- /dev/null
+++ b/src/android/net/apf/ApfMdnsOffloadEngine.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2025 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.annotation.NonNull;
+import android.annotation.RequiresApi;
+import android.net.nsd.NsdManager;
+import android.net.nsd.OffloadEngine;
+import android.net.nsd.OffloadServiceInfo;
+import android.os.Build;
+import android.os.Handler;
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * APF offload engine implementation for managing mDNS offloads.
+ */
+@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+public class ApfMdnsOffloadEngine implements OffloadEngine {
+
+    private static final String TAG = ApfMdnsOffloadEngine.class.getSimpleName();
+
+    /**
+     * Callback interface for receiving notifications about offload rule updates.
+     */
+    public interface Callback {
+        /**
+         * Called when the offload rules are updated.
+         * <p>
+         * This method is called on the handler thread.
+         *
+         * @param allRules The updated list of MDNS offload rules.
+         */
+        void onOffloadRulesUpdated(@NonNull List<MdnsOffloadRule> allRules);
+    }
+
+    @NonNull
+    private final List<OffloadServiceInfo> mOffloadServiceInfos = new ArrayList<>();
+    @NonNull
+    private final String mInterfaceName;
+    @NonNull
+    private final Handler mHandler;
+    @NonNull
+    private final NsdManager mNsdManager;
+    @NonNull
+    private final Callback mCallback;
+
+    /**
+     * Constructor for ApfOffloadEngine.
+     */
+    public ApfMdnsOffloadEngine(@NonNull String interfaceName, @NonNull Handler handler,
+            @NonNull NsdManager nsdManager, @NonNull Callback callback) {
+        mInterfaceName = interfaceName;
+        mHandler = handler;
+        mNsdManager = nsdManager;
+        mCallback = callback;
+    }
+
+    @Override
+    public void onOffloadServiceUpdated(@NonNull OffloadServiceInfo info) {
+        handleOffloadServiceUpdated(info, false /* isRemoved */);
+    }
+
+    @Override
+    public void onOffloadServiceRemoved(@NonNull OffloadServiceInfo info) {
+        handleOffloadServiceUpdated(info, true /* isRemoved */);
+    }
+
+    private void handleOffloadServiceUpdated(@NonNull OffloadServiceInfo info, boolean isRemoved) {
+        if (isRemoved) {
+            mOffloadServiceInfos.removeIf(i -> i.getKey().equals(info.getKey()));
+        } else {
+            mOffloadServiceInfos.removeIf(i -> i.getKey().equals(info.getKey()));
+            mOffloadServiceInfos.add(info);
+        }
+        try {
+            List<MdnsOffloadRule> offloadRules = ApfMdnsUtils.extractOffloadReplyRule(
+                    mOffloadServiceInfos);
+            mCallback.onOffloadRulesUpdated(offloadRules);
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to extract offload reply rule", e);
+        }
+    }
+
+    /**
+     * Registers the offload engine with the NsdManager.
+     */
+    public void registerOffloadEngine() {
+        mNsdManager.registerOffloadEngine(mInterfaceName, OFFLOAD_TYPE_REPLY,
+                OFFLOAD_CAPABILITY_BYPASS_MULTICAST_LOCK, mHandler::post, this);
+    }
+
+    /**
+     * Unregisters the offload engine with the NsdManager.
+     */
+    public void unregisterOffloadEngine() {
+        mNsdManager.unregisterOffloadEngine(this);
+        mOffloadServiceInfos.clear();
+    }
+}
diff --git a/src/android/net/apf/ApfMdnsUtils.java b/src/android/net/apf/ApfMdnsUtils.java
index 7666864..4e2190e 100644
--- a/src/android/net/apf/ApfMdnsUtils.java
+++ b/src/android/net/apf/ApfMdnsUtils.java
@@ -27,6 +27,7 @@
 import android.os.Build;
 import android.util.ArraySet;
 
+import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.DnsUtils;
 
 import java.io.ByteArrayOutputStream;
@@ -56,14 +57,6 @@
         allMatchers.add(matcher);
     }
 
-    private static String[] prepend(String[] suffix, String... prefixes) {
-        String[] result = new String[prefixes.length + suffix.length];
-        System.arraycopy(prefixes, 0, result, 0, prefixes.length);
-        System.arraycopy(suffix, 0, result, prefixes.length, suffix.length);
-        return result;
-    }
-
-
     /**
      * Extract the offload rules from the list of offloadServiceInfos. The rules are returned in
      * priority order (most important first). If there are too many rules, APF could decide only
@@ -83,21 +76,18 @@
         final List<MdnsOffloadRule> rules = new ArrayList<>();
         final Set<MdnsOffloadRule.Matcher> allMatchers = new ArraySet<>();
         for (OffloadServiceInfo info : sortedOffloadServiceInfos) {
-            // Don't offload the records if the priority is not configured.
-            int priority = info.getPriority();
-            if (priority == Integer.MAX_VALUE) {
-                continue;
-            }
             List<MdnsOffloadRule.Matcher> matcherGroup = new ArrayList<>();
             final OffloadServiceInfo.Key key = info.getKey();
-            final String[] serviceTypeLabels = key.getServiceType().split("\\.", 0);
-            final String[] fullQualifiedName = prepend(serviceTypeLabels, key.getServiceName());
+            final String[] serviceTypeLabels = CollectionUtils.appendArray(String.class,
+                    key.getServiceType().split("\\.", 0), "local");
+            final String[] fullQualifiedName = CollectionUtils.prependArray(String.class,
+                    serviceTypeLabels, key.getServiceName());
             final byte[] replyPayload = info.getOffloadPayload();
             final byte[] encodedServiceType = encodeQname(serviceTypeLabels);
            // If (QTYPE == PTR) and (QNAME == mServiceName + mServiceType), then reply.
             MdnsOffloadRule.Matcher ptrMatcher = new MdnsOffloadRule.Matcher(
                     encodedServiceType,
-                    TYPE_PTR
+                    new int[] { TYPE_PTR }
             );
             addMatcherIfNotExist(allMatchers, matcherGroup, ptrMatcher);
             final List<String> subTypes = info.getSubtypes();
@@ -106,41 +96,43 @@
             boolean tooManySubtypes = subTypes.size() > MAX_SUPPORTED_SUBTYPES;
             if (tooManySubtypes) {
                 // If (QTYPE == PTR) and (QNAME == wildcard + _sub + mServiceType), then fail open.
-                final String[] serviceTypeSuffix = prepend(serviceTypeLabels, "_sub");
+                final String[] serviceTypeSuffix = CollectionUtils.prependArray(String.class,
+                        serviceTypeLabels, "_sub");
                 final ByteArrayOutputStream buf = new ByteArrayOutputStream();
                 // byte = 0xff is used as a wildcard.
                 buf.write(-1);
                 final byte[] encodedFullServiceType = encodeQname(buf, serviceTypeSuffix);
                 final MdnsOffloadRule.Matcher subtypePtrMatcher = new MdnsOffloadRule.Matcher(
-                        encodedFullServiceType, TYPE_PTR);
+                        encodedFullServiceType, new int[] { TYPE_PTR });
                 addMatcherIfNotExist(allMatchers, matcherGroup, subtypePtrMatcher);
             } else {
                 // If (QTYPE == PTR) and (QNAME == subType + _sub + mServiceType), then reply.
                 for (String subType : subTypes) {
-                    final String[] fullServiceType = prepend(serviceTypeLabels, subType, "_sub");
+                    final String[] fullServiceType = CollectionUtils.prependArray(String.class,
+                            serviceTypeLabels, subType, "_sub");
                     final byte[] encodedFullServiceType = encodeQname(fullServiceType);
                     // If (QTYPE == PTR) and (QNAME == subType + "_sub" + mServiceType), then reply.
                     final MdnsOffloadRule.Matcher subtypePtrMatcher = new MdnsOffloadRule.Matcher(
-                            encodedFullServiceType, TYPE_PTR);
+                            encodedFullServiceType, new int[] { TYPE_PTR });
                     addMatcherIfNotExist(allMatchers, matcherGroup, subtypePtrMatcher);
                 }
             }
             final byte[] encodedFullQualifiedNameQname = encodeQname(fullQualifiedName);
             // If (QTYPE == SRV) and (QNAME == mServiceName + mServiceType), then reply.
-            addMatcherIfNotExist(allMatchers, matcherGroup,
-                    new MdnsOffloadRule.Matcher(encodedFullQualifiedNameQname, TYPE_SRV));
             // If (QTYPE == TXT) and (QNAME == mServiceName + mServiceType), then reply.
             addMatcherIfNotExist(allMatchers, matcherGroup,
-                    new MdnsOffloadRule.Matcher(encodedFullQualifiedNameQname, TYPE_TXT));
+                    new MdnsOffloadRule.Matcher(encodedFullQualifiedNameQname,
+                            new int[] { TYPE_SRV, TYPE_TXT }));
             // If (QTYPE == A or AAAA) and (QNAME == mDeviceHostName), then reply.
             final String[] hostNameLabels = info.getHostname().split("\\.", 0);
             final byte[] encodedHostName = encodeQname(hostNameLabels);
             addMatcherIfNotExist(allMatchers, matcherGroup,
-                    new MdnsOffloadRule.Matcher(encodedHostName, TYPE_A));
-            addMatcherIfNotExist(allMatchers, matcherGroup,
-                    new MdnsOffloadRule.Matcher(encodedHostName, TYPE_AAAA));
+                    new MdnsOffloadRule.Matcher(encodedHostName,
+                            new int[] { TYPE_A, TYPE_AAAA }));
             if (!matcherGroup.isEmpty()) {
-                rules.add(new MdnsOffloadRule(matcherGroup, tooManySubtypes ? null : replyPayload));
+                rules.add(new MdnsOffloadRule(
+                        key.getServiceName() + "." + key.getServiceType(),
+                        matcherGroup, tooManySubtypes ? null : replyPayload));
             }
         }
         return rules;
diff --git a/src/android/net/apf/ApfV4Generator.java b/src/android/net/apf/ApfV4Generator.java
index f9918b2..fe081df 100644
--- a/src/android/net/apf/ApfV4Generator.java
+++ b/src/android/net/apf/ApfV4Generator.java
@@ -33,20 +33,8 @@
  */
 public final class ApfV4Generator extends ApfV4GeneratorBase<ApfV4Generator> {
 
-    /**
-     * Jump to this label to terminate the program, increment the counter and indicate the packet
-     * should be passed to the AP.
-     */
-    private static final String COUNT_AND_PASS_LABEL = "__COUNT_AND_PASS__";
-
-    /**
-     * Jump to this label to terminate the program, increment counter, and indicate the packet
-     * should be dropped.
-     */
-    private static final String COUNT_AND_DROP_LABEL = "__COUNT_AND_DROP__";
-
-    public final String mCountAndDropLabel;
-    public final String mCountAndPassLabel;
+    public final short mCountAndDropLabel;
+    public final short mCountAndPassLabel;
 
     /**
      * Returns true if we support the specified {@code version}, otherwise false.
@@ -65,8 +53,8 @@
             throws IllegalInstructionException {
         // make sure mVersion is not greater than 4 when using this class
         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;
+        mCountAndDropLabel = version > 2 ? getUniqueLabel() : DROP_LABEL;
+        mCountAndPassLabel = version > 2 ? getUniqueLabel() : PASS_LABEL;
     }
 
     /**
@@ -80,6 +68,11 @@
     }
 
     @Override
+    public int getBaseProgramSize() {
+        return 0;
+    }
+
+    @Override
     void addR0ArithR1(Opcodes opcode) {
         append(new Instruction(opcode, Rbit1));  // APFv2/4: R0 op= R1
     }
@@ -236,7 +229,7 @@
         if (values.isEmpty()) {
             throw new IllegalArgumentException("values cannot be empty");
         }
-        String tgt = getUniqueLabel();
+        short tgt = getUniqueLabel();
         for (Long v : values) {
             addJumpIfR0Equals(v, tgt);
         }
@@ -251,7 +244,7 @@
         if (values.isEmpty()) {
             throw new IllegalArgumentException("values cannot be empty");
         }
-        String tgt = getUniqueLabel();
+        short tgt = getUniqueLabel();
         for (Long v : values) {
             addJumpIfR0Equals(v, tgt);
         }
@@ -265,10 +258,10 @@
             throws IllegalInstructionException {
         final List<byte[]> deduplicatedList = validateDeduplicateBytesList(bytesList);
         maybeAddLoadCounterOffset(R1, cnt);
-        String matchLabel = getUniqueLabel();
-        String allNoMatchLabel = getUniqueLabel();
+        short matchLabel = getUniqueLabel();
+        short allNoMatchLabel = getUniqueLabel();
         for (byte[] v : deduplicatedList) {
-            String notMatchLabel = getUniqueLabel();
+            short notMatchLabel = getUniqueLabel();
             addJumpIfBytesAtR0NotEqual(v, notMatchLabel);
             addJump(matchLabel);
             defineLabel(notMatchLabel);
@@ -356,6 +349,18 @@
         return maybeAddLoadCounterOffset(register.other(), counter).addStoreData(register, 0);
     }
 
+    @Override
+    public ApfV4Generator addAdd(long val) {
+        if (val == 0) return self();  // nop, as APFv6 would '+= R1'
+        return append(new Instruction(Opcodes.ADD).addTwosCompUnsigned(val));
+    }
+
+    @Override
+    public ApfV4Generator addAnd(long val) {
+        if (val == 0) return addLoadImmediate(R0, 0);  // equivalent, as APFv6 would '+= R1'
+        return append(new Instruction(Opcodes.AND).addTwosCompUnsigned(val));
+    }
+
     /**
      * Append the count & (pass|drop) trampoline, which increments the counter at the data address
      * pointed to by R1, then jumps to the (pass|drop) label. This saves a few bytes over inserting
@@ -367,18 +372,34 @@
     @Override
     public ApfV4Generator addCountTrampoline() throws IllegalInstructionException {
         if (mVersion <= 2) return self();
-        return defineLabel(COUNT_AND_PASS_LABEL)
+        return defineLabel(mCountAndPassLabel)
                 .addLoadData(R0, 0)  // R0 = *(R1 + 0)
                 .addAdd(1)           // R0++
                 .addStoreData(R0, 0) // *(R1 + 0) = R0
                 .addJump(PASS_LABEL)
-                .defineLabel(COUNT_AND_DROP_LABEL)
+                .defineLabel(mCountAndDropLabel)
                 .addLoadData(R0, 0)  // R0 = *(R1 + 0)
                 .addAdd(1)           // R0++
                 .addStoreData(R0, 0) // *(R1 + 0) = R0
                 .addJump(DROP_LABEL);
     }
 
+    @Override
+    public int getDefaultPacketHandlingSizeOverEstimate() {
+        // addCountAndPass(PASSED_IPV6_ICMP); -> 7 bytes
+        // defineLabel(mCountAndPassLabel)
+        // .addLoadData(R0, 0) ->  1 bytes
+        // .addAdd(1) -> 2 bytes
+        // .addStoreData(R0, 0) -> 1 bytes
+        // .addJump(PASS_LABEL) -> 5 bytes
+        // .defineLabel(mCountAndDropLabel)
+        // .addLoadData(R0, 0) -> 1 bytes
+        // .addAdd(1) -> 2 bytes
+        // .addStoreData(R0, 0) -> 1 bytes
+        // .addJump(DROP_LABEL); -> 5 bytes
+        return 25;
+    }
+
     /**
      * This function is no-op in APFv4
      */
diff --git a/src/android/net/apf/ApfV4GeneratorBase.java b/src/android/net/apf/ApfV4GeneratorBase.java
index a00aa2f..d750229 100644
--- a/src/android/net/apf/ApfV4GeneratorBase.java
+++ b/src/android/net/apf/ApfV4GeneratorBase.java
@@ -16,6 +16,10 @@
 
 package android.net.apf;
 
+import static android.net.apf.ApfConstants.IPV4_FRAGMENT_MORE_FRAGS_MASK;
+import static android.net.apf.ApfConstants.IPV4_FRAGMENT_OFFSET_MASK;
+import static android.net.apf.ApfConstants.IPV4_FRAGMENT_OFFSET_OFFSET;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_ICMP;
 import static android.net.apf.BaseApfGenerator.Rbit.Rbit0;
 import static android.net.apf.BaseApfGenerator.Register.R0;
 import static android.net.apf.BaseApfGenerator.Register.R1;
@@ -85,14 +89,14 @@
      * </pre>
      * In this case "next_filter" may not have any generated code associated with it.
      */
-    public final Type defineLabel(String name) throws IllegalInstructionException {
+    public final Type defineLabel(short name) throws IllegalInstructionException {
         return append(new Instruction(Opcodes.LABEL).setLabel(name));
     }
 
     /**
      * Add an unconditional jump instruction to the end of the program.
      */
-    public final Type addJump(String target) {
+    public final Type addJump(short target) {
         return append(new Instruction(Opcodes.JMP).setTargetLabel(target));
     }
 
@@ -107,24 +111,24 @@
      * Add an instruction to the end of the program to load the byte at offset {@code offset}
      * bytes from the beginning of the packet into {@code register}.
      */
-    public final Type addLoad8(Register r, int ofs) {
-        return append(new Instruction(Opcodes.LDB, r).addPacketOffset(ofs));
+    public final Type addLoad8intoR0(int ofs) {
+        return append(new Instruction(Opcodes.LDB, R0).addPacketOffset(ofs));
     }
 
     /**
      * Add an instruction to the end of the program to load 16-bits at offset {@code offset}
      * bytes from the beginning of the packet into {@code register}.
      */
-    public final Type addLoad16(Register r, int ofs) {
-        return append(new Instruction(Opcodes.LDH, r).addPacketOffset(ofs));
+    public final Type addLoad16intoR0(int ofs) {
+        return append(new Instruction(Opcodes.LDH, R0).addPacketOffset(ofs));
     }
 
     /**
      * Add an instruction to the end of the program to load 32-bits at offset {@code offset}
      * bytes from the beginning of the packet into {@code register}.
      */
-    public final Type addLoad32(Register r, int ofs) {
-        return append(new Instruction(Opcodes.LDW, r).addPacketOffset(ofs));
+    public final Type addLoad32intoR0(int ofs) {
+        return append(new Instruction(Opcodes.LDW, R0).addPacketOffset(ofs));
     }
 
     /**
@@ -132,8 +136,8 @@
      * {@code register}. The offset of the loaded byte from the beginning of the packet is
      * the sum of {@code offset} and the value in register R1.
      */
-    public final Type addLoad8Indexed(Register r, int ofs) {
-        return append(new Instruction(Opcodes.LDBX, r).addTwosCompUnsigned(ofs));
+    public final Type addLoad8R1IndexedIntoR0(int ofs) {
+        return append(new Instruction(Opcodes.LDBX, R0).addTwosCompUnsigned(ofs));
     }
 
     /**
@@ -141,8 +145,8 @@
      * {@code register}. The offset of the loaded 16-bits from the beginning of the packet is
      * the sum of {@code offset} and the value in register R1.
      */
-    public final Type addLoad16Indexed(Register r, int ofs) {
-        return append(new Instruction(Opcodes.LDHX, r).addTwosCompUnsigned(ofs));
+    public final Type addLoad16R1IndexedIntoR0(int ofs) {
+        return append(new Instruction(Opcodes.LDHX, R0).addTwosCompUnsigned(ofs));
     }
 
     /**
@@ -150,17 +154,14 @@
      * {@code register}. The offset of the loaded 32-bits from the beginning of the packet is
      * the sum of {@code offset} and the value in register R1.
      */
-    public final Type addLoad32Indexed(Register r, int ofs) {
-        return append(new Instruction(Opcodes.LDWX, r).addTwosCompUnsigned(ofs));
+    public final Type addLoad32R1IndexedIntoR0(int ofs) {
+        return append(new Instruction(Opcodes.LDWX, R0).addTwosCompUnsigned(ofs));
     }
 
     /**
      * Add an instruction to the end of the program to add {@code value} to register R0.
      */
-    public final Type addAdd(long val) {
-        if (val == 0) return self();  // nop, as APFv6 would '+= R1'
-        return append(new Instruction(Opcodes.ADD).addTwosCompUnsigned(val));
-    }
+    public abstract Type addAdd(long val);
 
     /**
      * Add an instruction to the end of the program to subtract {@code value} from register R0.
@@ -188,10 +189,7 @@
     /**
      * Add an instruction to the end of the program to logically and register R0 with {@code value}.
      */
-    public final Type addAnd(long val) {
-        if (val == 0) return addLoadImmediate(R0, 0);  // equivalent, as APFv6 would '+= R1'
-        return append(new Instruction(Opcodes.AND).addTwosCompUnsigned(val));
-    }
+    public abstract Type addAnd(long val);
 
     /**
      * Add an instruction to the end of the program to logically or register R0 with {@code value}.
@@ -284,7 +282,7 @@
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value equals {@code value}.
      */
-    public final Type addJumpIfR0Equals(long val, String tgt) {
+    public final Type addJumpIfR0Equals(long val, short tgt) {
         return append(new Instruction(Opcodes.JEQ).addTwosCompUnsigned(val).setTargetLabel(tgt));
     }
 
@@ -308,7 +306,7 @@
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value does not equal {@code value}.
      */
-    public final Type addJumpIfR0NotEquals(long val, String tgt) {
+    public final Type addJumpIfR0NotEquals(long val, short tgt) {
         return append(new Instruction(Opcodes.JNE).addTwosCompUnsigned(val).setTargetLabel(tgt));
     }
 
@@ -332,7 +330,7 @@
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value is greater than {@code value}.
      */
-    public final Type addJumpIfR0GreaterThan(long val, String tgt) {
+    public final Type addJumpIfR0GreaterThan(long val, short tgt) {
         return append(new Instruction(Opcodes.JGT).addUnsigned(val).setTargetLabel(tgt));
     }
 
@@ -356,7 +354,7 @@
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value is less than {@code value}.
      */
-    public final Type addJumpIfR0LessThan(long val, String tgt) {
+    public final Type addJumpIfR0LessThan(long val, short tgt) {
         return append(new Instruction(Opcodes.JLT).addUnsigned(val).setTargetLabel(tgt));
     }
 
@@ -380,7 +378,7 @@
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value has any bits set that are also set in {@code value}.
      */
-    public final Type addJumpIfR0AnyBitsSet(long val, String tgt) {
+    public final Type addJumpIfR0AnyBitsSet(long val, short tgt) {
         return append(new Instruction(Opcodes.JSET).addTwosCompUnsigned(val).setTargetLabel(tgt));
     }
 
@@ -436,7 +434,7 @@
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value equals register R1's value.
      */
-    public final Type addJumpIfR0EqualsR1(String tgt) {
+    public final Type addJumpIfR0EqualsR1(short tgt) {
         return append(new Instruction(Opcodes.JEQ, R1).setTargetLabel(tgt));
     }
 
@@ -444,7 +442,7 @@
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value does not equal register R1's value.
      */
-    public final Type addJumpIfR0NotEqualsR1(String tgt) {
+    public final Type addJumpIfR0NotEqualsR1(short tgt) {
         return append(new Instruction(Opcodes.JNE, R1).setTargetLabel(tgt));
     }
 
@@ -452,7 +450,7 @@
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value is greater than register R1's value.
      */
-    public final Type addJumpIfR0GreaterThanR1(String tgt) {
+    public final Type addJumpIfR0GreaterThanR1(short tgt) {
         return append(new Instruction(Opcodes.JGT, R1).setTargetLabel(tgt));
     }
 
@@ -460,7 +458,7 @@
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value is less than register R1's value.
      */
-    public final Type addJumpIfR0LessThanR1(String target) {
+    public final Type addJumpIfR0LessThanR1(short target) {
         return append(new Instruction(Opcodes.JLT, R1).setTargetLabel(target));
     }
 
@@ -468,7 +466,7 @@
      * Add an instruction to the end of the program to jump to {@code target} if register R0's
      * value has any bits set that are also set in R1's value.
      */
-    public final Type addJumpIfR0AnyBitsSetR1(String tgt) {
+    public final Type addJumpIfR0AnyBitsSetR1(short tgt) {
         return append(new Instruction(Opcodes.JSET, R1).setTargetLabel(tgt));
     }
 
@@ -477,13 +475,24 @@
      * packet at an offset specified by register0 don't match {@code bytes}.
      * R=0 means check for not equal.
      */
-    public final Type addJumpIfBytesAtR0NotEqual(@NonNull byte[] bytes, String tgt) {
+    public final Type addJumpIfBytesAtR0NotEqual(@NonNull byte[] bytes, short tgt) {
         validateBytes(bytes);
         return append(new Instruction(Opcodes.JBSMATCH).addUnsigned(
                 bytes.length).setTargetLabel(tgt).setBytesImm(bytes));
     }
 
     /**
+     * Add an instruction to the end of the program to jump to {@code tgt} if the bytes of the
+     * packet at an offset specified by {@code offset} don't match {@code bytes}.
+     * This method needs to be non-final because APFv4 and APFv6 share the same implementation,
+     * but in APFv6.1, this method will be overridden to use the JBSPTRMATCH instruction.
+     */
+    public Type addJumpIfBytesAtOffsetNotEqual(int offset, @NonNull byte[] bytes, short tgt)
+            throws IllegalInstructionException {
+        return addLoadImmediate(R0, offset).addJumpIfBytesAtR0NotEqual(bytes, tgt);
+    }
+
+    /**
      * Add instructions to the end of the program to increase counter and drop packet if the
      * bytes of the packet at an offset specified by register0 don't match {@code bytes}.
      * WARNING: may modify R1
@@ -501,12 +510,56 @@
 
     /**
      * Add instructions to the end of the program to increase counter and drop packet if the
+     * bytes of the packet at an offset specified by {@code offset} don't match {@code bytes}.
+     * This method needs to be non-final because APFv4 and APFv6 share the same implementation,
+     * but in APFv6.1, this method will be overridden to use the JBSPTRMATCH instruction.
+     */
+    public Type addCountAndDropIfBytesAtOffsetNotEqual(int offset, byte[] bytes,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addLoadImmediate(R0, offset).addCountAndDropIfBytesAtR0NotEqual(bytes, cnt);
+    }
+
+    /**
+     * Add instructions to the end of the program to increase counter and pass packet if the
+     * bytes of the packet at an offset specified by {@code offset} don't match {@code bytes}.
+     * This method needs to be non-final because APFv4 and APFv6 share the same implementation,
+     * but in APFv6.1, this method will be overridden to use the JBSPTRMATCH instruction.
+     */
+    public Type addCountAndPassIfBytesAtOffsetNotEqual(int offset, byte[] bytes,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addLoadImmediate(R0, offset).addCountAndPassIfBytesAtR0NotEqual(bytes, cnt);
+    }
+
+    /**
+     * Add instructions to the end of the program to increase counter and drop packet if the
+     * bytes of the packet at an offset specified by {@code offset} does match {@code bytes}.
+     * This method needs to be non-final because APFv4 and APFv6 share the same implementation,
+     * but in APFv6.1, this method will be overridden to use the JBSPTRMATCH instruction.
+     */
+    public Type addCountAndDropIfBytesAtOffsetEqual(int offset, byte[] bytes,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addLoadImmediate(R0, offset).addCountAndDropIfBytesAtR0Equal(bytes, cnt);
+    }
+
+    /**
+     * Add instructions to the end of the program to increase counter and pass packet if the
+     * bytes of the packet at an offset specified by {@code offset} does match {@code bytes}.
+     * This method needs to be non-final because APFv4 and APFv6 share the same implementation,
+     * but in APFv6.1, this method will be overridden to use the JBSPTRMATCH instruction.
+     */
+    public Type addCountAndPassIfBytesAtOffsetEqual(int offset, byte[] bytes,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addLoadImmediate(R0, offset).addCountAndPassIfBytesAtR0Equal(bytes, cnt);
+    }
+
+    /**
+     * Add instructions to the end of the program to increase counter and drop packet if the
      * bytes of the packet at an offset specified by register0 match {@code bytes}.
      * WARNING: may modify R1
      */
     public final Type addCountAndDropIfBytesAtR0Equal(byte[] bytes,
             ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
-        final String tgt = getUniqueLabel();
+        final short tgt = getUniqueLabel();
         return addJumpIfBytesAtR0NotEqual(bytes, tgt).addCountAndDrop(cnt).defineLabel(tgt);
     }
 
@@ -518,7 +571,7 @@
      */
     public final Type addCountAndPassIfBytesAtR0Equal(byte[] bytes,
             ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
-        final String tgt = getUniqueLabel();
+        final short tgt = getUniqueLabel();
         return addJumpIfBytesAtR0NotEqual(bytes, tgt).addCountAndPass(cnt).defineLabel(tgt);
     }
 
@@ -573,6 +626,24 @@
     }
 
     /**
+     * Add instructions to the end of the program to check whether the packet is an unfragmented
+     * IPv4 packet with the specified protocol, and jump to the target if it is not.
+     * WARNING: this helper method will modify R0
+     */
+    public Type addJumpIfNotUnfragmentedIPv4Protocol(long protocol, short tgt) {
+        // Mask out all but the reserved and don't fragment bits, plus the TTL field.
+        // Because:
+        //   IPV4_FRAGMENT_OFFSET_MASK = 0x1fff
+        //   IPV4_FRAGMENT_MORE_FRAGS_MASK = 0x2000
+        // hence this constant ends up being 0x3FFF00FF.
+        // We want the more flag bit and offset to be 0 (ie. not a fragment),
+        // so after this masking we end up with just the ip protocol.
+        return addLoad32intoR0(IPV4_FRAGMENT_OFFSET_OFFSET)
+                .addAnd((IPV4_FRAGMENT_MORE_FRAGS_MASK | IPV4_FRAGMENT_OFFSET_MASK) << 16 | 0xFF)
+                .addJumpIfR0NotEquals(protocol, tgt);
+    }
+
+    /**
      * Add an instruction to the end of the program to logically not {@code register}.
      */
     public final Type addNot(Register r) {
@@ -664,5 +735,30 @@
      * @throws IllegalInstructionException
      */
     public abstract Type addCountTrampoline() throws IllegalInstructionException;
+
+    /**
+     * Returns the base program size when the interpreter is initialized.
+     */
+    public abstract int getBaseProgramSize();
+
+    /**
+     * Appends default packet handling and counting to the APF program.
+     * This method adds logic to:
+     * 1. Increment the {@code PASSED_IPV6_ICMP} counter and pass the packet.
+     * 3. Add trampoline logic for counter processing.
+     *
+     *
+     * @return The type resulting from the appended instructions.
+     * @throws IllegalInstructionException If an error occurs while adding instructions.
+     */
+    public final Type addDefaultPacketHandling() throws IllegalInstructionException {
+        addCountAndPass(PASSED_IPV6_ICMP);
+        return addCountTrampoline();
+    }
+
+    /**
+     * Returns an overestimate of the size of the default packet handling logic.
+     */
+    public abstract int getDefaultPacketHandlingSizeOverEstimate();
 }
 
diff --git a/src/android/net/apf/ApfV61Generator.java b/src/android/net/apf/ApfV61Generator.java
new file mode 100644
index 0000000..465596a
--- /dev/null
+++ b/src/android/net/apf/ApfV61Generator.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2025 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;
+
+/**
+ * APFv6.1 assembler/generator. A tool for generating an APFv6.1 program.
+ *
+ * @hide
+ */
+public final class ApfV61Generator extends ApfV61GeneratorBase<ApfV61Generator> {
+    /**
+     * Returns true if we support the specified {@code version}, otherwise false.
+     */
+    public static boolean supportsVersion(int version) {
+        return version >= APF_VERSION_61;
+    }
+
+    /**
+     * Creates an ApfV61Generator instance.
+     */
+    public ApfV61Generator(int version, int ramSize, int clampSize)
+            throws IllegalInstructionException {
+        super(new byte[0], version, ramSize, clampSize);
+    }
+}
diff --git a/src/android/net/apf/ApfV61GeneratorBase.java b/src/android/net/apf/ApfV61GeneratorBase.java
new file mode 100644
index 0000000..c60de72
--- /dev/null
+++ b/src/android/net/apf/ApfV61GeneratorBase.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net.apf;
+
+import static android.net.apf.BaseApfGenerator.Rbit.Rbit0;
+import static android.net.apf.BaseApfGenerator.Rbit.Rbit1;
+import static android.net.apf.BaseApfGenerator.Register.R0;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * The abstract class for APFv6.1 assembler/generator.
+ *
+ * @param <Type> the generator class
+ *
+ * @hide
+ */
+public abstract class ApfV61GeneratorBase<Type extends ApfV61GeneratorBase<Type>> extends
+            ApfV6GeneratorBase<Type> {
+
+    /**
+     * Creates an ApfV61GeneratorBase instance.
+     */
+    public ApfV61GeneratorBase(byte[] bytes, int version, int ramSize, int clampSize)
+            throws IllegalInstructionException {
+        super(bytes, version, ramSize, clampSize);
+    }
+
+    @Override
+    public final Type addCountAndDropIfR0Equals(long val, ApfCounterTracker.Counter cnt) {
+        return addJumpIfR0Equals(val, cnt.getJumpDropLabel());
+    }
+
+    @Override
+    public final Type addCountAndPassIfR0Equals(long val, ApfCounterTracker.Counter cnt) {
+        return addJumpIfR0Equals(val, cnt.getJumpPassLabel());
+    }
+
+    @Override
+    public final Type addCountAndDropIfR0NotEquals(long val, ApfCounterTracker.Counter cnt) {
+        return addJumpIfR0NotEquals(val, cnt.getJumpDropLabel());
+    }
+
+    @Override
+    public final Type addCountAndPassIfR0NotEquals(long val, ApfCounterTracker.Counter cnt) {
+        return addJumpIfR0NotEquals(val, cnt.getJumpPassLabel());
+    }
+
+    @Override
+    public Type addCountAndDropIfR0AnyBitsSet(long val, ApfCounterTracker.Counter cnt) {
+        return addJumpIfR0AnyBitsSet(val, cnt.getJumpDropLabel());
+    }
+
+    @Override
+    public Type addCountAndPassIfR0AnyBitsSet(long val, ApfCounterTracker.Counter cnt) {
+        return addJumpIfR0AnyBitsSet(val, cnt.getJumpPassLabel());
+    }
+
+    @Override
+    public final Type addCountAndDropIfR0LessThan(long val, ApfCounterTracker.Counter cnt) {
+        if (val <= 0) {
+            throw new IllegalArgumentException("val must > 0, current val: " + val);
+        }
+        return addJumpIfR0LessThan(val, cnt.getJumpDropLabel());
+    }
+
+    @Override
+    public final Type addCountAndPassIfR0LessThan(long val, ApfCounterTracker.Counter cnt) {
+        if (val <= 0) {
+            throw new IllegalArgumentException("val must > 0, current val: " + val);
+        }
+        return addJumpIfR0LessThan(val, cnt.getJumpPassLabel());
+    }
+
+    @Override
+    public Type addCountAndDropIfR0GreaterThan(long val, ApfCounterTracker.Counter cnt) {
+        if (val < 0 || val >= 4294967295L) {
+            throw new IllegalArgumentException("val must >= 0 and < 2^32-1, current val: " + val);
+        }
+        return addJumpIfR0GreaterThan(val, cnt.getJumpDropLabel());
+    }
+
+    @Override
+    public Type addCountAndPassIfR0GreaterThan(long val, ApfCounterTracker.Counter cnt) {
+        if (val < 0 || val >= 4294967295L) {
+            throw new IllegalArgumentException("val must >= 0 and < 2^32-1, current val: " + val);
+        }
+        return addJumpIfR0GreaterThan(val, cnt.getJumpPassLabel());
+    }
+
+    @Override
+    public final Type addCountAndDropIfBytesAtR0NotEqual(byte[] bytes,
+            ApfCounterTracker.Counter cnt) {
+        return addJumpIfBytesAtR0NotEqual(bytes, cnt.getJumpDropLabel());
+    }
+
+    @Override
+    public final Type addCountAndPassIfBytesAtR0NotEqual(byte[] bytes,
+            ApfCounterTracker.Counter cnt) {
+        return addJumpIfBytesAtR0NotEqual(bytes, cnt.getJumpPassLabel());
+    }
+
+    @Override
+    public Type addCountAndPassIfR0IsOneOf(@NonNull Set<Long> values,
+            ApfCounterTracker.Counter cnt) {
+        if (values.isEmpty()) {
+            throw new IllegalArgumentException("values cannot be empty");
+        }
+        if (values.size() == 1) {
+            return addCountAndPassIfR0Equals(values.iterator().next(), cnt);
+        }
+        return addJumpIfOneOf(R0, values, cnt.getJumpPassLabel());
+    }
+
+    @Override
+    public Type addCountAndDropIfR0IsOneOf(@NonNull Set<Long> values,
+            ApfCounterTracker.Counter cnt) {
+        if (values.isEmpty()) {
+            throw new IllegalArgumentException("values cannot be empty");
+        }
+        if (values.size() == 1) {
+            return addCountAndDropIfR0Equals(values.iterator().next(), cnt);
+        }
+        return addJumpIfOneOf(R0, values, cnt.getJumpDropLabel());
+    }
+
+    @Override
+    public Type addCountAndPassIfR0IsNoneOf(@NonNull Set<Long> values,
+            ApfCounterTracker.Counter cnt) {
+        if (values.isEmpty()) {
+            throw new IllegalArgumentException("values cannot be empty");
+        }
+        if (values.size() == 1) {
+            return addCountAndPassIfR0NotEquals(values.iterator().next(), cnt);
+        }
+        return addJumpIfNoneOf(R0, values, cnt.getJumpPassLabel());
+    }
+
+    @Override
+    public Type addCountAndDropIfBytesAtR0EqualsAnyOf(@NonNull List<byte[]> bytesList,
+            ApfCounterTracker.Counter cnt) {
+        return addJumpIfBytesAtR0EqualsAnyOf(bytesList, cnt.getJumpDropLabel());
+    }
+
+    @Override
+    public Type addCountAndPassIfBytesAtR0EqualsAnyOf(@NonNull List<byte[]> bytesList,
+            ApfCounterTracker.Counter cnt) {
+        return addJumpIfBytesAtR0EqualsAnyOf(bytesList, cnt.getJumpPassLabel());
+    }
+
+    @Override
+    public Type addCountAndDropIfBytesAtR0EqualsNoneOf(@NonNull List<byte[]> bytesList,
+            ApfCounterTracker.Counter cnt) {
+        return addJumpIfBytesAtR0EqualsNoneOf(bytesList, cnt.getJumpDropLabel());
+    }
+
+    @Override
+    public Type addCountAndPassIfBytesAtR0EqualsNoneOf(@NonNull List<byte[]> bytesList,
+            ApfCounterTracker.Counter cnt) {
+        return addJumpIfBytesAtR0EqualsNoneOf(bytesList, cnt.getJumpPassLabel());
+    }
+
+    @Override
+    public Type addCountAndDropIfR0IsNoneOf(@NonNull Set<Long> values,
+            ApfCounterTracker.Counter cnt) {
+        if (values.isEmpty()) {
+            throw new IllegalArgumentException("values cannot be empty");
+        }
+        if (values.size() == 1) {
+            return addCountAndDropIfR0NotEquals(values.iterator().next(), cnt);
+        }
+        return addJumpIfNoneOf(R0, values, cnt.getJumpDropLabel());
+    }
+
+    @Override
+    public final Type addJumpIfPktAtR0ContainDnsQ(byte[] qnames, int[] qtypes, short tgt) {
+        for (int i = 0; i < qtypes.length; i += 2) {
+            if (i == qtypes.length - 1) {
+                addJumpIfPktAtR0ContainDnsQ(qnames, qtypes[i], tgt);
+            } else {
+                addJumpIfPktAtR0ContainDnsQ2(qnames, qtypes[i], qtypes[i + 1], tgt);
+            }
+        }
+        return self();
+    }
+
+    @Override
+    public Type addAllocate(int size) {
+        final int imm = (size > 266) ? (size - 266 + 7) / 8 : 0;
+        return append(new Instruction(Opcodes.ALLOC_XMIT, Rbit1).addUnsigned(imm));
+    }
+
+    @Override
+    public Type addTransmitWithoutChecksum() {
+        return append(new Instruction(Opcodes.ALLOC_XMIT, Rbit0));
+    }
+
+    @Override
+    protected boolean handleOptimizedTransmit(int ipOfs, int csumOfs, int csumStart,
+                                              int partialCsum, boolean isUdp) {
+        if (ipOfs != 14) return false;
+        int v = -1;
+        if ( isUdp && csumStart == 26 && csumOfs == 40) v = 0;  // ether/ipv4/udp
+        if (!isUdp && csumStart == 26 && csumOfs == 44) v = 1;  // ether/ipv4/tcp
+        if (!isUdp && csumStart == 34 && csumOfs == 36) v = 2;  // ether/ipv4/icmp
+        if (!isUdp && csumStart == 38 && csumOfs == 40) v = 3;  // ether/ipv4/routeralert/icmp
+        if ( isUdp && csumStart == 22 && csumOfs == 60) v = 4;  // ether/ipv6/udp
+        if (!isUdp && csumStart == 22 && csumOfs == 64) v = 5;  // ether/ipv6/tcp
+        if (!isUdp && csumStart == 22 && csumOfs == 56) v = 6;  // ether/ipv6/icmp
+        if (!isUdp && csumStart == 22 && csumOfs == 64) v = 7;  // ether/ipv6/routeralert/icmp
+        if (v < 0) return false;
+        v |= partialCsum << 3;
+        append(new Instruction(Opcodes.ALLOC_XMIT, Rbit0).addUnsigned(v));
+        return true;
+    }
+
+    private List<byte[]> addJumpIfBytesAtOffsetEqualsHelper(int offset,
+            @NonNull List<byte[]> bytesList, short tgt, boolean jumpOnMatch)
+            throws IllegalInstructionException {
+        final List<byte[]> deduplicatedList =
+                bytesList.size() == 1 ? bytesList : validateDeduplicateBytesList(bytesList);
+        if (offset < 0 || offset > 255) {
+            return deduplicatedList;
+        }
+        final int count = deduplicatedList.size();
+        final int compareLength = deduplicatedList.get(0).length;
+        if (compareLength > 16) {
+            return deduplicatedList;
+        }
+        final List<byte[]> failbackList = new ArrayList<>();
+        final List<Integer> ptrs = new ArrayList<>();
+        for (int i = 0; i < count; ++i) {
+            final byte[] bytes = deduplicatedList.get(i);
+            int relativeOffset = mInstructions.get(0).findMatchInDataBytes(bytes, 0, bytes.length);
+            if (relativeOffset < 0 || relativeOffset % 2 == 1 || relativeOffset > 510) {
+                failbackList.add(bytes);
+                continue;
+            }
+            ptrs.add(relativeOffset / 2);
+        }
+        final Rbit rbit = jumpOnMatch ? Rbit1 : Rbit0;
+        int totalPtrs = ptrs.size();
+        for (int i = 0; i < totalPtrs; i += 16) {
+            final int currentCount = Math.min(totalPtrs - i, 16);
+            final Instruction instruction = new Instruction(Opcodes.JBSPTRMATCH, rbit)
+                    .addU8(offset)
+                    .addU8((currentCount - 1) * 16 + (compareLength - 1))
+                    .setTargetLabel(tgt);
+            for (int j = 0; j < currentCount; j++) {
+                instruction.addU8(ptrs.get(i + j));
+            }
+            append(instruction);
+        }
+        return failbackList;
+    }
+
+    /**
+     * Add an instruction to the end of the program to jump to {@code tgt} if the bytes of the
+     * packet at an offset specified by {@code offset} match any of the elements in
+     * {@code bytesList}.
+     */
+    public Type addJumpIfBytesAtOffsetEqualsAnyOf(int offset, @NonNull List<byte[]> bytesList,
+            short tgt) throws IllegalInstructionException {
+        final List<byte[]> failbackList = addJumpIfBytesAtOffsetEqualsHelper(offset, bytesList, tgt,
+                true /* jumpOnMatch */);
+        if (failbackList.isEmpty()) {
+            return self();
+        }
+        return addLoadImmediate(R0, offset).addJumpIfBytesAtR0EqualsAnyOf(failbackList, tgt);
+    }
+
+    /**
+     * Add an instruction to the end of the program to jump to {@code tgt} if the bytes of the
+     * packet at an offset specified by {@code offset} match none of the elements in
+     * {@code bytesList}.
+     */
+    public Type addJumpIfBytesAtOffsetEqualsNoneOf(int offset, @NonNull List<byte[]> bytesList,
+            short tgt) throws IllegalInstructionException {
+        final List<byte[]> failbackList = addJumpIfBytesAtOffsetEqualsHelper(offset, bytesList, tgt,
+                false /* jumpOnMatch */);
+        if (failbackList.isEmpty()) {
+            return self();
+        }
+        return addLoadImmediate(R0, offset).addJumpIfBytesAtR0EqualsNoneOf(failbackList, tgt);
+    }
+
+    @Override
+    public Type addCountAndDropIfBytesAtOffsetEqualsAnyOf(int offset, List<byte[]> bytesList,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addJumpIfBytesAtOffsetEqualsAnyOf(offset, bytesList, cnt.getJumpDropLabel());
+    }
+
+    @Override
+    public Type addCountAndPassIfBytesAtOffsetEqualsAnyOf(int offset, List<byte[]> bytesList,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addJumpIfBytesAtOffsetEqualsAnyOf(offset, bytesList, cnt.getJumpPassLabel());
+    }
+
+    @Override
+    public Type addCountAndDropIfBytesAtOffsetEqualsNoneOf(int offset, List<byte[]> bytesList,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addJumpIfBytesAtOffsetEqualsNoneOf(offset, bytesList, cnt.getJumpDropLabel());
+    }
+
+    @Override
+    public Type addCountAndPassIfBytesAtOffsetEqualsNoneOf(int offset, List<byte[]> bytesList,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addJumpIfBytesAtOffsetEqualsNoneOf(offset, bytesList, cnt.getJumpPassLabel());
+    }
+
+    @Override
+    public Type addCountAndPassIfBytesAtOffsetNotEqual(int offset, byte[] bytes,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addJumpIfBytesAtOffsetEqualsNoneOf(offset, List.of(bytes), cnt.getJumpPassLabel());
+    }
+
+    @Override
+    public Type addCountAndDropIfBytesAtOffsetNotEqual(int offset, byte[] bytes,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addJumpIfBytesAtOffsetEqualsNoneOf(offset, List.of(bytes), cnt.getJumpDropLabel());
+    }
+
+    @Override
+    public Type addCountAndPassIfBytesAtOffsetEqual(int offset, byte[] bytes,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addJumpIfBytesAtOffsetEqualsAnyOf(offset, List.of(bytes), cnt.getJumpPassLabel());
+    }
+
+    @Override
+    public Type addCountAndDropIfBytesAtOffsetEqual(int offset, byte[] bytes,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        return addJumpIfBytesAtOffsetEqualsAnyOf(offset, List.of(bytes), cnt.getJumpDropLabel());
+    }
+
+    @Override
+    public Type addJumpIfBytesAtOffsetNotEqual(int offset, @NonNull byte[] bytes, short tgt)
+            throws IllegalInstructionException {
+        return addJumpIfBytesAtOffsetEqualsNoneOf(offset, List.of(bytes), tgt);
+    }
+
+    /**
+     * Appends a conditional jump instruction to the program: Jumps to {@code tgt} if the UDP
+     * payload's DNS questions contain the QNAMEs specified in {@code qnames} and qtype
+     * equals {@code qtype1} or {@code qtype2}. Examines the payload starting at the offset in R0.
+     * Drops packets if packets are corrupted.
+     */
+    public final Type addJumpIfPktAtR0ContainDnsQ2(@android.annotation.NonNull byte[] qnames,
+            int qtype1, int qtype2, short tgt) {
+        validateNames(qnames);
+        return append(new Instruction(ExtendedOpcodes.JDNSQMATCH2, Rbit1).setTargetLabel(tgt)
+                .addU8(qtype1).addU8(qtype2).setBytesImm(qnames));
+    }
+
+    /**
+     * Preload the content of the data region.
+     */
+    public Type addPreloadData(@NonNull byte[] data) throws IllegalInstructionException {
+        mInstructions.get(0).maybeUpdateBytesImm(data, 0, data.length);
+        return self();
+    }
+}
diff --git a/src/android/net/apf/ApfV6Generator.java b/src/android/net/apf/ApfV6Generator.java
index f943bed..07bd191 100644
--- a/src/android/net/apf/ApfV6Generator.java
+++ b/src/android/net/apf/ApfV6Generator.java
@@ -15,9 +15,15 @@
  */
 package android.net.apf;
 
+import static android.net.apf.BaseApfGenerator.Rbit.Rbit1;
+import static android.net.apf.BaseApfGenerator.Register.R0;
+
+import android.annotation.NonNull;
+
 import com.android.internal.annotations.VisibleForTesting;
 
-import java.util.Objects;
+import java.util.List;
+import java.util.Set;
 
 /**
  * APFv6 assembler/generator. A tool for generating an APFv6 program.
@@ -40,12 +46,6 @@
         this(new byte[0], version, ramSize, clampSize);
     }
 
-    @Override
-    void updateExceptionBufferSize(int programSize) throws IllegalInstructionException {
-        mInstructions.get(1).updateExceptionBufferSize(
-                mRamSize - ApfCounterTracker.Counter.totalSize() - programSize);
-    }
-
     /**
      * Creates an ApfV6Generator instance which emits instructions APFv6.
      * Initializes the data region with {@code bytes}.
@@ -53,9 +53,261 @@
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     public ApfV6Generator(byte[] bytes, int version, int ramSize, int clampSize)
             throws IllegalInstructionException {
-        super(version, ramSize, clampSize);
-        Objects.requireNonNull(bytes);
-        addData(bytes);
-        addExceptionBuffer(0);
+        super(bytes, version, ramSize, clampSize);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndDropIfR0Equals(long val, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException {
+        final short tgt = getUniqueLabel();
+        return addJumpIfR0NotEquals(val, tgt).addCountAndDrop(cnt).defineLabel(tgt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndPassIfR0Equals(long val, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException {
+        final short tgt = getUniqueLabel();
+        return addJumpIfR0NotEquals(val, tgt).addCountAndPass(cnt).defineLabel(tgt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndDropIfR0NotEquals(long val, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException {
+        final short tgt = getUniqueLabel();
+        return addJumpIfR0Equals(val, tgt).addCountAndDrop(cnt).defineLabel(tgt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndPassIfR0NotEquals(long val, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException {
+        final short tgt = getUniqueLabel();
+        return addJumpIfR0Equals(val, tgt).addCountAndPass(cnt).defineLabel(tgt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndDropIfR0AnyBitsSet(long val, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException {
+        final short countAndDropLabel = getUniqueLabel();
+        final short skipLabel = getUniqueLabel();
+        return addJumpIfR0AnyBitsSet(val, countAndDropLabel)
+                .addJump(skipLabel)
+                .defineLabel(countAndDropLabel)
+                .addCountAndDrop(cnt)
+                .defineLabel(skipLabel);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndPassIfR0AnyBitsSet(long val, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException {
+        final short countAndPassLabel = getUniqueLabel();
+        final short skipLabel = getUniqueLabel();
+        return addJumpIfR0AnyBitsSet(val, countAndPassLabel)
+                .addJump(skipLabel)
+                .defineLabel(countAndPassLabel)
+                .addCountAndPass(cnt)
+                .defineLabel(skipLabel);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndDropIfR0LessThan(long val, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException {
+        if (val <= 0) {
+            throw new IllegalArgumentException("val must > 0, current val: " + val);
+        }
+        final short tgt = getUniqueLabel();
+        return addJumpIfR0GreaterThan(val - 1, tgt).addCountAndDrop(cnt).defineLabel(tgt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndPassIfR0LessThan(long val, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException {
+        if (val <= 0) {
+            throw new IllegalArgumentException("val must > 0, current val: " + val);
+        }
+        final short tgt = getUniqueLabel();
+        return addJumpIfR0GreaterThan(val - 1, tgt).addCountAndPass(cnt).defineLabel(tgt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndDropIfR0GreaterThan(long val, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException {
+        if (val < 0 || val >= 4294967295L) {
+            throw new IllegalArgumentException("val must >= 0 and < 2^32-1, current val: " + val);
+        }
+        final short tgt = getUniqueLabel();
+        return addJumpIfR0LessThan(val + 1, tgt).addCountAndDrop(cnt).defineLabel(tgt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndPassIfR0GreaterThan(long val, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException {
+        if (val < 0 || val >= 4294967295L) {
+            throw new IllegalArgumentException("val must >= 0 and < 2^32-1, current val: " + val);
+        }
+        final short tgt = getUniqueLabel();
+        return addJumpIfR0LessThan(val + 1, tgt).addCountAndPass(cnt).defineLabel(tgt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndDropIfBytesAtR0NotEqual(byte[] bytes,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        final short tgt = getUniqueLabel();
+        return addJumpIfBytesAtR0Equal(bytes, tgt).addCountAndDrop(cnt).defineLabel(tgt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndPassIfBytesAtR0NotEqual(byte[] bytes,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        final short tgt = getUniqueLabel();
+        return addJumpIfBytesAtR0Equal(bytes, tgt).addCountAndPass(cnt).defineLabel(tgt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndPassIfR0IsOneOf(@NonNull Set<Long> values,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        if (values.isEmpty()) {
+            throw new IllegalArgumentException("values cannot be empty");
+        }
+        if (values.size() == 1) {
+            return addCountAndPassIfR0Equals(values.iterator().next(), cnt);
+        }
+        final short tgt = getUniqueLabel();
+        return addJumpIfNoneOf(R0, values, tgt).addCountAndPass(cnt).defineLabel(tgt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndDropIfR0IsOneOf(@NonNull Set<Long> values,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        if (values.isEmpty()) {
+            throw new IllegalArgumentException("values cannot be empty");
+        }
+        if (values.size() == 1) {
+            return addCountAndDropIfR0Equals(values.iterator().next(), cnt);
+        }
+        final short tgt = getUniqueLabel();
+        return addJumpIfNoneOf(R0, values, tgt).addCountAndDrop(cnt).defineLabel(tgt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndPassIfR0IsNoneOf(@NonNull Set<Long> values,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        if (values.isEmpty()) {
+            throw new IllegalArgumentException("values cannot be empty");
+        }
+        if (values.size() == 1) {
+            return addCountAndPassIfR0NotEquals(values.iterator().next(), cnt);
+        }
+        final short tgt = getUniqueLabel();
+        return addJumpIfOneOf(R0, values, tgt).addCountAndPass(cnt).defineLabel(tgt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndDropIfBytesAtR0EqualsAnyOf(@NonNull List<byte[]> bytesList,
+            ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException {
+        final short tgt = getUniqueLabel();
+        return addJumpIfBytesAtR0EqualsNoneOf(bytesList, tgt).addCountAndDrop(cnt).defineLabel(tgt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndPassIfBytesAtR0EqualsAnyOf(@NonNull List<byte[]> bytesList,
+            ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException {
+        final short tgt = getUniqueLabel();
+        return addJumpIfBytesAtR0EqualsNoneOf(bytesList, tgt).addCountAndPass(cnt).defineLabel(tgt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndDropIfBytesAtR0EqualsNoneOf(@NonNull List<byte[]> bytesList,
+            ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException {
+        final short tgt = getUniqueLabel();
+        return addJumpIfBytesAtR0EqualsAnyOf(bytesList, tgt).addCountAndDrop(cnt).defineLabel(tgt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndPassIfBytesAtR0EqualsNoneOf(@NonNull List<byte[]> bytesList,
+            ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException {
+        final short tgt = getUniqueLabel();
+        return addJumpIfBytesAtR0EqualsAnyOf(bytesList, tgt).addCountAndPass(cnt).defineLabel(tgt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndDropIfBytesAtOffsetEqualsAnyOf(int offset,
+            List<byte[]> bytesList, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException {
+        return addLoadImmediate(R0, offset).addCountAndDropIfBytesAtR0EqualsAnyOf(bytesList, cnt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndPassIfBytesAtOffsetEqualsAnyOf(int offset,
+            List<byte[]> bytesList, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException {
+        return addLoadImmediate(R0, offset).addCountAndPassIfBytesAtR0EqualsAnyOf(bytesList, cnt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndDropIfBytesAtOffsetEqualsNoneOf(int offset,
+            List<byte[]> bytesList, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException {
+        return addLoadImmediate(R0, offset).addCountAndDropIfBytesAtR0EqualsNoneOf(bytesList, cnt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndPassIfBytesAtOffsetEqualsNoneOf(int offset,
+            List<byte[]> bytesList, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException {
+        return addLoadImmediate(R0, offset).addCountAndPassIfBytesAtR0EqualsNoneOf(bytesList, cnt);
+    }
+
+    @Override
+    public ApfV6Generator addJumpIfBytesAtOffsetEqualsAnyOf(int offset, List<byte[]> bytesList,
+            short tgt) throws IllegalInstructionException {
+        return addLoadImmediate(R0, offset).addJumpIfBytesAtR0EqualsAnyOf(bytesList, tgt);
+    }
+
+    @Override
+    public ApfV6Generator addJumpIfBytesAtOffsetEqualsNoneOf(int offset, List<byte[]> bytesList,
+            short tgt) throws IllegalInstructionException {
+        return addLoadImmediate(R0, offset).addJumpIfBytesAtR0EqualsNoneOf(bytesList, tgt);
+    }
+
+    @Override
+    public ApfV6Generator addCountAndDropIfR0IsNoneOf(@NonNull Set<Long> values,
+            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
+        if (values.isEmpty()) {
+            throw new IllegalArgumentException("values cannot be empty");
+        }
+        if (values.size() == 1) {
+            return addCountAndDropIfR0NotEquals(values.iterator().next(), cnt);
+        }
+        final short tgt = getUniqueLabel();
+        return addJumpIfOneOf(R0, values, tgt).addCountAndDrop(cnt).defineLabel(tgt);
+    }
+
+    @Override
+    public ApfV6Generator addJumpIfPktAtR0ContainDnsQ(byte[] qnames, int[] qtypes, short tgt) {
+        for (int qtype : qtypes) {
+            addJumpIfPktAtR0ContainDnsQ(qnames, qtype, tgt);
+        }
+        return self();
+    }
+
+    @Override
+    public ApfV6Generator addAllocate(int size) {
+        // Rbit1 means the extra be16 immediate is present
+        return append(new Instruction(ExtendedOpcodes.ALLOCATE, Rbit1).addU16(size));
+    }
+
+    @Override
+    public ApfV6Generator addTransmitWithoutChecksum() {
+        return addTransmit(-1 /* ipOfs */);
+    }
+
+    @Override
+    protected boolean handleOptimizedTransmit(int ipOfs, int csumOfs, int csumStart,
+                                              int partialCsum, boolean isUdp) {
+        return false;
     }
 }
diff --git a/src/android/net/apf/ApfV6GeneratorBase.java b/src/android/net/apf/ApfV6GeneratorBase.java
index 17629d1..90f0a28 100644
--- a/src/android/net/apf/ApfV6GeneratorBase.java
+++ b/src/android/net/apf/ApfV6GeneratorBase.java
@@ -46,9 +46,25 @@
      * the requested version is unsupported.
      *
      */
-    public ApfV6GeneratorBase(int version, int ramSize, int clampSize)
+    public ApfV6GeneratorBase(byte[] bytes, int version, int ramSize, int clampSize)
             throws IllegalInstructionException {
         super(version, ramSize, clampSize, false);
+        Objects.requireNonNull(bytes);
+        addData(bytes);
+        addExceptionBuffer(0);
+    }
+
+    @Override
+    public final int getBaseProgramSize() {
+        // When the APFv6+ generator is initialized, it always adds a 3-byte data jump
+        // instruction and a 4-byte exception instruction to the front of the program.
+        return 7;
+    }
+
+    @Override
+    void updateExceptionBufferSize(int programSize) throws IllegalInstructionException {
+        mInstructions.get(1).updateExceptionBufferSize(
+                mRamSize - ApfCounterTracker.Counter.totalSize() - programSize);
     }
 
     /**
@@ -98,10 +114,7 @@
      *
      * @param size the buffer length to be allocated.
      */
-    public final Type addAllocate(int size) {
-        // Rbit1 means the extra be16 immediate is present
-        return append(new Instruction(ExtendedOpcodes.ALLOCATE, Rbit1).addU16(size));
-    }
+    public abstract Type addAllocate(int size);
 
     /**
      * Add an instruction to the beginning of the program to reserve the empty data region.
@@ -137,9 +150,7 @@
      * Add an instruction to the end of the program to transmit the allocated buffer without
      * checksum.
      */
-    public final Type addTransmitWithoutChecksum() {
-        return addTransmit(-1 /* ipOfs */);
-    }
+    public abstract Type addTransmitWithoutChecksum();
 
     /**
      * Add an instruction to the end of the program to transmit the allocated buffer.
@@ -152,6 +163,8 @@
         return append(new Instruction(ExtendedOpcodes.TRANSMIT, Rbit0).addU8(ipOfs).addU8(255));
     }
 
+    protected abstract boolean handleOptimizedTransmit(int ipOfs, int csumOfs, int csumStart, int partialCsum, boolean isUdp);
+
     /**
      * Add an instruction to the end of the program to transmit the allocated buffer.
      */
@@ -165,6 +178,8 @@
             throw new IllegalArgumentException("L4 checksum requires csum offset of "
                                                + csumOfs + " < 255");
         }
+        if (handleOptimizedTransmit(ipOfs, csumOfs, csumStart, partialCsum, isUdp))
+            return self();
         return append(new Instruction(ExtendedOpcodes.TRANSMIT, isUdp ? Rbit1 : Rbit0)
                 .addU8(ipOfs).addU8(csumOfs).addU8(csumStart).addU16(partialCsum));
     }
@@ -237,19 +252,39 @@
     }
 
     /**
-     * Add an instruction to the end of the program to copy data from APF program/data region to
+     * Add instructions to the end of the program to copy data from APF program/data region to
      * output buffer and auto-increment the output buffer pointer.
      * This method requires the {@code addData} method to be called beforehand.
      * It will first attempt to match {@code content} with existing data bytes. If not exist, then
      * append the {@code content} to the data bytes.
+     * The method copies the content using multiple datacopy instructions if the content size
+     * exceeds 255 bytes. Each instruction will copy a maximum of 255 bytes.
      */
     public final Type addDataCopy(@NonNull byte[] content) throws IllegalInstructionException {
         if (mInstructions.isEmpty()) {
             throw new IllegalInstructionException("There is no instructions");
         }
         Objects.requireNonNull(content);
-        int copySrc = mInstructions.get(0).maybeUpdateBytesImm(content);
-        return addDataCopy(copySrc, content.length);
+        final int chunkSize = 255;
+        for (int fromIndex = 0; fromIndex < content.length; fromIndex += chunkSize) {
+            final int toIndex = Math.min(content.length, fromIndex + chunkSize);
+            final int copySrc = mInstructions.get(0).maybeUpdateBytesImm(content, fromIndex,
+                    toIndex);
+            addDataCopy(copySrc, (toIndex - fromIndex));
+        }
+        return self();
+    }
+
+    /**
+     * Add the content to the data region if it wasn't exist.
+     */
+    public final Type maybeUpdateDataRegion(@NonNull byte[] content)
+            throws IllegalInstructionException {
+        if (mInstructions.isEmpty()) {
+            throw new IllegalInstructionException("There are no instructions");
+        }
+        mInstructions.get(0).maybeUpdateBytesImm(content, 0, content.length);
+        return self();
     }
 
     /**
@@ -334,7 +369,7 @@
      * Drops packets if packets are corrupted.
      */
     public final Type addJumpIfPktAtR0DoesNotContainDnsQ(@NonNull byte[] qnames, int qtype,
-                                                             @NonNull String tgt) {
+            short tgt) {
         validateNames(qnames);
         return append(new Instruction(ExtendedOpcodes.JDNSQMATCH, Rbit0).setTargetLabel(tgt).addU8(
                 qtype).setBytesImm(qnames));
@@ -345,7 +380,7 @@
      * corrupted.
      */
     public final Type addJumpIfPktAtR0DoesNotContainDnsQSafe(@NonNull byte[] qnames, int qtype,
-            @NonNull String tgt) {
+            short tgt) {
         validateNames(qnames);
         return append(new Instruction(ExtendedOpcodes.JDNSQMATCHSAFE, Rbit0).setTargetLabel(
                 tgt).addU8(qtype).setBytesImm(qnames));
@@ -354,12 +389,59 @@
     /**
      * Appends a conditional jump instruction to the program: Jumps to {@code tgt} if the UDP
      * payload's DNS questions contain the QNAMEs specified in {@code qnames} and qtype
+     * equals any of {@code qtypes}. Examines the payload starting at the offset in R0.
+     * Drops packets if packets are corrupted.
+     */
+    public abstract Type addJumpIfPktAtR0ContainDnsQ(@NonNull byte[] qnames, @NonNull int[] qtypes,
+            short tgt);
+
+    /**
+     * Add an instruction to the end of the program to count and drop if the bytes of the
+     * packet at an offset specified by {@code offset} match any of the elements in
+     * {@code bytesList}.
+     * This method will use JBSPTRMATCH in APFv6.1 when possible.
+     */
+    public abstract Type addCountAndDropIfBytesAtOffsetEqualsAnyOf(int offset,
+            @NonNull List<byte[]> bytesList, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException;
+
+    /**
+     * Add an instruction to the end of the program to count and pass if the bytes of the
+     * packet at an offset specified by {@code offset} match any of the elements in
+     * {@code bytesList}.
+     * This method will use JBSPTRMATCH in APFv6.1 when possible.
+     */
+    public abstract Type addCountAndPassIfBytesAtOffsetEqualsAnyOf(int offset,
+            @NonNull List<byte[]> bytesList, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException;
+
+    /**
+     * Add an instruction to the end of the program to count and drop if the bytes of the
+     * packet at an offset specified by {@code offset} match none the elements in {@code bytesList}.
+     * This method will use JBSPTRMATCH in APFv6.1 when possible.
+     */
+    public abstract Type addCountAndDropIfBytesAtOffsetEqualsNoneOf(int offset,
+            @NonNull List<byte[]> bytesList, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException;
+
+    /**
+     * Add an instruction to the end of the program to count and pass if the bytes of the
+     * packet at an offset specified by {@code offset} match none of the elements in
+     * {@code bytesList}.
+     * This method will use JBSPTRMATCH in APFv6.1 when possible.
+     */
+    public abstract Type addCountAndPassIfBytesAtOffsetEqualsNoneOf(int offset,
+            @NonNull List<byte[]> bytesList, ApfCounterTracker.Counter cnt)
+            throws IllegalInstructionException;
+
+    /**
+     * Appends a conditional jump instruction to the program: Jumps to {@code tgt} if the UDP
+     * payload's DNS questions contain the QNAMEs specified in {@code qnames} and qtype
      * equals {@code qtype}. Examines the payload starting at the offset in R0.
      * R = 1 means check for "contain".
      * Drops packets if packets are corrupted.
      */
-    public final Type addJumpIfPktAtR0ContainDnsQ(@NonNull byte[] qnames, int qtype,
-                                                      @NonNull String tgt) {
+    public final Type addJumpIfPktAtR0ContainDnsQ(@NonNull byte[] qnames, int qtype, short tgt) {
         validateNames(qnames);
         return append(new Instruction(ExtendedOpcodes.JDNSQMATCH, Rbit1).setTargetLabel(tgt).addU8(
                 qtype).setBytesImm(qnames));
@@ -370,7 +452,7 @@
      * corrupted.
      */
     public final Type addJumpIfPktAtR0ContainDnsQSafe(@NonNull byte[] qnames, int qtype,
-            @NonNull String tgt) {
+            short tgt) {
         validateNames(qnames);
         return append(new Instruction(ExtendedOpcodes.JDNSQMATCHSAFE, Rbit1).setTargetLabel(
                 tgt).addU8(qtype).setBytesImm(qnames));
@@ -383,8 +465,7 @@
      * R = 0 means check for "does not contain".
      * Drops packets if packets are corrupted.
      */
-    public final Type addJumpIfPktAtR0DoesNotContainDnsA(@NonNull byte[] names,
-                                                             @NonNull String tgt) {
+    public final Type addJumpIfPktAtR0DoesNotContainDnsA(@NonNull byte[] names, short tgt) {
         validateNames(names);
         return append(new Instruction(ExtendedOpcodes.JDNSAMATCH, Rbit0).setTargetLabel(tgt)
                         .setBytesImm(names));
@@ -394,8 +475,7 @@
      * Same as {@link #addJumpIfPktAtR0DoesNotContainDnsA} except passes packets if packets are
      * corrupted.
      */
-    public final Type addJumpIfPktAtR0DoesNotContainDnsASafe(@NonNull byte[] names,
-            @NonNull String tgt) {
+    public final Type addJumpIfPktAtR0DoesNotContainDnsASafe(@NonNull byte[] names, short tgt) {
         validateNames(names);
         return append(new Instruction(ExtendedOpcodes.JDNSAMATCHSAFE, Rbit0).setTargetLabel(tgt)
                 .setBytesImm(names));
@@ -408,8 +488,7 @@
      * R = 1 means check for "contain".
      * Drops packets if packets are corrupted.
      */
-    public final Type addJumpIfPktAtR0ContainDnsA(@NonNull byte[] names,
-                                                      @NonNull String tgt) {
+    public final Type addJumpIfPktAtR0ContainDnsA(@NonNull byte[] names, short tgt) {
         validateNames(names);
         return append(new Instruction(ExtendedOpcodes.JDNSAMATCH, Rbit1).setTargetLabel(
                 tgt).setBytesImm(names));
@@ -419,8 +498,7 @@
      * Same as {@link #addJumpIfPktAtR0ContainDnsA} except passes packets if packets are
      * corrupted.
      */
-    public final Type addJumpIfPktAtR0ContainDnsASafe(@NonNull byte[] names,
-            @NonNull String tgt) {
+    public final Type addJumpIfPktAtR0ContainDnsASafe(@NonNull byte[] names, short tgt) {
         validateNames(names);
         return append(new Instruction(ExtendedOpcodes.JDNSAMATCHSAFE, Rbit1).setTargetLabel(
                 tgt).setBytesImm(names));
@@ -431,14 +509,14 @@
      * packet at an offset specified by register0 match {@code bytes}.
      * R=1 means check for equal.
      */
-    public final Type addJumpIfBytesAtR0Equal(@NonNull byte[] bytes, String tgt)
+    public final Type addJumpIfBytesAtR0Equal(@NonNull byte[] bytes, short tgt)
             throws IllegalInstructionException {
         validateBytes(bytes);
         return append(new Instruction(Opcodes.JBSMATCH, R1).addUnsigned(
                 bytes.length).setTargetLabel(tgt).setBytesImm(bytes));
     }
 
-    private Type addJumpIfBytesAtR0EqualsHelper(@NonNull List<byte[]> bytesList, String tgt,
+    private Type addJumpIfBytesAtR0EqualsHelper(@NonNull List<byte[]> bytesList, short tgt,
             boolean jumpOnMatch) {
         final List<byte[]> deduplicatedList = validateDeduplicateBytesList(bytesList);
         final int elementSize = deduplicatedList.get(0).length;
@@ -461,7 +539,7 @@
      * packet at an offset specified by register0 match any of the elements in {@code bytesSet}.
      * R=1 means check for equal.
      */
-    public final Type addJumpIfBytesAtR0EqualsAnyOf(@NonNull List<byte[]> bytesList, String tgt) {
+    public final Type addJumpIfBytesAtR0EqualsAnyOf(@NonNull List<byte[]> bytesList, short tgt) {
         return addJumpIfBytesAtR0EqualsHelper(bytesList, tgt, true /* jumpOnMatch */);
     }
 
@@ -470,19 +548,35 @@
      * packet at an offset specified by register0 match none of the elements in {@code bytesSet}.
      * R=0 means check for not equal.
      */
-    public final Type addJumpIfBytesAtR0EqualNoneOf(@NonNull List<byte[]> bytesList, String tgt) {
+    public final Type addJumpIfBytesAtR0EqualsNoneOf(@NonNull List<byte[]> bytesList, short tgt) {
         return addJumpIfBytesAtR0EqualsHelper(bytesList, tgt, false /* jumpOnMatch */);
     }
 
+    /**
+     * Add an instruction to the end of the program to jump to {@code tgt} if the bytes of the
+     * packet at an offset specified by {@code offset} match any of the elements in
+     * {@code bytesSet}.
+     */
+    public abstract Type addJumpIfBytesAtOffsetEqualsAnyOf(int offset,
+            @NonNull List<byte[]> bytesList, short tgt) throws IllegalInstructionException;
 
     /**
-     * Check if the byte is valid dns character: A-Z,0-9,-,_
+     * Add an instruction to the end of the program to jump to {@code tgt} if the bytes of the
+     * packet at an offset specified by {@code offset} match none of the elements in
+     * {@code bytesSet}.
+     */
+    public abstract Type addJumpIfBytesAtOffsetEqualsNoneOf(int offset,
+            @NonNull List<byte[]> bytesList, short tgt) throws IllegalInstructionException;
+
+    /**
+     * Check if the byte is valid dns character: A-Z,0-9,-,_,%,@
      */
     private static boolean isValidDnsCharacter(byte c) {
-        return (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '%';
+        return (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '%'
+                || c == '@';
     }
 
-    private static void validateNames(@NonNull byte[] names) {
+    static void validateNames(@NonNull byte[] names) {
         final int len = names.length;
         if (len < 4) {
             throw new IllegalArgumentException("qnames must have at least length 4");
@@ -517,7 +611,7 @@
     }
 
     private Type addJumpIfOneOfHelper(Register reg, @NonNull Set<Long> values,
-            boolean jumpOnMatch, @NonNull String tgt) {
+            boolean jumpOnMatch, short tgt) {
         if (values == null || values.size() < 2 || values.size() > 33)  {
             throw new IllegalArgumentException(
                     "size of values set must be >= 2 and <= 33, current size: " + values.size());
@@ -560,8 +654,7 @@
      * Add an instruction to the end of the program to jump to {@code tgt} if {@code reg} is
      * one of the {@code values}.
      */
-    public final Type addJumpIfOneOf(Register reg, @NonNull Set<Long> values,
-            @NonNull String tgt) {
+    public final Type addJumpIfOneOf(Register reg, @NonNull Set<Long> values, short tgt) {
         return addJumpIfOneOfHelper(reg, values, true /* jumpOnMatch */, tgt);
     }
 
@@ -569,8 +662,7 @@
      * Add an instruction to the end of the program to jump to {@code tgt} if {@code reg} is
      * not one of the {@code values}.
      */
-    public final Type addJumpIfNoneOf(Register reg, @NonNull Set<Long> values,
-            @NonNull String tgt) {
+    public final Type addJumpIfNoneOf(Register reg, @NonNull Set<Long> values, short tgt) {
         return addJumpIfOneOfHelper(reg, values, false /* jumpOnMatch */, tgt);
     }
 
@@ -579,6 +671,18 @@
         append(new Instruction(opcode, R0));  // APFv6+: R0 op= R1
     }
 
+    @Override
+    public final Type addAdd(long val) {
+        if (val == 0) return self();
+        return append(new Instruction(Opcodes.ADD).addTwosCompSigned(val));
+    }
+
+    @Override
+    public final Type addAnd(long val) {
+        if (val == 0) return addLoadImmediate(R0, 0);
+        return append(new Instruction(Opcodes.AND).addTwosCompSigned(val));
+    }
+
     /**
      * Add an instruction to the end of the program to increment the counter value and
      * immediately return PASS.
@@ -604,196 +708,6 @@
     }
 
     @Override
-    public final Type addCountAndDropIfR0Equals(long val, ApfCounterTracker.Counter cnt)
-            throws IllegalInstructionException {
-        final String tgt = getUniqueLabel();
-        return addJumpIfR0NotEquals(val, tgt).addCountAndDrop(cnt).defineLabel(tgt);
-    }
-
-    @Override
-    public final Type addCountAndPassIfR0Equals(long val, ApfCounterTracker.Counter cnt)
-            throws IllegalInstructionException {
-        final String tgt = getUniqueLabel();
-        return addJumpIfR0NotEquals(val, tgt).addCountAndPass(cnt).defineLabel(tgt);
-    }
-
-    @Override
-    public final Type addCountAndDropIfR0NotEquals(long val, ApfCounterTracker.Counter cnt)
-            throws IllegalInstructionException {
-        final String tgt = getUniqueLabel();
-        return addJumpIfR0Equals(val, tgt).addCountAndDrop(cnt).defineLabel(tgt);
-    }
-
-    @Override
-    public final Type addCountAndPassIfR0NotEquals(long val, ApfCounterTracker.Counter cnt)
-            throws IllegalInstructionException {
-        final String tgt = getUniqueLabel();
-        return addJumpIfR0Equals(val, tgt).addCountAndPass(cnt).defineLabel(tgt);
-    }
-
-    @Override
-    public Type addCountAndDropIfR0AnyBitsSet(long val, ApfCounterTracker.Counter cnt)
-            throws IllegalInstructionException {
-        final String countAndDropLabel = getUniqueLabel();
-        final String skipLabel = getUniqueLabel();
-        return addJumpIfR0AnyBitsSet(val, countAndDropLabel)
-                .addJump(skipLabel)
-                .defineLabel(countAndDropLabel)
-                .addCountAndDrop(cnt)
-                .defineLabel(skipLabel);
-    }
-
-    @Override
-    public Type addCountAndPassIfR0AnyBitsSet(long val, ApfCounterTracker.Counter cnt)
-            throws IllegalInstructionException {
-        final String countAndPassLabel = getUniqueLabel();
-        final String skipLabel = getUniqueLabel();
-        return addJumpIfR0AnyBitsSet(val, countAndPassLabel)
-                .addJump(skipLabel)
-                .defineLabel(countAndPassLabel)
-                .addCountAndPass(cnt)
-                .defineLabel(skipLabel);
-    }
-
-    @Override
-    public final Type addCountAndDropIfR0LessThan(long val, ApfCounterTracker.Counter cnt)
-            throws IllegalInstructionException {
-        if (val <= 0) {
-            throw new IllegalArgumentException("val must > 0, current val: " + val);
-        }
-        final String tgt = getUniqueLabel();
-        return addJumpIfR0GreaterThan(val - 1, tgt).addCountAndDrop(cnt).defineLabel(tgt);
-    }
-
-    @Override
-    public final Type addCountAndPassIfR0LessThan(long val, ApfCounterTracker.Counter cnt)
-            throws IllegalInstructionException {
-        if (val <= 0) {
-            throw new IllegalArgumentException("val must > 0, current val: " + val);
-        }
-        final String tgt = getUniqueLabel();
-        return addJumpIfR0GreaterThan(val - 1, tgt).addCountAndPass(cnt).defineLabel(tgt);
-    }
-
-    @Override
-    public Type addCountAndDropIfR0GreaterThan(long val, ApfCounterTracker.Counter cnt)
-            throws IllegalInstructionException {
-        if (val < 0 || val >= 4294967295L) {
-            throw new IllegalArgumentException("val must >= 0 and < 2^32-1, current val: " + val);
-        }
-        final String tgt = getUniqueLabel();
-        return addJumpIfR0LessThan(val + 1, tgt).addCountAndDrop(cnt).defineLabel(tgt);
-    }
-
-    @Override
-    public Type addCountAndPassIfR0GreaterThan(long val, ApfCounterTracker.Counter cnt)
-            throws IllegalInstructionException {
-        if (val < 0 || val >= 4294967295L) {
-            throw new IllegalArgumentException("val must >= 0 and < 2^32-1, current val: " + val);
-        }
-        final String tgt = getUniqueLabel();
-        return addJumpIfR0LessThan(val + 1, tgt).addCountAndPass(cnt).defineLabel(tgt);
-    }
-
-    @Override
-    public final Type addCountAndDropIfBytesAtR0NotEqual(byte[] bytes,
-            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
-        final String tgt = getUniqueLabel();
-        return addJumpIfBytesAtR0Equal(bytes, tgt).addCountAndDrop(cnt).defineLabel(tgt);
-    }
-
-    @Override
-    public final Type addCountAndPassIfBytesAtR0NotEqual(byte[] bytes,
-            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
-        final String tgt = getUniqueLabel();
-        return addJumpIfBytesAtR0Equal(bytes, tgt).addCountAndPass(cnt).defineLabel(tgt);
-    }
-
-    @Override
-    public Type addCountAndPassIfR0IsOneOf(@NonNull Set<Long> values,
-            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
-        if (values.isEmpty()) {
-            throw new IllegalArgumentException("values cannot be empty");
-        }
-        if (values.size() == 1) {
-            return addCountAndPassIfR0Equals(values.iterator().next(), cnt);
-        }
-        final String tgt = getUniqueLabel();
-        return addJumpIfNoneOf(R0, values, tgt).addCountAndPass(cnt).defineLabel(tgt);
-    }
-
-    @Override
-    public Type addCountAndDropIfR0IsOneOf(@NonNull Set<Long> values,
-            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
-        if (values.isEmpty()) {
-            throw new IllegalArgumentException("values cannot be empty");
-        }
-        if (values.size() == 1) {
-            return addCountAndDropIfR0Equals(values.iterator().next(), cnt);
-        }
-        final String tgt = getUniqueLabel();
-        return addJumpIfNoneOf(R0, values, tgt).addCountAndDrop(cnt).defineLabel(tgt);
-    }
-
-    @Override
-    public Type addCountAndPassIfR0IsNoneOf(@NonNull Set<Long> values,
-            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
-        if (values.isEmpty()) {
-            throw new IllegalArgumentException("values cannot be empty");
-        }
-        if (values.size() == 1) {
-            return addCountAndPassIfR0NotEquals(values.iterator().next(), cnt);
-        }
-        final String tgt = getUniqueLabel();
-        return addJumpIfOneOf(R0, values, tgt).addCountAndPass(cnt).defineLabel(tgt);
-    }
-
-    @Override
-    public Type addCountAndDropIfBytesAtR0EqualsAnyOf(@NonNull List<byte[]> bytesList,
-            ApfCounterTracker.Counter cnt)
-            throws IllegalInstructionException {
-        final String tgt = getUniqueLabel();
-        return addJumpIfBytesAtR0EqualNoneOf(bytesList, tgt).addCountAndDrop(cnt).defineLabel(tgt);
-    }
-
-    @Override
-    public Type addCountAndPassIfBytesAtR0EqualsAnyOf(@NonNull List<byte[]> bytesList,
-            ApfCounterTracker.Counter cnt)
-            throws IllegalInstructionException {
-        final String tgt = getUniqueLabel();
-        return addJumpIfBytesAtR0EqualNoneOf(bytesList, tgt).addCountAndPass(cnt).defineLabel(tgt);
-    }
-
-    @Override
-    public Type addCountAndDropIfBytesAtR0EqualsNoneOf(@NonNull List<byte[]> bytesList,
-            ApfCounterTracker.Counter cnt)
-            throws IllegalInstructionException {
-        final String tgt = getUniqueLabel();
-        return addJumpIfBytesAtR0EqualsAnyOf(bytesList, tgt).addCountAndDrop(cnt).defineLabel(tgt);
-    }
-
-    @Override
-    public Type addCountAndPassIfBytesAtR0EqualsNoneOf(@NonNull List<byte[]> bytesList,
-            ApfCounterTracker.Counter cnt)
-            throws IllegalInstructionException {
-        final String tgt = getUniqueLabel();
-        return addJumpIfBytesAtR0EqualsAnyOf(bytesList, tgt).addCountAndPass(cnt).defineLabel(tgt);
-    }
-
-    @Override
-    public Type addCountAndDropIfR0IsNoneOf(@NonNull Set<Long> values,
-            ApfCounterTracker.Counter cnt) throws IllegalInstructionException {
-        if (values.isEmpty()) {
-            throw new IllegalArgumentException("values cannot be empty");
-        }
-        if (values.size() == 1) {
-            return addCountAndDropIfR0NotEquals(values.iterator().next(), cnt);
-        }
-        final String tgt = getUniqueLabel();
-        return addJumpIfOneOf(R0, values, tgt).addCountAndDrop(cnt).defineLabel(tgt);
-    }
-
-    @Override
     public final Type addLoadCounter(Register register, ApfCounterTracker.Counter counter)
             throws IllegalInstructionException {
         return append(new Instruction(Opcodes.LDDW, register).addUnsigned(counter.value()));
@@ -812,4 +726,10 @@
     public final Type addCountTrampoline() {
         return self();
     }
+
+    @Override
+    public final int getDefaultPacketHandlingSizeOverEstimate() {
+        // addCountAndPass(PASSED_IPV6_ICMP); -> 2 bytes
+        return 2;
+    }
 }
diff --git a/src/android/net/apf/BaseApfGenerator.java b/src/android/net/apf/BaseApfGenerator.java
index 2eab5ab..21d8be3 100644
--- a/src/android/net/apf/BaseApfGenerator.java
+++ b/src/android/net/apf/BaseApfGenerator.java
@@ -21,15 +21,12 @@
 import static android.net.apf.BaseApfGenerator.Register.R0;
 
 import android.annotation.NonNull;
+import android.util.SparseArray;
 
-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;
 
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Objects;
 
@@ -109,7 +106,33 @@
         // R=1 means copy from APF program/data region.
         // The copy length is stored in (u8)imm2.
         // e.g. "pktcopy 5, 5" "datacopy 5, 5"
-        PKTDATACOPY(25);
+        PKTDATACOPY(25),
+        // JSET with reverse condition (jump if no bits set)
+        JNSET(26),
+        // APFv6.1: Compare byte sequence [R=0 not] equal, e.g. "jbsptrne 22,16,label,<dataptr>"
+        // imm1 is jmp target
+        // imm2(u8) is offset [0..255] into packet
+        // imm3(u8) is (count - 1) * 16 + (compare_len - 1), thus both count & compare_len are in
+        // [1..16] which is followed by compare_len u8 'even offset' ptrs into max 526 byte data
+        // section to compare against - ie. they are multipied by 2 and have 3 added to them
+        // (to skip over 'datajmp u16')
+        // Warning: do not specify the same byte sequence multiple times.
+        JBSPTRMATCH(27),
+        // APFv6.1: Bytecode optimized allocate | transmit instruction.
+        // R=1 -> allocate(266 + imm * 8)
+        // R=0 -> transmit
+        //   immlen=0 -> no checksum offload (transmit ip_ofs=255)
+        //   immlen>0 -> with checksum offload (transmit(udp) ip_ofs=14 ...)
+        //     imm & 7 | type of offload      | ip_ofs | udp | csum_start  | csum_ofs      | partial_csum |
+        //         0   | ip4/udp              |   14   |  X  | 14+20-8 =26 | 14+20   +6=40 |   imm >> 3   |
+        //         1   | ip4/tcp              |   14   |     | 14+20-8 =26 | 14+20  +10=44 |     --"--    |
+        //         2   | ip4/icmp             |   14   |     | 14+20   =34 | 14+20   +2=36 |     --"--    |
+        //         3   | ip4/routeralert/icmp |   14   |     | 14+20+4 =38 | 14+20+4 +2=40 |     --"--    |
+        //         4   | ip6/udp              |   14   |  X  | 14+40-32=22 | 14+40   +6=60 |     --"--    |
+        //         5   | ip6/tcp              |   14   |     | 14+40-32=22 | 14+40  +10=64 |     --"--    |
+        //         6   | ip6/icmp             |   14   |     | 14+40-32=22 | 14+40   +2=56 |     --"--    |
+        //         7   | ip6/routeralert/icmp |   14   |     | 14+40-32=22 | 14+40+8 +2=64 |     --"--    |
+        ALLOC_XMIT(28);
 
         final int value;
 
@@ -191,11 +214,25 @@
         //        bottom 1 bit  - =0 jmp if in set, =1 if not in set
         // imm4(imm3 * 1/2/3/4 bytes): the *UNIQUE* values to compare against
         JONEOF(47),
-        /* Specify length of exception buffer, which is populated on abnormal program termination.
-         * imm1: Extended opcode
-         * imm2(u16): Length of exception buffer (located *immediately* after the program itself)
-         */
-        EXCEPTIONBUFFER(48);
+        // Specify length of exception buffer, which is populated on abnormal program termination.
+        // imm1: Extended opcode
+        // imm2(u16): Length of exception buffer (located *immediately* after the program itself)
+        EXCEPTIONBUFFER(48),
+        // Jumps if the UDP payload content (starting at R0) does [not] match one
+        // of the specified QNAMEs in question records, applying case insensitivity.
+        // The qtypes in the input packet can match either of the two supplied qtypes.
+        // SAFE version PASSES corrupt packets, while the other one DROPS.
+        // R=0/1 meaning 'does not match'/'matches'
+        // R0: Offset to UDP payload content
+        // imm1: Extended opcode
+        // imm2: Jump label offset
+        // imm3(u8): Question type1 (PTR/SRV/TXT/A/AAAA)
+        // imm4(u8): Question type2 (PTR/SRV/TXT/A/AAAA)
+        // imm5(bytes): null terminated list of null terminated LV-encoded QNAMEs
+        // e.g.: "jdnsqeq2 R0,label,A,AAAA,\002aa\005local\0\0",
+        //       "jdnsqne2 R0,label,A,AAAA,\002aa\005local\0\0"
+        JDNSQMATCH2(51),
+        JDNSQMATCHSAFE2(53);
 
         final int value;
 
@@ -353,9 +390,10 @@
         // When mOpcode is a jump:
         private int mTargetLabelSize;
         private int mImmSizeOverride = -1;
-        private String mTargetLabel;
-        // When mOpcode == Opcodes.LABEL:
-        private String mLabel;
+        // mTargetLabel == -1 indicates it is uninitialized. mTargetLabel < -1 indicates a label
+        // within the program used for offset calculation. mTargetLabel >= 0 indicates a pass/drop
+        // label, its offset is mTargetLabel + program size.
+        private short mTargetLabel = -1;
         public byte[] mBytesImm;
         // Offset in bytes from the beginning of this program.
         // Set by {@link BaseApfGenerator#generate}.
@@ -457,19 +495,18 @@
             return this;
         }
 
-        Instruction setLabel(String label) throws IllegalInstructionException {
-            if (mLabels.containsKey(label)) {
+        Instruction setLabel(short label) throws IllegalInstructionException {
+            if (mLabels.get(label) != null) {
                 throw new IllegalInstructionException("duplicate label " + label);
             }
             if (mOpcode != Opcodes.LABEL) {
                 throw new IllegalStateException("adding label to non-label instruction");
             }
-            mLabel = label;
             mLabels.put(label, this);
             return this;
         }
 
-        Instruction setTargetLabel(String label) {
+        Instruction setTargetLabel(short label) {
             mTargetLabel = label;
             mTargetLabelSize = 4; // May shrink later on in generate().
             return this;
@@ -485,15 +522,16 @@
             return this;
         }
 
-        /**
-         * Attempts to match {@code content} with existing data bytes. If not exist, then
-         * append the {@code content} to the data bytes.
-         * Returns the start offset of the content from the beginning of the program.
-         */
-        int maybeUpdateBytesImm(byte[] content) throws IllegalInstructionException {
+        int findMatchInDataBytes(@NonNull byte[] content, int fromIndex, int toIndex)
+                throws IllegalInstructionException {
+            if (fromIndex >= toIndex || fromIndex < 0 || toIndex > content.length) {
+                throw new IllegalArgumentException(
+                        String.format("fromIndex: %d, toIndex: %d, content length: %d", fromIndex,
+                                toIndex, content.length));
+            }
             if (mOpcode != Opcodes.JMP || mBytesImm == null) {
                 throw new IllegalInstructionException(String.format(
-                        "maybeUpdateBytesImm() is only valid for jump data instruction, mOpcode "
+                        "this method is only valid for jump data instruction, mOpcode "
                                 + ":%s, mBytesImm: %s", Opcodes.JMP,
                         mBytesImm == null ? "(empty)" : HexDump.toHexString(mBytesImm)));
             }
@@ -501,10 +539,45 @@
                 throw new IllegalInstructionException(
                         "mImmSizeOverride must be 2, mImmSizeOverride: " + mImmSizeOverride);
             }
-            int offsetInDataBytes = CollectionUtils.indexOfSubArray(mBytesImm, content);
+            final int subArrayLength = toIndex - fromIndex;
+            for (int i = 0; i < mBytesImm.length - subArrayLength + 1; i++) {
+                boolean found = true;
+                for (int j = 0; j < subArrayLength; j++) {
+                    if (mBytesImm[i + j] != content[fromIndex + j]) {
+                        found = false;
+                        break;
+                    }
+                }
+                if (found) {
+                    return i;
+                }
+            }
+            return -1;
+        }
+
+        private static byte[] concat(byte[] prefix, byte[] suffix, int suffixFrom, int suffixTo) {
+            final byte[] newArray = new byte[prefix.length + suffixTo - suffixFrom];
+            System.arraycopy(prefix, 0, newArray, 0, prefix.length);
+            System.arraycopy(suffix, suffixFrom, newArray, prefix.length, suffixTo - suffixFrom);
+            return newArray;
+        }
+
+        /**
+         * Manages and updates the data region.
+         * <p>
+         * Searches for the specified subarray within the existing data region. If the subarray
+         * is not found, it is appended to the data region. The subarray is defined as the
+         * portion of the {@code content} starting at {@code fromIndex} (inclusive)
+         * and ending at {@code toIndex} (exclusive).
+         * <p>
+         * @return The starting position of the subarray within the data region.
+         */
+        int maybeUpdateBytesImm(byte[] content, int fromIndex, int toIndex)
+                throws IllegalInstructionException {
+            int offsetInDataBytes = findMatchInDataBytes(content, fromIndex, toIndex);
             if (offsetInDataBytes == -1) {
                 offsetInDataBytes = mBytesImm.length;
-                mBytesImm = ByteUtils.concat(mBytesImm, content);
+                mBytesImm = concat(mBytesImm, content, fromIndex, toIndex);
                 // Update the length immediate (first imm) value. Due to mValue within
                 // IntImmediate being final, we must remove and re-add the value to apply changes.
                 mIntImms.remove(0);
@@ -543,7 +616,7 @@
             for (IntImmediate imm : mIntImms) {
                 size += imm.getEncodingSize(indeterminateSize);
             }
-            if (mTargetLabel != null) {
+            if (mTargetLabel != -1) {
                 size += indeterminateSize;
             }
             if (mBytesImm != null) {
@@ -558,7 +631,7 @@
          * @return {@code true} if shrunk.
          */
         boolean shrink() throws IllegalInstructionException {
-            if (mTargetLabel == null) {
+            if (mTargetLabel == -1) {
                 return false;
             }
             int oldTargetLabelSize = mTargetLabelSize;
@@ -618,7 +691,7 @@
                 writingOffset = mIntImms.get(startOffset++).writeValue(bytecode, writingOffset,
                         indeterminateSize);
             }
-            if (mTargetLabel != null) {
+            if (mTargetLabel != -1) {
                 writingOffset = writeValue(calculateTargetLabelOffset(), bytecode, writingOffset,
                         indeterminateSize);
             }
@@ -667,20 +740,18 @@
         }
 
         private int calculateTargetLabelOffset() throws IllegalInstructionException {
-            Instruction targetLabelInstruction;
-            if (mTargetLabel == DROP_LABEL) {
-                targetLabelInstruction = mDropLabel;
-            } else if (mTargetLabel == PASS_LABEL) {
-                targetLabelInstruction = mPassLabel;
+            int targetOffset;
+            if (mTargetLabel >= 0) {
+                targetOffset = mTotalSize + mTargetLabel;
             } else {
-                targetLabelInstruction = mLabels.get(mTargetLabel);
+                final Instruction targetLabelInstruction = mLabels.get(mTargetLabel);
+                if (targetLabelInstruction == null) {
+                    throw new IllegalInstructionException("label not found: " + mTargetLabel);
+                }
+                targetOffset = targetLabelInstruction.offset;
             }
-            if (targetLabelInstruction == null) {
-                throw new IllegalInstructionException("label not found: " + mTargetLabel);
-            }
-            // Calculate distance from end of this instruction to instruction.offset.
-            final int targetLabelOffset = targetLabelInstruction.offset - (offset + size());
-            return targetLabelOffset;
+            // Calculate distance from end of this instruction to targetOffset.
+            return targetOffset - (offset + size());
         }
     }
 
@@ -725,24 +796,12 @@
 
     void checkPassCounterRange(ApfCounterTracker.Counter cnt) {
         if (mDisableCounterRangeCheck) return;
-        if (cnt.value() < ApfCounterTracker.MIN_PASS_COUNTER.value()
-                || cnt.value() > ApfCounterTracker.MAX_PASS_COUNTER.value()) {
-            throw new IllegalArgumentException(
-                    String.format("Counter %s, is not in range [%s, %s]", cnt,
-                            ApfCounterTracker.MIN_PASS_COUNTER,
-                            ApfCounterTracker.MAX_PASS_COUNTER));
-        }
+        cnt.getJumpPassLabel();
     }
 
     void checkDropCounterRange(ApfCounterTracker.Counter cnt) {
         if (mDisableCounterRangeCheck) return;
-        if (cnt.value() < ApfCounterTracker.MIN_DROP_COUNTER.value()
-                || cnt.value() > ApfCounterTracker.MAX_DROP_COUNTER.value()) {
-            throw new IllegalArgumentException(
-                    String.format("Counter %s, is not in range [%s, %s]", cnt,
-                            ApfCounterTracker.MIN_DROP_COUNTER,
-                            ApfCounterTracker.MAX_DROP_COUNTER));
-        }
+        cnt.getJumpDropLabel();
     }
 
     /**
@@ -758,6 +817,8 @@
      */
     abstract void updateExceptionBufferSize(int programSize) throws IllegalInstructionException;
 
+    private int mTotalSize;
+
     /**
      * Generate the bytecode for the APF program.
      * @return the bytecode.
@@ -771,7 +832,6 @@
             throw new IllegalStateException("Can only generate() once!");
         }
         mGenerated = true;
-        int total_size;
         boolean shrunk;
         // Shrink the immediate value fields of instructions.
         // As we shrink the instructions some branch offset
@@ -781,10 +841,7 @@
         // Limit iterations to avoid O(n^2) behavior.
         int iterations_remaining = 10;
         do {
-            total_size = updateInstructionOffsets();
-            // Update drop and pass label offsets.
-            mDropLabel.offset = total_size + 1;
-            mPassLabel.offset = total_size;
+            mTotalSize = updateInstructionOffsets();
             // Limit run-time in aberant circumstances.
             if (iterations_remaining-- == 0) break;
             // Attempt to shrink instructions.
@@ -796,8 +853,8 @@
             }
         } while (shrunk);
         // Generate bytecode for instructions.
-        byte[] bytecode = new byte[total_size];
-        updateExceptionBufferSize(total_size);
+        byte[] bytecode = new byte[mTotalSize];
+        updateExceptionBufferSize(mTotalSize);
         for (Instruction instruction : mInstructions) {
             instruction.generate(bytecode);
         }
@@ -850,27 +907,30 @@
         }
     }
 
-    private int mLabelCount = 0;
+    private short mLabelCount = 0;
 
     /**
      * Return a unique label string.
      */
-    @VisibleForTesting
-    public String getUniqueLabel() {
-        return "LABEL_" + mLabelCount++;
+    public short getUniqueLabel() {
+        final short nextLabel = (short) -(2 + mLabelCount++);
+        if (nextLabel == Short.MIN_VALUE) {
+            throw new IllegalStateException("Running out of unique labels");
+        }
+        return nextLabel;
     }
 
     /**
      * Jump to this label to terminate the program and indicate the packet
      * should be dropped.
      */
-    public static final String DROP_LABEL = "__DROP__";
+    public static final short DROP_LABEL = 1;
 
     /**
      * Jump to this label to terminate the program and indicate the packet
      * should be passed to the AP.
      */
-    public static final String PASS_LABEL = "__PASS__";
+    public static final short PASS_LABEL = 0;
 
     /**
      * Number of memory slots available for access via APF stores to memory and loads from memory.
@@ -952,12 +1012,12 @@
     public static final int APF_VERSION_3 = 3;
     public static final int APF_VERSION_4 = 4;
     public static final int APF_VERSION_6 = 6000;
+    // TODO: update the version code once we finalized APFv6.1.
+    public static final int APF_VERSION_61 = 20250228;
 
 
     final ArrayList<Instruction> mInstructions = new ArrayList<Instruction>();
-    private final HashMap<String, Instruction> mLabels = new HashMap<String, Instruction>();
-    private final Instruction mDropLabel = new Instruction(Opcodes.LABEL);
-    private final Instruction mPassLabel = new Instruction(Opcodes.LABEL);
+    private final SparseArray<Instruction> mLabels = new SparseArray<>();
     public final int mVersion;
     public final int mRamSize;
     public final int mClampSize;
diff --git a/src/android/net/apf/DnsUtils.java b/src/android/net/apf/DnsUtils.java
deleted file mode 100644
index ad5d20c..0000000
--- a/src/android/net/apf/DnsUtils.java
+++ /dev/null
@@ -1,414 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net.apf;
-
-import static android.net.apf.BaseApfGenerator.MemorySlot;
-import static android.net.apf.BaseApfGenerator.Register.R0;
-import static android.net.apf.BaseApfGenerator.Register.R1;
-
-import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN;
-import static com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN;
-
-import android.annotation.NonNull;
-
-/**
- * Utility class that generates generating APF filters for DNS packets.
- */
-public class DnsUtils {
-
-    /** Length of the DNS header. */
-    private static final int DNS_HEADER_LEN = 12;
-    /** Offset of the qdcount field within the DNS header. */
-    private static final int DNS_QDCOUNT_OFFSET = 4;
-
-    // Static labels
-    private static final String LABEL_START_MATCH = "start_match";
-    private static final String LABEL_PARSE_DNS_LABEL = "parse_dns_label";
-    private static final String LABEL_FIND_NEXT_DNS_QUESTION = "find_next_dns_question";
-
-    // Length of the pointers used by compressed names.
-    private static final int LABEL_SIZE = Byte.BYTES;
-    private static final int POINTER_SIZE = Short.BYTES;
-    private static final int QUESTION_HEADER_SIZE = Short.BYTES + Short.BYTES;
-    private static final int LABEL_AND_QUESTION_HEADER_SIZE = LABEL_SIZE + QUESTION_HEADER_SIZE;
-    private static final int POINTER_AND_QUESTION_HEADER_SIZE = POINTER_SIZE + QUESTION_HEADER_SIZE;
-
-    /** Memory slot that stores the offset within the packet of the DNS header. */
-    private static final MemorySlot SLOT_DNS_HEADER_OFFSET = MemorySlot.SLOT_1;
-    /** Memory slot that stores the current parsing offset. */
-    private static final MemorySlot SLOT_CURRENT_PARSE_OFFSET = MemorySlot.SLOT_2;
-    /**
-     * Memory slot that stores the offset after the current question, if the code is currently
-     * parsing a pointer, or 0 if it is not.
-     */
-    private static final MemorySlot SLOT_AFTER_POINTER_OFFSET = MemorySlot.SLOT_3;
-    /**
-     * Contains qdcount remaining, as a negative number. For example, will be -1 when starting to
-     * parse a DNS packet with one question in it. It's stored as a negative number because adding 1
-     * is much easier than subtracting 1 (which can't be done just by adding -1, because that just
-     * adds 254).
-     */
-    private static final MemorySlot SLOT_NEGATIVE_QDCOUNT_REMAINING = MemorySlot.SLOT_4;
-    /** Memory slot used by the jump table. */
-    private static final MemorySlot SLOT_RETURN_VALUE_INDEX = MemorySlot.SLOT_5;
-
-    /**
-     * APF function: parse_dns_label
-     *
-     * Parses a label potentially containing a pointer, and calculates the label length and the
-     * offset of the label data.
-     *
-     * Inputs:
-     * - m[SLOT_DNS_HEADER_OFFSET]: offset of DNS header
-     * - m[SLOT_CURRENT_PARSE_OFFSET]: current parsing offset
-     * - m[SLOT_AFTER_POINTER_OFFSET]: offset after the question (e.g., offset of the next question,
-     *        or offset of the answer section) if a pointer is being chased, 0 otherwise
-     * - m[SLOT_RETURN_VALUE_INDEX]: index into return jump table
-     *
-     * Outputs:
-     * - R1: label length
-     * - m[SLOT_CURRENT_PARSE_OFFSET]: offset of label text
-     */
-    private static void genParseDnsLabel(ApfV4Generator gen, JumpTable jumpTable) throws Exception {
-        final String labelParseDnsLabelReal = "parse_dns_label_real";
-        final String labelPointerOffsetStored = "pointer_offset_stored";
-
-        /**
-         * :parse_dns_label
-         * // Load parsing offset.
-         * LDM R1, 2                        // R1 = parsing offset. (All indexed loads use R1.)
-         */
-        gen.defineLabel(LABEL_PARSE_DNS_LABEL);
-        gen.addLoadFromMemory(R1, SLOT_CURRENT_PARSE_OFFSET);
-
-
-        /**
-         * // Check that we’re in the DNS packet, i.e., that R1 >= m[SLOT_DNS_HEADER_OFFSET].
-         * LDM R0, 1                        // R0 = DNS header offset
-         * JGT R0, R1, DROP                 // Bad pointer. Drop.
-         */
-        gen.addLoadFromMemory(R0, SLOT_DNS_HEADER_OFFSET);
-        gen.addJumpIfR0GreaterThanR1(ApfV4Generator.DROP_LABEL);
-
-        /**
-         * // Now parse the label.
-         * LDBX R0, [R1]                    // R0 = label length, R1 = parsing offset
-         * AND R0, 0xc0                     // Is this a pointer?
-         *
-         * JEQ R0, 0, :parse_dns_label_real
-         */
-        gen.addLoad8Indexed(R0, 0);
-        gen.addAnd(0xc0);
-        gen.addJumpIfR0Equals(0, labelParseDnsLabelReal);
-
-
-        /**
-         * // If we’re not already chasing a pointer, store offset after pointer into
-         * // m[SLOT_AFTER_POINTER_OFFSET].
-         * LDM R0, 3                        // R0 = previous offset after pointer
-         * JNE 0, :pointer_offset_stored
-         * MOV R0, R1                       // R0 = R1
-         * ADD R0, 6                        // R0 = offset after pointer and record
-         * STM R0, 3                        // Store offset after pointer
-         */
-        gen.addLoadFromMemory(R0, SLOT_AFTER_POINTER_OFFSET);
-        gen.addJumpIfR0NotEquals(0, labelPointerOffsetStored);
-        gen.addMove(R0);
-        gen.addAdd(POINTER_AND_QUESTION_HEADER_SIZE);
-        gen.addStoreToMemory(SLOT_AFTER_POINTER_OFFSET, R0);
-
-        /**
-         * :pointer_offset_stored
-         * LDHX R0, [R1]                    // R0 = 2-byte pointer value
-         * AND R0, 0x3ff                    // R0 = pointer destination offset (from DNS header)
-         * LDM R1, 1                        // R1 = offset in packet of DNS header
-         * ADD R0, R1                       // R0 = pointer destination offset
-         * LDM R1, 2                        // R1 = current parsing offset
-         * JEQ R0, R1, DROP                 // Drop if pointer points here...
-         * JGT R0, R1, DROP                 // ... or after here (must point backwards)
-         * STM R0, 2                        // Set next parsing offset to pointer destination
-         */
-        gen.defineLabel(labelPointerOffsetStored);
-        gen.addLoad16Indexed(R0, 0);
-        gen.addAnd(0x3ff);
-        gen.addLoadFromMemory(R1, SLOT_DNS_HEADER_OFFSET);
-        gen.addAddR1ToR0();
-        gen.addLoadFromMemory(R1, SLOT_CURRENT_PARSE_OFFSET);
-        gen.addJumpIfR0EqualsR1(ApfV4Generator.DROP_LABEL);
-        gen.addJumpIfR0GreaterThanR1(ApfV4Generator.DROP_LABEL);
-        gen.addStoreToMemory(SLOT_CURRENT_PARSE_OFFSET, R0);
-
-        /** // Pointer chased. Parse starting from the pointer destination (which may also be a
-         * pointer).
-         * JMP :parse_dns_label
-         */
-        gen.addJump(LABEL_PARSE_DNS_LABEL);
-
-        /**
-         * :parse_real_label
-         * // This is where the real (non-pointer) label starts.
-         * // Load label length into R1, and return to caller.
-         * // m[SLOT_CURRENT_PARSE_OFFSET] already contains label offset.
-         * LDHX R1, [R1]                    // R1 = label length
-         */
-        gen.defineLabel(labelParseDnsLabelReal);
-        gen.addLoad8Indexed(R1, 0);
-
-        /** // Return
-         * LDM R0, 10
-         * JMP :jump_table
-         */
-        gen.addLoadFromMemory(R0, SLOT_RETURN_VALUE_INDEX);
-        gen.addJump(jumpTable.getStartLabel());
-    }
-
-    /**
-     * APF function: find_next_dns_question
-     *
-     * Finds the next question in the question section, or drops the packet if there is none.
-     *
-     * Inputs:
-     * - m[SLOT_CURRENT_PARSE_OFFSET]: current parsing offset
-     * - m[SLOT_AFTER_POINTER_OFFSET]: offset after first pointer in name, or 0 if not chasing a
-     *           pointer
-     * - m[SLOT_NEGATIVE_QDCOUNT_REMAINING]: qdcount remaining, as a negative number. This is
-     *           because adding 1 is much easier than subtracting 1 (which can't be done just by
-     *           adding -1, because that just adds 254)
-     * - m[SLOT_RETURN_VALUE_INDEX]: index into return jump table
-     *
-     * Outputs:
-     * None
-     */
-    private static void genFindNextDnsQuestion(ApfV4Generator gen, JumpTable jumpTable)
-            throws Exception {
-        final String labelFindNextDnsQuestionFollow = "find_next_dns_question_follow";
-        final String labelFindNextDnsQuestionLabel = "find_next_dns_question_label";
-        final String labelFindNextDnsQuestionLoop = "find_next_dns_question_loop";
-        final String labelFindNextDnsQuestionNoPointer = "find_next_dns_question_no_pointer";
-        final String labelFindNextDnsQuestionReturn = "find_next_dns_question_return";
-
-        // Function entry point.
-        gen.defineLabel(LABEL_FIND_NEXT_DNS_QUESTION);
-
-        // Are we chasing a pointer?
-        gen.addLoadFromMemory(R0, SLOT_AFTER_POINTER_OFFSET);
-        gen.addJumpIfR0Equals(0, labelFindNextDnsQuestionFollow);
-
-        // If so, offset after the pointer and question is stored in m[SLOT_AFTER_POINTER_OFFSET].
-        // Move parsing offset there, clear m[SLOT_AFTER_POINTER_OFFSET], and return.
-        gen.addStoreToMemory(SLOT_CURRENT_PARSE_OFFSET, R0);
-        gen.addLoadImmediate(R0, 0);
-        gen.addStoreToMemory(SLOT_AFTER_POINTER_OFFSET, R0);
-        gen.addJump(labelFindNextDnsQuestionReturn);
-
-        // We weren't chasing a pointer. Loop, following the label chain, until we reach a
-        // zero-length label or a pointer. At the beginning of the loop, the current parsing offset
-        // is m[SLOT_CURRENT_PARSE_OFFSET]. Move it to R1 and keep it in R1 throughout the loop.
-        gen.defineLabel(labelFindNextDnsQuestionFollow);
-        gen.addLoadFromMemory(R1, SLOT_CURRENT_PARSE_OFFSET);
-
-        // Load label length.
-        gen.defineLabel(labelFindNextDnsQuestionLoop);
-        gen.addLoad8Indexed(R0, 0);
-        // Is it a pointer?
-        gen.addAnd(0xc0);
-        gen.addJumpIfR0Equals(0, labelFindNextDnsQuestionNoPointer);
-        // It's a pointer. Skip the pointer and question, and return.
-        gen.addLoadImmediate(R0, POINTER_AND_QUESTION_HEADER_SIZE);
-        gen.addAddR1ToR0();
-        gen.addStoreToMemory(SLOT_CURRENT_PARSE_OFFSET, R0);
-        gen.addJump(labelFindNextDnsQuestionReturn);
-
-        // R1 still contains parsing offset.
-        gen.defineLabel(labelFindNextDnsQuestionNoPointer);
-        gen.addLoad8Indexed(R0, 0);
-
-        // Zero-length label? We're done.
-        // Skip the label (1 byte) and query (2 bytes qtype, 2 bytes qclass) and return.
-        gen.addJumpIfR0NotEquals(0, labelFindNextDnsQuestionLabel);
-        gen.addLoadImmediate(R0, LABEL_AND_QUESTION_HEADER_SIZE);
-        gen.addAddR1ToR0();
-        gen.addStoreToMemory(SLOT_CURRENT_PARSE_OFFSET, R0);
-        gen.addJump(labelFindNextDnsQuestionReturn);
-
-        // Non-zero length label. Consume it and continue.
-        gen.defineLabel(labelFindNextDnsQuestionLabel);
-        gen.addAdd(1);
-        gen.addAddR1ToR0();
-        gen.addMove(R1);
-        gen.addJump(labelFindNextDnsQuestionLoop);
-
-        gen.defineLabel(labelFindNextDnsQuestionReturn);
-
-        // Is this the last question? If so, drop.
-        gen.addLoadFromMemory(R0, SLOT_NEGATIVE_QDCOUNT_REMAINING);
-        gen.addAdd(1);
-        gen.addStoreToMemory(SLOT_NEGATIVE_QDCOUNT_REMAINING, R0);
-        gen.addJumpIfR0Equals(0, ApfV4Generator.DROP_LABEL);
-
-        // If not, return.
-        gen.addJump(jumpTable.getStartLabel());
-    }
-
-    /** @return jump label that points to the start of a DNS label's parsing code. */
-    private static String getStartMatchLabel(int labelIndex) {
-        return "dns_parse_" + labelIndex;
-    }
-
-    /** @return jump label used while parsing the specified DNS label. */
-    private static String getPostMatchJumpTargetForLabel(int labelIndex) {
-        return "dns_parsed_" + labelIndex;
-    }
-
-    /** @return jump label used when the match for the specified DNS label fails. */
-    private static String getNoMatchLabel(int labelIndex) {
-        return "dns_nomatch_" + labelIndex;
-    }
-
-    private static void addMatchLabel(@NonNull ApfV4Generator gen, @NonNull JumpTable jumpTable,
-            int labelIndex, @NonNull String label, @NonNull String nextLabel) throws Exception {
-        final String parsedLabel = getPostMatchJumpTargetForLabel(labelIndex);
-        final String noMatchLabel = getNoMatchLabel(labelIndex);
-        gen.defineLabel(getStartMatchLabel(labelIndex));
-
-        // Store return address.
-        gen.addLoadImmediate(R0, jumpTable.getIndex(parsedLabel));
-        gen.addStoreToMemory(SLOT_RETURN_VALUE_INDEX, R0);
-
-        // Call the parse_label function.
-        gen.addJump(LABEL_PARSE_DNS_LABEL);
-
-        gen.defineLabel(parsedLabel);
-
-        // If label length is 0, this is the end of the name and the match failed.
-        gen.addSwap(); // Move label length from R1 to R0
-        gen.addJumpIfR0Equals(0, noMatchLabel);
-
-        // Label parsed, check it matches what we're looking for.
-        gen.addJumpIfR0NotEquals(label.length(), noMatchLabel);
-        gen.addLoadFromMemory(R0, SLOT_CURRENT_PARSE_OFFSET);
-        gen.addAdd(1);
-        gen.addJumpIfBytesAtR0NotEqual(label.getBytes(), noMatchLabel);
-
-        // Prep offset of next label.
-        gen.addAdd(label.length());
-        gen.addStoreToMemory(SLOT_CURRENT_PARSE_OFFSET, R0);
-
-        // Match, go to next label.
-        gen.addJump(nextLabel);
-
-        // Match failed. Go to next name, and restart from the first match.
-        gen.defineLabel(noMatchLabel);
-        gen.addLoadImmediate(R1, jumpTable.getIndex(LABEL_START_MATCH));
-        gen.addStoreToMemory(SLOT_RETURN_VALUE_INDEX, R1);
-        gen.addJump(LABEL_FIND_NEXT_DNS_QUESTION);
-    }
-
-    /**
-     * Generates a filter that accepts DNS packet that ask for the specified name.
-     *
-     * The filter supports compressed DNS names and scanning through multiple questions in the same
-     * packet, e.g., as used by MDNS. However, it currently only supports one DNS name.
-     *
-     * Limitations:
-     * <ul>
-     * <li>Filter size is just under 300 bytes for a typical question.
-     * <li>Because the bytecode extensively uses backwards jumps, it can hit the APF interpreter
-     *   instruction limit. This limit causes the APF interpreter to accept the packet once it has
-     *   executed a number of instructions equal to the program length in bytes.
-     *   A program that consists *only* of this filter will be able to execute just under 300
-     *   instructions, and will be able to correctly drop packets with two questions but not three
-     *   questions. In a real APF setup, there will be other code (e.g., RA filtering) which counts
-     *   against the limit, so the filter should be able to parse packets with more questions.
-     * <li>Matches are case-sensitive. This is due to the use of JNEBS to match DNS labels and is
-     *   likely impossible to overcome without interpreter changes.
-     * </ul>
-     *
-     * TODO:
-     * <ul>
-     * <li>Add unit tests for the parse_dns_label and find_next_dns_question functions.
-     * <li>Support accepting more than one name.
-     * <li>For devices where power saving is a priority (e.g., flat panel TVs), add support for
-     *   dropping packets with more than X queries, to ensure the filter will drop the packet rather
-     *   than hit the instruction limit.
-     * </ul>
-     */
-    public static void generateFilter(ApfV4Generator gen, String[] labels) throws Exception {
-        final int etherPlusUdpLen = ETHER_HEADER_LEN + UDP_HEADER_LEN;
-
-        final String labelJumpTable = "jump_table";
-
-        // Initialize parsing
-        /**
-         * - R1: length of IP header.
-         * - m[SLOT_DNS_HEADER_OFFSET]: offset of DNS header
-         * - m[SLOT_CURRENT_PARSE_OFFSET]: current parsing offset (start of question section)
-         * - m[SLOT_AFTER_POINTER_OFFSET]: offset after first pointer in name, must be 0 when
-         *                                 starting a new name
-         * - m[SLOT_NEGATIVE_QDCOUNT_REMAINING]: negative qdcount
-         */
-        // Move IP header length to R0 and use it to find the DNS header offset.
-        // TODO: this uses R1 for consistency with ApfFilter#generateMdnsFilterLocked. Evaluate
-        // using R0 instead.
-        gen.addMove(R0);
-        gen.addAdd(etherPlusUdpLen);
-        gen.addStoreToMemory(SLOT_DNS_HEADER_OFFSET, R0);
-
-        gen.addAdd(DNS_QDCOUNT_OFFSET);
-        gen.addMove(R1);
-        gen.addLoad16Indexed(R1, 0);
-        gen.addNeg(R1);
-        gen.addStoreToMemory(SLOT_NEGATIVE_QDCOUNT_REMAINING, R1);
-
-        gen.addAdd(DNS_HEADER_LEN - DNS_QDCOUNT_OFFSET);
-        gen.addStoreToMemory(SLOT_CURRENT_PARSE_OFFSET, R0);
-
-        gen.addLoadImmediate(R0, 0);
-        gen.addStoreToMemory(SLOT_AFTER_POINTER_OFFSET, R0);
-
-        gen.addJump(LABEL_START_MATCH);
-
-        // Create JumpTable but
-        final JumpTable table = new JumpTable(labelJumpTable, SLOT_RETURN_VALUE_INDEX);
-
-        // Generate bytecode for parse_label function.
-        genParseDnsLabel(gen, table);
-        genFindNextDnsQuestion(gen, table);
-
-        // Populate jump table. Should be before the code that calls to it (i.e., the addMatchLabel
-        // calls below) because otherwise all the jumps are backwards, and backwards jumps are more
-        // expensive (5 bytes of bytecode)
-        for (int i = 0; i < labels.length; i++) {
-            table.addLabel(getPostMatchJumpTargetForLabel(i));
-        }
-        table.addLabel(LABEL_START_MATCH);
-        table.generate(gen);
-
-        // Add match statements for name.
-        gen.defineLabel(LABEL_START_MATCH);
-        for (int i = 0; i < labels.length; i++) {
-            final String nextLabel = (i == labels.length - 1)
-                    ? ApfV4Generator.PASS_LABEL
-                    : getStartMatchLabel(i + 1);
-            addMatchLabel(gen, table, i, labels[i], nextLabel);
-        }
-        gen.addJump(ApfV4Generator.DROP_LABEL);
-    }
-
-    private DnsUtils() {
-    }
-}
diff --git a/src/android/net/apf/JumpTable.java b/src/android/net/apf/JumpTable.java
deleted file mode 100644
index 367c901..0000000
--- a/src/android/net/apf/JumpTable.java
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net.apf;
-
-import static android.net.apf.BaseApfGenerator.MemorySlot;
-import static android.net.apf.BaseApfGenerator.Register.R0;
-
-import android.annotation.NonNull;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.NoSuchElementException;
-import java.util.Objects;
-
-/**
- * A table that stores program labels to jump to.
- *
- * This is needed to implement subroutines because APF jump targets must be known at compile
- * time and cannot be computed dynamically.
- *
- * At compile time, any code that calls a subroutine must:
- *
- * <ul>
- * <li>Define a label (via {@link ApfV4Generator#defineLabel}) immediately after the code that
- *     invokes the subroutine.
- * <li>Add the label to the jump table using {@link #addLabel}.
- * <li>Generate the jump table in the program.
- * </ul>
- *
- * <p>At runtime, before invoking the subroutine, the APF code must store the index of the return
- * label (obtained via {@link #getIndex}) into the jump table's return address memory slot, and then
- * jump to the subroutine. To return to the caller, the subroutine must jump to the label returned
- * by {@link #getStartLabel}, and the jump table will then jump to the return label.
- *
- * <p>Implementation details:
- * <ul>
- * <li>The jumps are added to the program in the same order as the labels were added.
- * <li>Using the jump table will overwrite the value of register R0.
- * <li>If, before calling a subroutine, the APF code stores a nonexistent return label index, then
- *     the jump table will pass the packet. This cannot happen if the code correctly obtains the
- *     label using {@link #getIndex}, as that would throw an exception when generating the program.
- * </ul>
- *
- * For example:
- * <pre>
- *     JumpTable t = new JumpTable("my_jump_table", 7);
- *     t.addLabel("jump_1");
- *     ...
- *     t.addLabel("after_parsing");
- *     ...
- *     t.addLabel("after_subroutine");
- *     t.generate(gen);
- *</pre>
- * generates the following APF code:
- * <pre>
- *     :my_jump_table
- *     ldm r0, 7
- *     jeq r0, 0, jump_1
- *     jeq r0, 1, after_parsing
- *     jeq r0, 2, after_subroutine
- *     jmp DROP
- * </pre>
- */
-public class JumpTable {
-    /** Maps jump indices to jump labels. LinkedHashMap guarantees iteration in insertion order. */
-    private final Map<String, Integer> mJumpLabels = new LinkedHashMap<>();
-    /** Label to jump to to execute this jump table. */
-    private final String mStartLabel;
-    /** Memory slot that contains the return value index. */
-    private final MemorySlot mReturnAddressMemorySlot;
-
-    private int mIndex = 0;
-
-    public JumpTable(@NonNull String startLabel, MemorySlot returnAddressMemorySlot) {
-        Objects.requireNonNull(startLabel);
-        mStartLabel = startLabel;
-        if (returnAddressMemorySlot.value < 0
-                || returnAddressMemorySlot.value >= MemorySlot.FIRST_PREFILLED.value) {
-            throw new IllegalArgumentException(
-                    "Invalid memory slot " + returnAddressMemorySlot.value);
-        }
-        mReturnAddressMemorySlot = returnAddressMemorySlot;
-    }
-
-    /** Returns the label to jump to to start executing the table. */
-    @NonNull
-    public String getStartLabel() {
-        return mStartLabel;
-    }
-
-    /**
-     * Adds a jump label to this table. Passing a label that was already added is not an error.
-     *
-     * @param label the label to add
-     */
-    public void addLabel(@NonNull String label) {
-        Objects.requireNonNull(label);
-        if (mJumpLabels.putIfAbsent(label, mIndex) == null) mIndex++;
-    }
-
-    /**
-     * Gets the index of a previously-added label.
-     * @return the label's index.
-     * @throws NoSuchElementException if the label was never added.
-     */
-    public int getIndex(@NonNull String label) {
-        final Integer index = mJumpLabels.get(label);
-        if (index == null) throw new NoSuchElementException("Unknown label " + label);
-        return index;
-    }
-
-    /** Generates APF code for this jump table */
-    public void generate(@NonNull ApfV4Generator gen)
-            throws ApfV4Generator.IllegalInstructionException {
-        gen.defineLabel(mStartLabel);
-        gen.addLoadFromMemory(R0, mReturnAddressMemorySlot);
-        for (Map.Entry<String, Integer> e : mJumpLabels.entrySet()) {
-            gen.addJumpIfR0Equals(e.getValue(), e.getKey());
-        }
-        // Cannot happen unless the program is malformed (i.e., the APF code loads an invalid return
-        // label index before jumping to the subroutine.
-        gen.addJump(ApfV4Generator.PASS_LABEL);
-    }
-}
diff --git a/src/android/net/apf/LegacyApfFilter.java b/src/android/net/apf/LegacyApfFilter.java
deleted file mode 100644
index 2cd0eec..0000000
--- a/src/android/net/apf/LegacyApfFilter.java
+++ /dev/null
@@ -1,2439 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net.apf;
-
-import static android.net.apf.BaseApfGenerator.MemorySlot;
-import static android.net.apf.BaseApfGenerator.Register.R0;
-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.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_RAW;
-
-import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST;
-import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
-import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT;
-import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
-import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION;
-import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_LEN;
-
-import android.annotation.Nullable;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.net.LinkAddress;
-import android.net.LinkProperties;
-import android.net.NattKeepalivePacketDataParcelable;
-import android.net.TcpKeepalivePacketDataParcelable;
-import android.net.apf.ApfCounterTracker.Counter;
-import android.net.apf.BaseApfGenerator.IllegalInstructionException;
-import android.net.ip.IpClient.IpClientCallbacksWrapper;
-import android.net.metrics.ApfProgramEvent;
-import android.net.metrics.ApfStats;
-import android.net.metrics.IpConnectivityLog;
-import android.net.metrics.RaEvent;
-import android.os.PowerManager;
-import android.os.SystemClock;
-import android.stats.connectivity.NetworkQuirkEvent;
-import android.system.ErrnoException;
-import android.system.Os;
-import android.text.format.DateUtils;
-import android.util.Log;
-import android.util.SparseArray;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.HexDump;
-import com.android.internal.util.IndentingPrintWriter;
-import com.android.net.module.util.CollectionUtils;
-import com.android.net.module.util.ConnectivityUtils;
-import com.android.net.module.util.InterfaceParams;
-import com.android.net.module.util.SocketUtils;
-import com.android.networkstack.metrics.ApfSessionInfoMetrics;
-import com.android.networkstack.metrics.IpClientRaInfoMetrics;
-import com.android.networkstack.metrics.NetworkQuirkMetrics;
-import com.android.networkstack.util.NetworkStackUtils;
-
-import java.io.ByteArrayOutputStream;
-import java.io.FileDescriptor;
-import java.io.IOException;
-import java.net.Inet4Address;
-import java.net.Inet6Address;
-import java.net.InetAddress;
-import java.net.SocketAddress;
-import java.net.SocketException;
-import java.net.UnknownHostException;
-import java.nio.BufferUnderflowException;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-
-/**
- * For networks that support packet filtering via APF programs, {@code ApfFilter}
- * listens for IPv6 ICMPv6 router advertisements (RAs) and generates APF programs to
- * filter out redundant duplicate ones.
- *
- * Threading model:
- * A collection of RAs we've received is kept in mRas. Generating APF programs uses mRas to
- * know what RAs to filter for, thus generating APF programs is dependent on mRas.
- * mRas can be accessed by multiple threads:
- * - ReceiveThread, which listens for RAs and adds them to mRas, and generates APF programs.
- * - callers of:
- *    - setMulticastFilter(), which can cause an APF program to be generated.
- *    - dump(), which dumps mRas among other things.
- *    - shutdown(), which clears mRas.
- * So access to mRas is synchronized.
- *
- * @hide
- */
-public class LegacyApfFilter implements AndroidPacketFilter {
-
-    // Enums describing the outcome of receiving an RA packet.
-    private static enum ProcessRaResult {
-        MATCH,          // Received RA matched a known RA
-        DROPPED,        // Received RA ignored due to MAX_RAS
-        PARSE_ERROR,    // Received RA could not be parsed
-        ZERO_LIFETIME,  // Received RA had 0 lifetime
-        UPDATE_NEW_RA,  // APF program updated for new RA
-        UPDATE_EXPIRY   // APF program updated for expiry
-    }
-
-    /**
-     * When APFv4 is supported, loads R1 with the offset of the specified counter.
-     */
-    private void maybeSetupCounter(ApfV4Generator gen, Counter c) {
-        if (hasDataAccess(mApfVersionSupported)) {
-            gen.addLoadImmediate(R1, c.offset());
-        }
-    }
-
-    // When APFv4 is supported, these point to the trampolines generated by emitEpilogue().
-    // Otherwise, they're just aliases for PASS_LABEL and DROP_LABEL.
-    private final String mCountAndPassLabel;
-    private final String mCountAndDropLabel;
-
-    /** A wrapper class of {@link SystemClock} to be mocked in unit tests. */
-    public static class Clock {
-        /**
-         * @see SystemClock#elapsedRealtime
-         */
-        public long elapsedRealtime() {
-            return SystemClock.elapsedRealtime();
-        }
-    }
-
-    // Thread to listen for RAs.
-    @VisibleForTesting
-    public class ReceiveThread extends Thread {
-        private final byte[] mPacket = new byte[1514];
-        private final FileDescriptor mSocket;
-        private final long mStart = mClock.elapsedRealtime();
-
-        private int mReceivedRas = 0;
-        private int mMatchingRas = 0;
-        private int mDroppedRas = 0;
-        private int mParseErrors = 0;
-        private int mZeroLifetimeRas = 0;
-        private int mProgramUpdates = 0;
-
-        private volatile boolean mStopped;
-
-        public ReceiveThread(FileDescriptor socket) {
-            mSocket = socket;
-        }
-
-        public void halt() {
-            mStopped = true;
-            // Interrupts the read() call the thread is blocked in.
-            SocketUtils.closeSocketQuietly(mSocket);
-        }
-
-        @Override
-        public void run() {
-            log("begin monitoring");
-            while (!mStopped) {
-                try {
-                    int length = Os.read(mSocket, mPacket, 0, mPacket.length);
-                    updateStats(processRa(mPacket, length));
-                } catch (IOException|ErrnoException e) {
-                    if (!mStopped) {
-                        Log.e(TAG, "Read error", e);
-                    }
-                }
-            }
-            logStats();
-        }
-
-        private void updateStats(ProcessRaResult result) {
-            mReceivedRas++;
-            switch(result) {
-                case MATCH:
-                    mMatchingRas++;
-                    return;
-                case DROPPED:
-                    mDroppedRas++;
-                    return;
-                case PARSE_ERROR:
-                    mParseErrors++;
-                    return;
-                case ZERO_LIFETIME:
-                    mZeroLifetimeRas++;
-                    return;
-                case UPDATE_EXPIRY:
-                    mMatchingRas++;
-                    mProgramUpdates++;
-                    return;
-                case UPDATE_NEW_RA:
-                    mProgramUpdates++;
-                    return;
-            }
-        }
-
-        private void logStats() {
-            final long nowMs = mClock.elapsedRealtime();
-            synchronized (LegacyApfFilter.this) {
-                final ApfStats stats = new ApfStats.Builder()
-                        .setReceivedRas(mReceivedRas)
-                        .setMatchingRas(mMatchingRas)
-                        .setDroppedRas(mDroppedRas)
-                        .setParseErrors(mParseErrors)
-                        .setZeroLifetimeRas(mZeroLifetimeRas)
-                        .setProgramUpdates(mProgramUpdates)
-                        .setDurationMs(nowMs - mStart)
-                        .setMaxProgramSize(mMaximumApfProgramSize)
-                        .setProgramUpdatesAll(mNumProgramUpdates)
-                        .setProgramUpdatesAllowingMulticast(mNumProgramUpdatesAllowingMulticast)
-                        .build();
-                mMetricsLog.log(stats);
-                logApfProgramEventLocked(nowMs / DateUtils.SECOND_IN_MILLIS);
-            }
-        }
-    }
-
-    private static final String TAG = "ApfFilter";
-    private static final boolean DBG = true;
-    private static final boolean VDBG = false;
-
-    private static final int ETH_HEADER_LEN = 14;
-    private static final int ETH_DEST_ADDR_OFFSET = 0;
-    private static final int ETH_ETHERTYPE_OFFSET = 12;
-    private static final int ETH_TYPE_MIN = 0x0600;
-    private static final int ETH_TYPE_MAX = 0xFFFF;
-    // TODO: Make these offsets relative to end of link-layer header; don't include ETH_HEADER_LEN.
-    private static final int IPV4_TOTAL_LENGTH_OFFSET = ETH_HEADER_LEN + 2;
-    private static final int IPV4_FRAGMENT_OFFSET_OFFSET = ETH_HEADER_LEN + 6;
-    // Endianness is not an issue for this constant because the APF interpreter always operates in
-    // network byte order.
-    private static final int IPV4_FRAGMENT_OFFSET_MASK = 0x1fff;
-    private static final int IPV4_PROTOCOL_OFFSET = ETH_HEADER_LEN + 9;
-    private static final int IPV4_DEST_ADDR_OFFSET = ETH_HEADER_LEN + 16;
-    private static final int IPV4_ANY_HOST_ADDRESS = 0;
-    private static final int IPV4_BROADCAST_ADDRESS = -1; // 255.255.255.255
-    private static final int IPV4_HEADER_LEN = 20; // Without options
-
-    // Traffic class and Flow label are not byte aligned. Luckily we
-    // don't care about either value so we'll consider bytes 1-3 of the
-    // IPv6 header as don't care.
-    private static final int IPV6_FLOW_LABEL_OFFSET = ETH_HEADER_LEN + 1;
-    private static final int IPV6_FLOW_LABEL_LEN = 3;
-    private static final int IPV6_NEXT_HEADER_OFFSET = ETH_HEADER_LEN + 6;
-    private static final int IPV6_SRC_ADDR_OFFSET = ETH_HEADER_LEN + 8;
-    private static final int IPV6_DEST_ADDR_OFFSET = ETH_HEADER_LEN + 24;
-    private static final int IPV6_HEADER_LEN = 40;
-    // The IPv6 all nodes address ff02::1
-    private static final byte[] IPV6_ALL_NODES_ADDRESS =
-            { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
-
-    private static final int ICMP6_TYPE_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN;
-
-    private static final int IPPROTO_HOPOPTS = 0;
-
-    // NOTE: this must be added to the IPv4 header length in MemorySlot.IPV4_HEADER_SIZE
-    private static final int UDP_DESTINATION_PORT_OFFSET = ETH_HEADER_LEN + 2;
-    private static final int UDP_HEADER_LEN = 8;
-
-    private static final int TCP_HEADER_SIZE_OFFSET = 12;
-
-    private static final int DHCP_CLIENT_PORT = 68;
-    // NOTE: this must be added to the IPv4 header length in MemorySlot.IPV4_HEADER_SIZE
-    private static final int DHCP_CLIENT_MAC_OFFSET = ETH_HEADER_LEN + UDP_HEADER_LEN + 28;
-
-    private static final int ARP_HEADER_OFFSET = ETH_HEADER_LEN;
-    private static final byte[] ARP_IPV4_HEADER = {
-            0, 1, // Hardware type: Ethernet (1)
-            8, 0, // Protocol type: IP (0x0800)
-            6,    // Hardware size: 6
-            4,    // Protocol size: 4
-    };
-    private static final int ARP_OPCODE_OFFSET = ARP_HEADER_OFFSET + 6;
-    // Opcode: ARP request (0x0001), ARP reply (0x0002)
-    private static final short ARP_OPCODE_REQUEST = 1;
-    private static final short ARP_OPCODE_REPLY = 2;
-    private static final int ARP_SOURCE_IP_ADDRESS_OFFSET = ARP_HEADER_OFFSET + 14;
-    private static final int ARP_TARGET_IP_ADDRESS_OFFSET = ARP_HEADER_OFFSET + 24;
-    // Do not log ApfProgramEvents whose actual lifetimes was less than this.
-    private static final int APF_PROGRAM_EVENT_LIFETIME_THRESHOLD = 2;
-    // Limit on the Black List size to cap on program usage for this
-    // TODO: Select a proper max length
-    private static final int APF_MAX_ETH_TYPE_BLACK_LIST_LEN = 20;
-
-    private static final byte[] ETH_MULTICAST_MDNS_V4_MAC_ADDRESS =
-            {(byte) 0x01, (byte) 0x00, (byte) 0x5e, (byte) 0x00, (byte) 0x00, (byte) 0xfb};
-    private static final byte[] ETH_MULTICAST_MDNS_V6_MAC_ADDRESS =
-            {(byte) 0x33, (byte) 0x33, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xfb};
-    private static final int MDNS_PORT = 5353;
-    private static final int DNS_HEADER_LEN = 12;
-    private static final int DNS_QDCOUNT_OFFSET = 4;
-    // NOTE: this must be added to the IPv4 header length in MemorySlot.IPV4_HEADER_SIZE, or the
-    // IPv6 header length.
-    private static final int MDNS_QDCOUNT_OFFSET =
-            ETH_HEADER_LEN + UDP_HEADER_LEN + DNS_QDCOUNT_OFFSET;
-    private static final int MDNS_QNAME_OFFSET =
-            ETH_HEADER_LEN + UDP_HEADER_LEN + DNS_HEADER_LEN;
-
-
-    public final int mApfVersionSupported;
-    public final int mMaximumApfProgramSize;
-    private final IpClientCallbacksWrapper mIpClientCallback;
-    private final InterfaceParams mInterfaceParams;
-    private final IpConnectivityLog mMetricsLog;
-
-    @VisibleForTesting
-    public byte[] mHardwareAddress;
-    @VisibleForTesting
-    public ReceiveThread mReceiveThread;
-    @GuardedBy("this")
-    private long mUniqueCounter;
-    @GuardedBy("this")
-    private boolean mMulticastFilter;
-    @GuardedBy("this")
-    private boolean mInDozeMode;
-    private final boolean mDrop802_3Frames;
-    private final int[] mEthTypeBlackList;
-
-    private final ApfCounterTracker mApfCounterTracker = new ApfCounterTracker();
-    @GuardedBy("this")
-    private long mSessionStartMs = 0;
-    @GuardedBy("this")
-    private int mNumParseErrorRas = 0;
-    @GuardedBy("this")
-    private int mNumZeroLifetimeRas = 0;
-    @GuardedBy("this")
-    private int mLowestRouterLifetimeSeconds = Integer.MAX_VALUE;
-    @GuardedBy("this")
-    private long mLowestPioValidLifetimeSeconds = Long.MAX_VALUE;
-    @GuardedBy("this")
-    private long mLowestRioRouteLifetimeSeconds = Long.MAX_VALUE;
-    @GuardedBy("this")
-    private long mLowestRdnssLifetimeSeconds = Long.MAX_VALUE;
-
-    // Ignore non-zero RDNSS lifetimes below this value.
-    private final int mMinRdnssLifetimeSec;
-
-    // Minimum session time for metrics, duration less than this time will not be logged.
-    private final long mMinMetricsSessionDurationMs;
-
-    private final Clock mClock;
-    private final NetworkQuirkMetrics mNetworkQuirkMetrics;
-    private final IpClientRaInfoMetrics mIpClientRaInfoMetrics;
-    private final ApfSessionInfoMetrics mApfSessionInfoMetrics;
-
-    // Detects doze mode state transitions.
-    private final BroadcastReceiver mDeviceIdleReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            String action = intent.getAction();
-            if (action.equals(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED)) {
-                PowerManager powerManager =
-                        (PowerManager) context.getSystemService(Context.POWER_SERVICE);
-                final boolean deviceIdle = powerManager.isDeviceIdleMode();
-                setDozeMode(deviceIdle);
-            }
-        }
-    };
-    private final Context mContext;
-
-    // Our IPv4 address, if we have just one, otherwise null.
-    @GuardedBy("this")
-    private byte[] mIPv4Address;
-    // The subnet prefix length of our IPv4 network. Only valid if mIPv4Address is not null.
-    @GuardedBy("this")
-    private int mIPv4PrefixLength;
-
-    // mIsRunning is reflects the state of the LegacyApfFilter during integration tests.
-    // LegacyApfFilter can be paused using "adb shell cmd apf <iface> <cmd>" commands. A paused
-    // LegacyApfFilter will not install any new programs, but otherwise operates normally.
-    private volatile boolean mIsRunning = true;
-
-    private final ApfFilter.Dependencies mDependencies;
-
-    @VisibleForTesting
-    public LegacyApfFilter(Context context, ApfFilter.ApfConfiguration config,
-            InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback,
-            IpConnectivityLog log, NetworkQuirkMetrics networkQuirkMetrics) {
-        this(context, config, ifParams, ipClientCallback, log, networkQuirkMetrics,
-                new ApfFilter.Dependencies(context), new Clock());
-    }
-
-    @VisibleForTesting
-    public LegacyApfFilter(Context context, ApfFilter.ApfConfiguration config,
-            InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback,
-            IpConnectivityLog log, NetworkQuirkMetrics networkQuirkMetrics,
-            ApfFilter.Dependencies dependencies, Clock clock) {
-        mApfVersionSupported = config.apfVersionSupported;
-        mMaximumApfProgramSize = config.apfRamSize;
-        mIpClientCallback = ipClientCallback;
-        mInterfaceParams = ifParams;
-        mMulticastFilter = config.multicastFilter;
-        mDrop802_3Frames = config.ieee802_3Filter;
-        mMinRdnssLifetimeSec = config.minRdnssLifetimeSec;
-        mContext = context;
-        mClock = clock;
-        mDependencies = dependencies;
-        mNetworkQuirkMetrics = networkQuirkMetrics;
-        mIpClientRaInfoMetrics = dependencies.getIpClientRaInfoMetrics();
-        mApfSessionInfoMetrics = dependencies.getApfSessionInfoMetrics();
-        mSessionStartMs = mClock.elapsedRealtime();
-        mMinMetricsSessionDurationMs = config.minMetricsSessionDurationMs;
-
-        if (hasDataAccess(mApfVersionSupported)) {
-            mCountAndPassLabel = "countAndPass";
-            mCountAndDropLabel = "countAndDrop";
-        } else {
-            // APFv4 unsupported: turn jumps to the counter trampolines to immediately PASS or DROP,
-            // preserving the original pre-APFv4 behavior.
-            mCountAndPassLabel = ApfV4Generator.PASS_LABEL;
-            mCountAndDropLabel = ApfV4Generator.DROP_LABEL;
-        }
-
-        // Now fill the black list from the passed array
-        mEthTypeBlackList = filterEthTypeBlackList(config.ethTypeBlackList);
-
-        mMetricsLog = log;
-
-        // TODO: ApfFilter should not generate programs until IpClient sends provisioning success.
-        maybeStartFilter();
-
-        // Listen for doze-mode transition changes to enable/disable the IPv6 multicast filter.
-        mContext.registerReceiver(mDeviceIdleReceiver,
-                new IntentFilter(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED));
-
-        mDependencies.onApfFilterCreated(this);
-        // mReceiveThread is created in maybeStartFilter() and halted in shutdown().
-        mDependencies.onThreadCreated(mReceiveThread);
-    }
-
-    @Override
-    public synchronized String setDataSnapshot(byte[] data) {
-        mDataSnapshot = data;
-        if (mIsRunning) {
-            mApfCounterTracker.updateCountersFromData(data);
-        }
-        return mApfCounterTracker.getCounters().toString();
-    }
-
-    private void log(String s) {
-        Log.d(TAG, "(" + mInterfaceParams.name + "): " + s);
-    }
-
-    @GuardedBy("this")
-    private long getUniqueNumberLocked() {
-        return mUniqueCounter++;
-    }
-
-    private static int[] filterEthTypeBlackList(int[] ethTypeBlackList) {
-        ArrayList<Integer> bl = new ArrayList<Integer>();
-
-        for (int p : ethTypeBlackList) {
-            // Check if the protocol is a valid ether type
-            if ((p < ETH_TYPE_MIN) || (p > ETH_TYPE_MAX)) {
-                continue;
-            }
-
-            // Check if the protocol is not repeated in the passed array
-            if (bl.contains(p)) {
-                continue;
-            }
-
-            // Check if list reach its max size
-            if (bl.size() == APF_MAX_ETH_TYPE_BLACK_LIST_LEN) {
-                Log.w(TAG, "Passed EthType Black List size too large (" + bl.size() +
-                        ") using top " + APF_MAX_ETH_TYPE_BLACK_LIST_LEN + " protocols");
-                break;
-            }
-
-            // Now add the protocol to the list
-            bl.add(p);
-        }
-
-        return bl.stream().mapToInt(Integer::intValue).toArray();
-    }
-
-    /**
-     * Attempt to start listening for RAs and, if RAs are received, generating and installing
-     * 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(mApfVersionSupported)) {
-                    byte[] zeroes = new byte[mMaximumApfProgramSize];
-                    if (!mIpClientCallback.installPacketFilter(zeroes)) {
-                        sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
-                    }
-                }
-
-                // Install basic filters
-                installNewProgramLocked();
-            }
-            socket = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IPV6);
-            SocketAddress addr = makePacketSocketAddress(ETH_P_IPV6, mInterfaceParams.index);
-            Os.bind(socket, addr);
-            NetworkStackUtils.attachRaFilter(socket);
-        } catch(SocketException|ErrnoException e) {
-            Log.e(TAG, "Error starting filter", e);
-            return;
-        }
-        mReceiveThread = new ReceiveThread(socket);
-        mReceiveThread.start();
-    }
-
-    // Returns seconds since device boot.
-    @VisibleForTesting
-    protected long currentTimeSeconds() {
-        return mClock.elapsedRealtime() / DateUtils.SECOND_IN_MILLIS;
-    }
-
-    public static class InvalidRaException extends Exception {
-        public InvalidRaException(String m) {
-            super(m);
-        }
-    }
-
-    /**
-     *  Class to keep track of a section in a packet.
-     */
-    private static class PacketSection {
-        public enum Type {
-            MATCH,     // A field that should be matched (e.g., the router IP address).
-            IGNORE,    // An ignored field such as the checksum of the flow label. Not matched.
-            LIFETIME,  // A lifetime. Not matched, and generally counts toward minimum RA lifetime.
-        }
-
-        /** The type of section. */
-        public final Type type;
-        /** Offset into the packet at which this section begins. */
-        public final int start;
-        /** Length of this section in bytes. */
-        public final int length;
-        /** If this is a lifetime, the ICMP option that defined it. 0 for router lifetime. */
-        public final int option;
-        /** If this is a lifetime, the lifetime value. */
-        public final long lifetime;
-
-        PacketSection(int start, int length, Type type, int option, long lifetime) {
-            this.start = start;
-            this.length = length;
-            this.type = type;
-            this.option = option;
-            this.lifetime = lifetime;
-        }
-
-        public String toString() {
-            if (type == Type.LIFETIME) {
-                return String.format("%s: (%d, %d) %d %d", type, start, length, option, lifetime);
-            } else {
-                return String.format("%s: (%d, %d)", type, start, length);
-            }
-        }
-    }
-
-    // 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_LEN = 32;
-        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_OFFSET = 8;
-        private static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN = 4;
-
-        // From RFC6106: Recursive DNS Server option
-        private static final int ICMP6_RDNSS_OPTION_TYPE = 25;
-        // From RFC6106: DNS Search List option
-        private static final int ICMP6_DNSSL_OPTION_TYPE = 31;
-
-        // From RFC4191: Route Information option
-        private static final int ICMP6_ROUTE_INFO_OPTION_TYPE = 24;
-        // Above three options all have the same format:
-        private static final int ICMP6_4_BYTE_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;
-
-        // List of sections in the packet.
-        private final ArrayList<PacketSection> mPacketSections = new ArrayList<>();
-
-        // Router lifetime in packet
-        private final int mRouterLifetime;
-        // Minimum valid lifetime of PIOs in packet, Long.MAX_VALUE means not seen.
-        private long mMinPioValidLifetime = Long.MAX_VALUE;
-        // Minimum route lifetime of RIOs in packet, Long.MAX_VALUE means not seen.
-        private long mMinRioRouteLifetime = Long.MAX_VALUE;
-        // Minimum lifetime of RDNSSs in packet, Long.MAX_VALUE means not seen.
-        private long mMinRdnssLifetime = Long.MAX_VALUE;
-        // Minimum lifetime in packet
-        long mMinLifetime;
-        // When the packet was last captured, in seconds since Unix Epoch
-        long mLastSeen;
-
-        // For debugging only. Offsets into the packet where PIOs are.
-        private final ArrayList<Integer> mPrefixOptionOffsets = new ArrayList<>();
-
-        // For debugging only. Offsets into the packet where RDNSS options are.
-        private final ArrayList<Integer> mRdnssOptionOffsets = new ArrayList<>();
-
-        // For debugging only. Offsets into the packet where RIO options are.
-        private final ArrayList<Integer> mRioOptionOffsets = new ArrayList<>();
-
-        // For debugging only. How many times this RA was seen.
-        int seenCount = 0;
-
-        // For debugging only. Returns the hex representation of the last matching packet.
-        String getLastMatchingPacket() {
-            return HexDump.toHexString(mPacket.array(), 0, mPacket.capacity(),
-                    false /* lowercase */);
-        }
-
-        // For debugging only. Returns the string representation of the IPv6 address starting at
-        // position pos in the packet.
-        private String IPv6AddresstoString(int pos) {
-            try {
-                byte[] array = mPacket.array();
-                // Can't just call copyOfRange() and see if it throws, because if it reads past the
-                // end it pads with zeros instead of throwing.
-                if (pos < 0 || pos + 16 > array.length || pos + 16 < pos) {
-                    return "???";
-                }
-                byte[] addressBytes = Arrays.copyOfRange(array, pos, pos + 16);
-                InetAddress address = (Inet6Address) InetAddress.getByAddress(addressBytes);
-                return address.getHostAddress();
-            } catch (UnsupportedOperationException e) {
-                // array() failed. Cannot happen, mPacket is array-backed and read-write.
-                return "???";
-            } catch (ClassCastException|UnknownHostException e) {
-                // Cannot happen.
-                return "???";
-            }
-        }
-
-        // Can't be static because it's in a non-static inner class.
-        // TODO: Make this static once RA is its own class.
-        private void prefixOptionToString(StringBuffer sb, int offset) {
-            String prefix = IPv6AddresstoString(offset + 16);
-            int length = getUint8(mPacket, offset + 2);
-            long valid = getUint32(mPacket, offset + 4);
-            long preferred = getUint32(mPacket, offset + 8);
-            sb.append(String.format("%s/%d %ds/%ds ", prefix, length, valid, preferred));
-        }
-
-        private void rdnssOptionToString(StringBuffer sb, int offset) {
-            int optLen = getUint8(mPacket, offset + 1) * 8;
-            if (optLen < 24) return;  // Malformed or empty.
-            long lifetime = getUint32(mPacket, offset + 4);
-            int numServers = (optLen - 8) / 16;
-            sb.append("DNS ").append(lifetime).append("s");
-            for (int server = 0; server < numServers; server++) {
-                sb.append(" ").append(IPv6AddresstoString(offset + 8 + 16 * server));
-            }
-            sb.append(" ");
-        }
-
-        private void rioOptionToString(StringBuffer sb, int offset) {
-            int optLen = getUint8(mPacket, offset + 1) * 8;
-            if (optLen < 8 || optLen > 24) return;  // Malformed or empty.
-            int prefixLen = getUint8(mPacket, offset + 2);
-            long lifetime = getUint32(mPacket, offset + 4);
-
-            // This read is variable length because the prefix can be 0, 8 or 16 bytes long.
-            // We can't use any of the ByteBuffer#get methods here because they all start reading
-            // from the buffer's current position.
-            byte[] prefix = new byte[IPV6_ADDR_LEN];
-            System.arraycopy(mPacket.array(), offset + 8, prefix, 0, optLen - 8);
-            sb.append("RIO ").append(lifetime).append("s ");
-            try {
-                InetAddress address = (Inet6Address) InetAddress.getByAddress(prefix);
-                sb.append(address.getHostAddress());
-            } catch (UnknownHostException impossible) {
-                sb.append("???");
-            }
-            sb.append("/").append(prefixLen).append(" ");
-        }
-
-        public String toString() {
-            try {
-                StringBuffer sb = new StringBuffer();
-                sb.append(String.format("RA %s -> %s %ds ",
-                        IPv6AddresstoString(IPV6_SRC_ADDR_OFFSET),
-                        IPv6AddresstoString(IPV6_DEST_ADDR_OFFSET),
-                        getUint16(mPacket, ICMP6_RA_ROUTER_LIFETIME_OFFSET)));
-                for (int i: mPrefixOptionOffsets) {
-                    prefixOptionToString(sb, i);
-                }
-                for (int i: mRdnssOptionOffsets) {
-                    rdnssOptionToString(sb, i);
-                }
-                for (int i: mRioOptionOffsets) {
-                    rioOptionToString(sb, i);
-                }
-                return sb.toString();
-            } catch (BufferUnderflowException|IndexOutOfBoundsException e) {
-                return "<Malformed RA>";
-            }
-        }
-
-        /**
-         * Add a packet section that should be matched, starting from the current position.
-         * @param length the length of the section
-         */
-        private void addMatchSection(int length) {
-            // Don't generate JNEBS instruction for 0 bytes as they will fail the
-            // ASSERT_FORWARD_IN_PROGRAM(pc + cmp_imm - 1) check (where cmp_imm is
-            // the number of bytes to compare) and immediately pass the packet.
-            // The code does not attempt to generate such matches, but add a safety
-            // check to prevent doing so in the presence of bugs or malformed or
-            // truncated packets.
-            if (length == 0) return;
-            mPacketSections.add(
-                    new PacketSection(mPacket.position(), length, PacketSection.Type.MATCH, 0, 0));
-            mPacket.position(mPacket.position() + length);
-        }
-
-        /**
-         * Add a packet section that should be matched, starting from the current position.
-         * @param end the offset in the packet before which the section ends
-         */
-        private void addMatchUntil(int end) {
-            addMatchSection(end - mPacket.position());
-        }
-
-        /**
-         * Add a packet section that should be ignored, starting from the current position.
-         * @param length the length of the section in bytes
-         */
-        private void addIgnoreSection(int length) {
-            mPacketSections.add(
-                    new PacketSection(mPacket.position(), length, PacketSection.Type.IGNORE, 0, 0));
-            mPacket.position(mPacket.position() + length);
-        }
-
-        /**
-         * Add a packet section that represents a lifetime, starting from the current position.
-         * @param length the length of the section in bytes
-         * @param optionType the RA option containing this lifetime, or 0 for router lifetime
-         * @param lifetime the lifetime
-         */
-        private void addLifetimeSection(int length, int optionType, long lifetime) {
-            mPacketSections.add(
-                    new PacketSection(mPacket.position(), length, PacketSection.Type.LIFETIME,
-                            optionType, lifetime));
-            mPacket.position(mPacket.position() + length);
-        }
-
-        /**
-         * Adds packet sections for an RA option with a 4-byte lifetime 4 bytes into the option
-         * @param optionType the RA option that is being added
-         * @param optionLength the length of the option in bytes
-         */
-        private long add4ByteLifetimeOption(int optionType, int optionLength) {
-            addMatchSection(ICMP6_4_BYTE_LIFETIME_OFFSET);
-            final long lifetime = getUint32(mPacket, mPacket.position());
-            addLifetimeSection(ICMP6_4_BYTE_LIFETIME_LEN, optionType, lifetime);
-            addMatchSection(optionLength - ICMP6_4_BYTE_LIFETIME_OFFSET
-                    - ICMP6_4_BYTE_LIFETIME_LEN);
-            return lifetime;
-        }
-
-        /**
-         * Return the router lifetime of the RA
-         */
-        public int routerLifetime() {
-            return mRouterLifetime;
-        }
-
-        /**
-         * Return the minimum valid lifetime in PIOs
-         */
-        public long minPioValidLifetime() {
-            return mMinPioValidLifetime;
-        }
-
-        /**
-         * Return the minimum route lifetime in RIOs
-         */
-        public long minRioRouteLifetime() {
-            return mMinRioRouteLifetime;
-        }
-
-        /**
-         * Return the minimum lifetime in RDNSSs
-         */
-        public long minRdnssLifetime() {
-            return mMinRdnssLifetime;
-        }
-
-        // http://b/66928272 http://b/65056012
-        // DnsServerRepository ignores RDNSS servers with lifetimes that are too low. Ignore these
-        // lifetimes for the purpose of filter lifetime calculations.
-        private boolean shouldIgnoreLifetime(int optionType, long lifetime) {
-            return optionType == ICMP6_RDNSS_OPTION_TYPE
-                    && lifetime != 0 && lifetime < mMinRdnssLifetimeSec;
-        }
-
-        private boolean isRelevantLifetime(PacketSection section) {
-            return section.type == PacketSection.Type.LIFETIME
-                    && !shouldIgnoreLifetime(section.option, section.lifetime);
-        }
-
-        // Note that this parses RA and may throw InvalidRaException (from
-        // Buffer.position(int) or due to an invalid-length option) or IndexOutOfBoundsException
-        // (from ByteBuffer.get(int) ) if parsing encounters something non-compliant with
-        // specifications.
-        @VisibleForTesting
-        public Ra(byte[] packet, int length) throws InvalidRaException {
-            if (length < ICMP6_RA_OPTION_OFFSET) {
-                throw new InvalidRaException("Not an ICMP6 router advertisement: too short");
-            }
-
-            mPacket = ByteBuffer.wrap(Arrays.copyOf(packet, length));
-            mLastSeen = currentTimeSeconds();
-
-            // Check packet in case a packet arrives before we attach RA filter
-            // to our packet socket. b/29586253
-            if (getUint16(mPacket, ETH_ETHERTYPE_OFFSET) != ETH_P_IPV6 ||
-                    getUint8(mPacket, IPV6_NEXT_HEADER_OFFSET) != IPPROTO_ICMPV6 ||
-                    getUint8(mPacket, ICMP6_TYPE_OFFSET) != ICMPV6_ROUTER_ADVERTISEMENT) {
-                throw new InvalidRaException("Not an ICMP6 router advertisement");
-            }
-
-
-            RaEvent.Builder builder = new RaEvent.Builder();
-
-            // Ignore the flow label and low 4 bits of traffic class.
-            addMatchUntil(IPV6_FLOW_LABEL_OFFSET);
-            addIgnoreSection(IPV6_FLOW_LABEL_LEN);
-
-            // Ignore checksum.
-            addMatchUntil(ICMP6_RA_CHECKSUM_OFFSET);
-            addIgnoreSection(ICMP6_RA_CHECKSUM_LEN);
-
-            // Parse router lifetime
-            addMatchUntil(ICMP6_RA_ROUTER_LIFETIME_OFFSET);
-            mRouterLifetime = getUint16(mPacket, ICMP6_RA_ROUTER_LIFETIME_OFFSET);
-            addLifetimeSection(ICMP6_RA_ROUTER_LIFETIME_LEN, 0, mRouterLifetime);
-            builder.updateRouterLifetime(mRouterLifetime);
-
-            // Add remaining fields (reachable time and retransmission timer) to match section.
-            addMatchUntil(ICMP6_RA_OPTION_OFFSET);
-
-            while (mPacket.hasRemaining()) {
-                final int position = mPacket.position();
-                final int optionType = getUint8(mPacket, position);
-                final int optionLength = getUint8(mPacket, position + 1) * 8;
-                long lifetime;
-                switch (optionType) {
-                    case ICMP6_PREFIX_OPTION_TYPE:
-                        mPrefixOptionOffsets.add(position);
-
-                        // Parse valid lifetime
-                        addMatchSection(ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET);
-                        lifetime = getUint32(mPacket, mPacket.position());
-                        addLifetimeSection(ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN,
-                                ICMP6_PREFIX_OPTION_TYPE, lifetime);
-                        builder.updatePrefixValidLifetime(lifetime);
-                        mMinPioValidLifetime = getMinForPositiveValue(
-                                mMinPioValidLifetime, lifetime);
-
-                        // Parse preferred lifetime
-                        lifetime = getUint32(mPacket, mPacket.position());
-                        addLifetimeSection(ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN,
-                                ICMP6_PREFIX_OPTION_TYPE, lifetime);
-                        builder.updatePrefixPreferredLifetime(lifetime);
-
-                        addMatchSection(4);       // Reserved bytes
-                        addMatchSection(IPV6_ADDR_LEN);  // The prefix itself
-                        break;
-                    // These three options have the same lifetime offset and size, and
-                    // are processed with the same specialized add4ByteLifetimeOption:
-                    case ICMP6_RDNSS_OPTION_TYPE:
-                        mRdnssOptionOffsets.add(position);
-                        lifetime = add4ByteLifetimeOption(optionType, optionLength);
-                        builder.updateRdnssLifetime(lifetime);
-                        mMinRdnssLifetime = getMinForPositiveValue(mMinRdnssLifetime, lifetime);
-                        break;
-                    case ICMP6_ROUTE_INFO_OPTION_TYPE:
-                        mRioOptionOffsets.add(position);
-                        lifetime = add4ByteLifetimeOption(optionType, optionLength);
-                        builder.updateRouteInfoLifetime(lifetime);
-                        mMinRioRouteLifetime = getMinForPositiveValue(
-                                mMinRioRouteLifetime, lifetime);
-                        break;
-                    case ICMP6_DNSSL_OPTION_TYPE:
-                        lifetime = add4ByteLifetimeOption(optionType, optionLength);
-                        builder.updateDnsslLifetime(lifetime);
-                        break;
-                    default:
-                        // RFC4861 section 4.2 dictates we ignore unknown options for forwards
-                        // compatibility.
-                        mPacket.position(position + optionLength);
-                        break;
-                }
-                if (optionLength <= 0) {
-                    throw new InvalidRaException(String.format(
-                        "Invalid option length opt=%d len=%d", optionType, optionLength));
-                }
-            }
-            mMinLifetime = minLifetime();
-            mMetricsLog.log(builder.build());
-        }
-
-        // Considering only the MATCH sections, does {@code packet} match this RA?
-        boolean matches(byte[] packet, int length) {
-            if (length != mPacket.capacity()) return false;
-            byte[] referencePacket = mPacket.array();
-            for (PacketSection section : mPacketSections) {
-                if (section.type != PacketSection.Type.MATCH) continue;
-                for (int i = section.start; i < (section.start + section.length); i++) {
-                    if (packet[i] != referencePacket[i]) return false;
-                }
-            }
-            return true;
-        }
-
-        // What is the minimum of all lifetimes within {@code packet} in seconds?
-        // Precondition: matches(packet, length) already returned true.
-        long minLifetime() {
-            long minLifetime = Long.MAX_VALUE;
-            for (PacketSection section : mPacketSections) {
-                if (isRelevantLifetime(section)) {
-                    minLifetime = Math.min(minLifetime, section.lifetime);
-                }
-            }
-            return minLifetime;
-        }
-
-        // How many seconds does this RA's have to live, taking into account the fact
-        // that we might have seen it a while ago.
-        long currentLifetime() {
-            return mMinLifetime - (currentTimeSeconds() - mLastSeen);
-        }
-
-        boolean isExpired() {
-            // TODO: We may want to handle 0 lifetime RAs differently, if they are common. We'll
-            // have to calculate the filter lifetime specially as a fraction of 0 is still 0.
-            return currentLifetime() <= 0;
-        }
-
-        // Filter for a fraction of the lifetime and adjust for the age of the RA.
-        @GuardedBy("LegacyApfFilter.this")
-        int filterLifetime() {
-            return (int) (mMinLifetime / FRACTION_OF_LIFETIME_TO_FILTER)
-                    - (int) (mProgramBaseTime - mLastSeen);
-        }
-
-        @GuardedBy("LegacyApfFilter.this")
-        boolean shouldFilter() {
-            return filterLifetime() > 0;
-        }
-
-        // Append a filter for this RA to {@code gen}. Jump to DROP_LABEL if it should be dropped.
-        // Jump to the next filter if packet doesn't match this RA.
-        // Return Long.MAX_VALUE if we don't install any filter program for this RA. As the return
-        // value of this function is used to calculate the program min lifetime (which corresponds
-        // to the smallest generated filter lifetime). Returning Long.MAX_VALUE in the case no
-        // filter gets generated makes sure the program lifetime stays unaffected.
-        @GuardedBy("LegacyApfFilter.this")
-        long generateFilterLocked(ApfV4Generator gen) throws IllegalInstructionException {
-            String nextFilterLabel = "Ra" + getUniqueNumberLocked();
-            // Skip if packet is not the right size
-            gen.addLoadFromMemory(R0, MemorySlot.PACKET_SIZE);
-            gen.addJumpIfR0NotEquals(mPacket.capacity(), nextFilterLabel);
-            // Skip filter if expired
-            gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_SECONDS);
-            gen.addJumpIfR0GreaterThan(filterLifetime(), nextFilterLabel);
-            for (PacketSection section : mPacketSections) {
-                // Generate code to match the packet bytes.
-                if (section.type == PacketSection.Type.MATCH) {
-                    gen.addLoadImmediate(R0, section.start);
-                    gen.addJumpIfBytesAtR0NotEqual(
-                            Arrays.copyOfRange(mPacket.array(), section.start,
-                                    section.start + section.length),
-                            nextFilterLabel);
-                }
-
-                // Generate code to test the lifetimes haven't gone down too far.
-                // The packet is accepted if any non-ignored lifetime is lower than filterLifetime.
-                if (isRelevantLifetime(section)) {
-                    switch (section.length) {
-                        case 4: gen.addLoad32(R0, section.start); break;
-                        case 2: gen.addLoad16(R0, section.start); break;
-                        default:
-                            throw new IllegalStateException(
-                                    "bogus lifetime size " + section.length);
-                    }
-                    gen.addJumpIfR0LessThan(filterLifetime(), nextFilterLabel);
-                }
-            }
-            maybeSetupCounter(gen, Counter.DROPPED_RA);
-            gen.addJump(mCountAndDropLabel);
-            gen.defineLabel(nextFilterLabel);
-            return filterLifetime();
-        }
-    }
-
-    // TODO: Refactor these subclasses to avoid so much repetition.
-    private abstract static class KeepalivePacket {
-        // Note that the offset starts from IP header.
-        // These must be added ether header length when generating program.
-        static final int IP_HEADER_OFFSET = 0;
-        static final int IPV4_SRC_ADDR_OFFSET = IP_HEADER_OFFSET + 12;
-
-        // Append a filter for this keepalive ack to {@code gen}.
-        // Jump to drop if it matches the keepalive ack.
-        // Jump to the next filter if packet doesn't match the keepalive ack.
-        abstract void generateFilterLocked(ApfV4Generator gen) throws IllegalInstructionException;
-    }
-
-    // A class to hold NAT-T keepalive ack information.
-    private class NattKeepaliveResponse extends KeepalivePacket {
-        static final int UDP_LENGTH_OFFSET = 4;
-        static final int UDP_HEADER_LEN = 8;
-
-        protected class NattKeepaliveResponseData {
-            public final byte[] srcAddress;
-            public final int srcPort;
-            public final byte[] dstAddress;
-            public final int dstPort;
-
-            NattKeepaliveResponseData(final NattKeepalivePacketDataParcelable sentKeepalivePacket) {
-                srcAddress = sentKeepalivePacket.dstAddress;
-                srcPort = sentKeepalivePacket.dstPort;
-                dstAddress = sentKeepalivePacket.srcAddress;
-                dstPort = sentKeepalivePacket.srcPort;
-            }
-        }
-
-        protected final NattKeepaliveResponseData mPacket;
-        protected final byte[] mSrcDstAddr;
-        protected final byte[] mPortFingerprint;
-        // NAT-T keepalive packet
-        protected final byte[] mPayload = {(byte) 0xff};
-
-        NattKeepaliveResponse(final NattKeepalivePacketDataParcelable sentKeepalivePacket) {
-            mPacket = new NattKeepaliveResponseData(sentKeepalivePacket);
-            mSrcDstAddr = concatArrays(mPacket.srcAddress, mPacket.dstAddress);
-            mPortFingerprint = generatePortFingerprint(mPacket.srcPort, mPacket.dstPort);
-        }
-
-        byte[] generatePortFingerprint(int srcPort, int dstPort) {
-            final ByteBuffer fp = ByteBuffer.allocate(4);
-            fp.order(ByteOrder.BIG_ENDIAN);
-            fp.putShort((short) srcPort);
-            fp.putShort((short) dstPort);
-            return fp.array();
-        }
-
-        @Override
-        @GuardedBy("LegacyApfFilter.this")
-        void generateFilterLocked(ApfV4Generator gen) throws IllegalInstructionException {
-            final String nextFilterLabel = "natt_keepalive_filter" + getUniqueNumberLocked();
-
-            gen.addLoadImmediate(R0, ETH_HEADER_LEN + IPV4_SRC_ADDR_OFFSET);
-            gen.addJumpIfBytesAtR0NotEqual(mSrcDstAddr, nextFilterLabel);
-
-            // A NAT-T keepalive packet contains 1 byte payload with the value 0xff
-            // Check payload length is 1
-            gen.addLoadFromMemory(R0, MemorySlot.IPV4_HEADER_SIZE);
-            gen.addAdd(UDP_HEADER_LEN);
-            gen.addSwap();
-            gen.addLoad16(R0, IPV4_TOTAL_LENGTH_OFFSET);
-            gen.addNeg(R1);
-            gen.addAddR1ToR0();
-            gen.addJumpIfR0NotEquals(1, nextFilterLabel);
-
-            // Check that the ports match
-            gen.addLoadFromMemory(R0, MemorySlot.IPV4_HEADER_SIZE);
-            gen.addAdd(ETH_HEADER_LEN);
-            gen.addJumpIfBytesAtR0NotEqual(mPortFingerprint, nextFilterLabel);
-
-            // Payload offset = R0 + UDP header length
-            gen.addAdd(UDP_HEADER_LEN);
-            gen.addJumpIfBytesAtR0NotEqual(mPayload, nextFilterLabel);
-
-            maybeSetupCounter(gen, Counter.DROPPED_IPV4_NATT_KEEPALIVE);
-            gen.addJump(mCountAndDropLabel);
-            gen.defineLabel(nextFilterLabel);
-        }
-
-        public String toString() {
-            try {
-                return String.format("%s -> %s",
-                        ConnectivityUtils.addressAndPortToString(
-                                InetAddress.getByAddress(mPacket.srcAddress), mPacket.srcPort),
-                        ConnectivityUtils.addressAndPortToString(
-                                InetAddress.getByAddress(mPacket.dstAddress), mPacket.dstPort));
-            } catch (UnknownHostException e) {
-                return "Unknown host";
-            }
-        }
-    }
-
-    // A class to hold TCP keepalive ack information.
-    private abstract static class TcpKeepaliveAck extends KeepalivePacket {
-        protected static class TcpKeepaliveAckData {
-            public final byte[] srcAddress;
-            public final int srcPort;
-            public final byte[] dstAddress;
-            public final int dstPort;
-            public final int seq;
-            public final int ack;
-
-            // Create the characteristics of the ack packet from the sent keepalive packet.
-            TcpKeepaliveAckData(final TcpKeepalivePacketDataParcelable sentKeepalivePacket) {
-                srcAddress = sentKeepalivePacket.dstAddress;
-                srcPort = sentKeepalivePacket.dstPort;
-                dstAddress = sentKeepalivePacket.srcAddress;
-                dstPort = sentKeepalivePacket.srcPort;
-                seq = sentKeepalivePacket.ack;
-                ack = sentKeepalivePacket.seq + 1;
-            }
-        }
-
-        protected final TcpKeepaliveAckData mPacket;
-        protected final byte[] mSrcDstAddr;
-        protected final byte[] mPortSeqAckFingerprint;
-
-        TcpKeepaliveAck(final TcpKeepaliveAckData packet, final byte[] srcDstAddr) {
-            mPacket = packet;
-            mSrcDstAddr = srcDstAddr;
-            mPortSeqAckFingerprint = generatePortSeqAckFingerprint(mPacket.srcPort,
-                    mPacket.dstPort, mPacket.seq, mPacket.ack);
-        }
-
-        static byte[] generatePortSeqAckFingerprint(int srcPort, int dstPort, int seq, int ack) {
-            final ByteBuffer fp = ByteBuffer.allocate(12);
-            fp.order(ByteOrder.BIG_ENDIAN);
-            fp.putShort((short) srcPort);
-            fp.putShort((short) dstPort);
-            fp.putInt(seq);
-            fp.putInt(ack);
-            return fp.array();
-        }
-
-        public String toString() {
-            try {
-                return String.format("%s -> %s , seq=%d, ack=%d",
-                        ConnectivityUtils.addressAndPortToString(
-                                InetAddress.getByAddress(mPacket.srcAddress), mPacket.srcPort),
-                        ConnectivityUtils.addressAndPortToString(
-                                InetAddress.getByAddress(mPacket.dstAddress), mPacket.dstPort),
-                        Integer.toUnsignedLong(mPacket.seq),
-                        Integer.toUnsignedLong(mPacket.ack));
-            } catch (UnknownHostException e) {
-                return "Unknown host";
-            }
-        }
-
-        // Append a filter for this keepalive ack to {@code gen}.
-        // Jump to drop if it matches the keepalive ack.
-        // Jump to the next filter if packet doesn't match the keepalive ack.
-        abstract void generateFilterLocked(ApfV4Generator gen) throws IllegalInstructionException;
-    }
-
-    private class TcpKeepaliveAckV4 extends TcpKeepaliveAck {
-
-        TcpKeepaliveAckV4(final TcpKeepalivePacketDataParcelable sentKeepalivePacket) {
-            this(new TcpKeepaliveAckData(sentKeepalivePacket));
-        }
-        TcpKeepaliveAckV4(final TcpKeepaliveAckData packet) {
-            super(packet, concatArrays(packet.srcAddress, packet.dstAddress) /* srcDstAddr */);
-        }
-
-        @Override
-        @GuardedBy("LegacyApfFilter.this")
-        void generateFilterLocked(ApfV4Generator gen) throws IllegalInstructionException {
-            final String nextFilterLabel = "keepalive_ack" + getUniqueNumberLocked();
-
-            gen.addLoadImmediate(R0, ETH_HEADER_LEN + IPV4_SRC_ADDR_OFFSET);
-            gen.addJumpIfBytesAtR0NotEqual(mSrcDstAddr, nextFilterLabel);
-
-            // Skip to the next filter if it's not zero-sized :
-            // TCP_HEADER_SIZE + IPV4_HEADER_SIZE - ipv4_total_length == 0
-            // Load the IP header size into R1
-            gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE);
-            // Load the TCP header size into R0 (it's indexed by R1)
-            gen.addLoad8Indexed(R0, ETH_HEADER_LEN + TCP_HEADER_SIZE_OFFSET);
-            // Size offset is in the top nibble, but it must be multiplied by 4, and the two
-            // top bits of the low nibble are guaranteed to be zeroes. Right-shift R0 by 2.
-            gen.addRightShift(2);
-            // R0 += R1 -> R0 contains TCP + IP headers length
-            gen.addAddR1ToR0();
-            // Load IPv4 total length
-            gen.addLoad16(R1, IPV4_TOTAL_LENGTH_OFFSET);
-            gen.addNeg(R0);
-            gen.addAddR1ToR0();
-            gen.addJumpIfR0NotEquals(0, nextFilterLabel);
-            // Add IPv4 header length
-            gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE);
-            gen.addLoadImmediate(R0, ETH_HEADER_LEN);
-            gen.addAddR1ToR0();
-            gen.addJumpIfBytesAtR0NotEqual(mPortSeqAckFingerprint, nextFilterLabel);
-
-            maybeSetupCounter(gen, Counter.DROPPED_IPV4_KEEPALIVE_ACK);
-            gen.addJump(mCountAndDropLabel);
-            gen.defineLabel(nextFilterLabel);
-        }
-    }
-
-    private class TcpKeepaliveAckV6 extends TcpKeepaliveAck {
-        TcpKeepaliveAckV6(final TcpKeepalivePacketDataParcelable sentKeepalivePacket) {
-            this(new TcpKeepaliveAckData(sentKeepalivePacket));
-        }
-        TcpKeepaliveAckV6(final TcpKeepaliveAckData packet) {
-            super(packet, concatArrays(packet.srcAddress, packet.dstAddress) /* srcDstAddr */);
-        }
-
-        @Override
-        void generateFilterLocked(ApfV4Generator gen) throws IllegalInstructionException {
-            throw new UnsupportedOperationException("IPv6 TCP Keepalive is not supported yet");
-        }
-    }
-
-    // Maximum number of RAs to filter for.
-    private static final int MAX_RAS = 10;
-
-    @GuardedBy("this")
-    private ArrayList<Ra> mRas = new ArrayList<>();
-    @GuardedBy("this")
-    private SparseArray<KeepalivePacket> mKeepalivePackets = new SparseArray<>();
-    @GuardedBy("this")
-    private final List<String[]> mMdnsAllowList = new ArrayList<>();
-
-    // There is always some marginal benefit to updating the installed APF program when an RA is
-    // seen because we can extend the program's lifetime slightly, but there is some cost to
-    // updating the program, so don't bother unless the program is going to expire soon. This
-    // constant defines "soon" in seconds.
-    private static final long MAX_PROGRAM_LIFETIME_WORTH_REFRESHING = 30;
-    // We don't want to filter an RA for it's whole lifetime as it'll be expired by the time we ever
-    // see a refresh.  Using half the lifetime might be a good idea except for the fact that
-    // packets may be dropped, so let's use 6.
-    private static final int FRACTION_OF_LIFETIME_TO_FILTER = 6;
-
-    // The base time for this filter program. In seconds since Unix Epoch.
-    // This is the time when the APF program was generated. All filters in the program should use
-    // this base time as their current time for consistency purposes.
-    @GuardedBy("this")
-    private long mProgramBaseTime;
-    // When did we last install a filter program? In seconds since Unix Epoch.
-    @GuardedBy("this")
-    private long mLastTimeInstalledProgram;
-    // How long should the last installed filter program live for? In seconds.
-    @GuardedBy("this")
-    private long mLastInstalledProgramMinLifetime;
-    @GuardedBy("this")
-    private ApfProgramEvent.Builder mLastInstallEvent;
-
-    // For debugging only. The last program installed.
-    @GuardedBy("this")
-    private byte[] mLastInstalledProgram;
-
-    /**
-     * For debugging only. Contains the latest APF buffer snapshot captured from the firmware.
-     *
-     * A typical size for this buffer is 4KB. It is present only if the WiFi HAL supports
-     * IWifiStaIface#readApfPacketFilterData(), and the APF interpreter advertised support for
-     * the opcodes to access the data buffer (LDDW and STDW).
-     */
-    @GuardedBy("this") @Nullable
-    private byte[] mDataSnapshot;
-
-    // How many times the program was updated since we started.
-    @GuardedBy("this")
-    private int mNumProgramUpdates = 0;
-    // The maximum program size that updated since we started.
-    @GuardedBy("this")
-    private int mMaxProgramSize = 0;
-    // The maximum number of distinct RAs
-    @GuardedBy("this")
-    private int mMaxDistinctRas = 0;
-    // How many times the program was updated since we started for allowing multicast traffic.
-    @GuardedBy("this")
-    private int mNumProgramUpdatesAllowingMulticast = 0;
-
-    /**
-     * Generate filter code to process ARP packets. Execution of this code ends in either the
-     * DROP_LABEL or PASS_LABEL and does not fall off the end.
-     * Preconditions:
-     *  - Packet being filtered is ARP
-     */
-    @GuardedBy("this")
-    private void generateArpFilterLocked(ApfV4Generator gen) throws IllegalInstructionException {
-        // Here's a basic summary of what the ARP filter program does:
-        //
-        // if not ARP IPv4
-        //   pass
-        // if not ARP IPv4 reply or request
-        //   pass
-        // if ARP reply source ip is 0.0.0.0
-        //   drop
-        // if unicast ARP reply
-        //   pass
-        // if interface has no IPv4 address
-        //   if target ip is 0.0.0.0
-        //      drop
-        // else
-        //   if target ip is not the interface ip
-        //      drop
-        // pass
-
-        final String checkTargetIPv4 = "checkTargetIPv4";
-
-        // Pass if not ARP IPv4.
-        gen.addLoadImmediate(R0, ARP_HEADER_OFFSET);
-        maybeSetupCounter(gen, Counter.PASSED_ARP_NON_IPV4);
-        gen.addJumpIfBytesAtR0NotEqual(ARP_IPV4_HEADER, mCountAndPassLabel);
-
-        // Pass if unknown ARP opcode.
-        gen.addLoad16(R0, ARP_OPCODE_OFFSET);
-        gen.addJumpIfR0Equals(ARP_OPCODE_REQUEST, checkTargetIPv4); // Skip to unicast check
-        maybeSetupCounter(gen, Counter.PASSED_ARP_UNKNOWN);
-        gen.addJumpIfR0NotEquals(ARP_OPCODE_REPLY, mCountAndPassLabel);
-
-        // Drop if ARP reply source IP is 0.0.0.0
-        gen.addLoad32(R0, ARP_SOURCE_IP_ADDRESS_OFFSET);
-        maybeSetupCounter(gen, Counter.DROPPED_ARP_REPLY_SPA_NO_HOST);
-        gen.addJumpIfR0Equals(IPV4_ANY_HOST_ADDRESS, mCountAndDropLabel);
-
-        // Pass if unicast reply.
-        gen.addLoadImmediate(R0, ETH_DEST_ADDR_OFFSET);
-        maybeSetupCounter(gen, Counter.PASSED_ARP_UNICAST_REPLY);
-        gen.addJumpIfBytesAtR0NotEqual(ETHER_BROADCAST, mCountAndPassLabel);
-
-        // Either a unicast request, a unicast reply, or a broadcast reply.
-        gen.defineLabel(checkTargetIPv4);
-        if (mIPv4Address == null) {
-            // When there is no IPv4 address, drop GARP replies (b/29404209).
-            gen.addLoad32(R0, ARP_TARGET_IP_ADDRESS_OFFSET);
-            maybeSetupCounter(gen, Counter.DROPPED_GARP_REPLY);
-            gen.addJumpIfR0Equals(IPV4_ANY_HOST_ADDRESS, mCountAndDropLabel);
-        } else {
-            // When there is an IPv4 address, drop unicast/broadcast requests
-            // and broadcast replies with a different target IPv4 address.
-            gen.addLoadImmediate(R0, ARP_TARGET_IP_ADDRESS_OFFSET);
-            maybeSetupCounter(gen, Counter.DROPPED_ARP_OTHER_HOST);
-            gen.addJumpIfBytesAtR0NotEqual(mIPv4Address, mCountAndDropLabel);
-        }
-
-        maybeSetupCounter(gen, Counter.PASSED_ARP);
-        gen.addJump(mCountAndPassLabel);
-    }
-
-    /**
-     * Generate filter code to process IPv4 packets. Execution of this code ends in either the
-     * DROP_LABEL or PASS_LABEL and does not fall off the end.
-     * Preconditions:
-     *  - Packet being filtered is IPv4
-     */
-    @GuardedBy("this")
-    private void generateIPv4FilterLocked(ApfV4Generator gen) throws IllegalInstructionException {
-        // Here's a basic summary of what the IPv4 filter program does:
-        //
-        // if filtering multicast (i.e. multicast lock not held):
-        //   if it's DHCP destined to our MAC:
-        //     pass
-        //   if it's L2 broadcast:
-        //     drop
-        //   if it's IPv4 multicast:
-        //     drop
-        //   if it's IPv4 broadcast:
-        //     drop
-        // if keepalive ack
-        //   drop
-        // pass
-
-        if (mMulticastFilter) {
-            final String skipDhcpv4Filter = "skip_dhcp_v4_filter";
-
-            // Pass DHCP addressed to us.
-            // Check it's UDP.
-            gen.addLoad8(R0, IPV4_PROTOCOL_OFFSET);
-            gen.addJumpIfR0NotEquals(IPPROTO_UDP, skipDhcpv4Filter);
-            // Check it's not a fragment. This matches the BPF filter installed by the DHCP client.
-            gen.addLoad16(R0, IPV4_FRAGMENT_OFFSET_OFFSET);
-            gen.addJumpIfR0AnyBitsSet(IPV4_FRAGMENT_OFFSET_MASK, skipDhcpv4Filter);
-            // Check it's addressed to DHCP client port.
-            gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE);
-            gen.addLoad16Indexed(R0, UDP_DESTINATION_PORT_OFFSET);
-            gen.addJumpIfR0NotEquals(DHCP_CLIENT_PORT, skipDhcpv4Filter);
-            // Check it's DHCP to our MAC address.
-            gen.addLoadImmediate(R0, DHCP_CLIENT_MAC_OFFSET);
-            // NOTE: Relies on R1 containing IPv4 header offset.
-            gen.addAddR1ToR0();
-            gen.addJumpIfBytesAtR0NotEqual(mHardwareAddress, skipDhcpv4Filter);
-            maybeSetupCounter(gen, Counter.PASSED_DHCP);
-            gen.addJump(mCountAndPassLabel);
-
-            // Drop all multicasts/broadcasts.
-            gen.defineLabel(skipDhcpv4Filter);
-
-            // If IPv4 destination address is in multicast range, drop.
-            gen.addLoad8(R0, IPV4_DEST_ADDR_OFFSET);
-            gen.addAnd(0xf0);
-            maybeSetupCounter(gen, Counter.DROPPED_IPV4_MULTICAST);
-            gen.addJumpIfR0Equals(0xe0, mCountAndDropLabel);
-
-            // If IPv4 broadcast packet, drop regardless of L2 (b/30231088).
-            maybeSetupCounter(gen, Counter.DROPPED_IPV4_BROADCAST_ADDR);
-            gen.addLoad32(R0, IPV4_DEST_ADDR_OFFSET);
-            gen.addJumpIfR0Equals(IPV4_BROADCAST_ADDRESS, mCountAndDropLabel);
-            if (mIPv4Address != null && mIPv4PrefixLength < 31) {
-                maybeSetupCounter(gen, Counter.DROPPED_IPV4_BROADCAST_NET);
-                int broadcastAddr = ipv4BroadcastAddress(mIPv4Address, mIPv4PrefixLength);
-                gen.addJumpIfR0Equals(broadcastAddr, mCountAndDropLabel);
-            }
-
-            // If any TCP keepalive filter matches, drop
-            generateV4KeepaliveFilters(gen);
-
-            // If any NAT-T keepalive filter matches, drop
-            generateV4NattKeepaliveFilters(gen);
-
-            // Otherwise, this is an IPv4 unicast, pass
-            // If L2 broadcast packet, drop.
-            // TODO: can we invert this condition to fall through to the common pass case below?
-            maybeSetupCounter(gen, Counter.PASSED_IPV4_UNICAST);
-            gen.addLoadImmediate(R0, ETH_DEST_ADDR_OFFSET);
-            gen.addJumpIfBytesAtR0NotEqual(ETHER_BROADCAST, mCountAndPassLabel);
-            maybeSetupCounter(gen, Counter.DROPPED_IPV4_L2_BROADCAST);
-            gen.addJump(mCountAndDropLabel);
-        } else {
-            generateV4KeepaliveFilters(gen);
-            generateV4NattKeepaliveFilters(gen);
-        }
-
-        // Otherwise, pass
-        maybeSetupCounter(gen, Counter.PASSED_IPV4);
-        gen.addJump(mCountAndPassLabel);
-    }
-
-    @GuardedBy("this")
-    private void generateKeepaliveFilters(ApfV4Generator gen, Class<?> filterType, int proto,
-            int offset, String label) throws IllegalInstructionException {
-        final boolean haveKeepaliveResponses = CollectionUtils.any(mKeepalivePackets,
-                ack -> filterType.isInstance(ack));
-
-        // If no keepalive packets of this type
-        if (!haveKeepaliveResponses) return;
-
-        // If not the right proto, skip keepalive filters
-        gen.addLoad8(R0, offset);
-        gen.addJumpIfR0NotEquals(proto, label);
-
-        // Drop Keepalive responses
-        for (int i = 0; i < mKeepalivePackets.size(); ++i) {
-            final KeepalivePacket response = mKeepalivePackets.valueAt(i);
-            if (filterType.isInstance(response)) response.generateFilterLocked(gen);
-        }
-
-        gen.defineLabel(label);
-    }
-
-    @GuardedBy("this")
-    private void generateV4KeepaliveFilters(ApfV4Generator gen) throws IllegalInstructionException {
-        generateKeepaliveFilters(gen, TcpKeepaliveAckV4.class, IPPROTO_TCP, IPV4_PROTOCOL_OFFSET,
-                "skip_v4_keepalive_filter");
-    }
-
-    @GuardedBy("this")
-    private void generateV4NattKeepaliveFilters(ApfV4Generator gen)
-            throws IllegalInstructionException {
-        generateKeepaliveFilters(gen, NattKeepaliveResponse.class,
-                IPPROTO_UDP, IPV4_PROTOCOL_OFFSET, "skip_v4_nattkeepalive_filter");
-    }
-
-    /**
-     * Generate filter code to process IPv6 packets. Execution of this code ends in either the
-     * DROP_LABEL or PASS_LABEL, or falls off the end for ICMPv6 packets.
-     * Preconditions:
-     *  - Packet being filtered is IPv6
-     */
-    @GuardedBy("this")
-    private void generateIPv6FilterLocked(ApfV4Generator gen) throws IllegalInstructionException {
-        // Here's a basic summary of what the IPv6 filter program does:
-        //
-        // if there is a hop-by-hop option present (e.g. MLD query)
-        //   pass
-        // if we're dropping multicast
-        //   if it's not IPCMv6 or it's ICMPv6 but we're in doze mode:
-        //     if it's multicast:
-        //       drop
-        //     pass
-        // if it's ICMPv6 RS to any:
-        //   drop
-        // if it's ICMPv6 NA to anything in ff02::/120
-        //   drop
-        // if keepalive ack
-        //   drop
-
-        gen.addLoad8(R0, IPV6_NEXT_HEADER_OFFSET);
-
-        // MLD packets set the router-alert hop-by-hop option.
-        // TODO: be smarter about not blindly passing every packet with HBH options.
-        maybeSetupCounter(gen, Counter.PASSED_MLD);
-        gen.addJumpIfR0Equals(IPPROTO_HOPOPTS, mCountAndPassLabel);
-
-        // Drop multicast if the multicast filter is enabled.
-        if (mMulticastFilter) {
-            final String skipIPv6MulticastFilterLabel = "skipIPv6MulticastFilter";
-            final String dropAllIPv6MulticastsLabel = "dropAllIPv6Multicast";
-
-            // While in doze mode, drop ICMPv6 multicast pings, let the others pass.
-            // While awake, let all ICMPv6 multicasts through.
-            if (mInDozeMode) {
-                // Not ICMPv6? -> Proceed to multicast filtering
-                gen.addJumpIfR0NotEquals(IPPROTO_ICMPV6, dropAllIPv6MulticastsLabel);
-
-                // ICMPv6 but not ECHO? -> Skip the multicast filter.
-                // (ICMPv6 ECHO requests will go through the multicast filter below).
-                gen.addLoad8(R0, ICMP6_TYPE_OFFSET);
-                gen.addJumpIfR0NotEquals(ICMPV6_ECHO_REQUEST_TYPE, skipIPv6MulticastFilterLabel);
-            } else {
-                gen.addJumpIfR0Equals(IPPROTO_ICMPV6, skipIPv6MulticastFilterLabel);
-            }
-
-            // Drop all other packets sent to ff00::/8 (multicast prefix).
-            gen.defineLabel(dropAllIPv6MulticastsLabel);
-            maybeSetupCounter(gen, Counter.DROPPED_IPV6_NON_ICMP_MULTICAST);
-            gen.addLoad8(R0, IPV6_DEST_ADDR_OFFSET);
-            gen.addJumpIfR0Equals(0xff, mCountAndDropLabel);
-            // If any keepalive filter matches, drop
-            generateV6KeepaliveFilters(gen);
-            // Not multicast. Pass.
-            maybeSetupCounter(gen, Counter.PASSED_IPV6_UNICAST_NON_ICMP);
-            gen.addJump(mCountAndPassLabel);
-            gen.defineLabel(skipIPv6MulticastFilterLabel);
-        } else {
-            generateV6KeepaliveFilters(gen);
-            // If not ICMPv6, pass.
-            maybeSetupCounter(gen, Counter.PASSED_IPV6_NON_ICMP);
-            gen.addJumpIfR0NotEquals(IPPROTO_ICMPV6, mCountAndPassLabel);
-        }
-
-        // If we got this far, the packet is ICMPv6.  Drop some specific types.
-
-        // Add unsolicited multicast neighbor announcements filter
-        String skipUnsolicitedMulticastNALabel = "skipUnsolicitedMulticastNA";
-        gen.addLoad8(R0, ICMP6_TYPE_OFFSET);
-        // Drop all router solicitations (b/32833400)
-        maybeSetupCounter(gen, Counter.DROPPED_IPV6_ROUTER_SOLICITATION);
-        gen.addJumpIfR0Equals(ICMPV6_ROUTER_SOLICITATION, mCountAndDropLabel);
-        // If not neighbor announcements, skip filter.
-        gen.addJumpIfR0NotEquals(ICMPV6_NEIGHBOR_ADVERTISEMENT, skipUnsolicitedMulticastNALabel);
-        // Drop all multicast NA to ff02::/120.
-        // This is a way to cover ff02::1 and ff02::2 with a single JNEBS.
-        // TODO: Drop only if they don't contain the address of on-link neighbours.
-        final byte[] unsolicitedNaDropPrefix = Arrays.copyOf(IPV6_ALL_NODES_ADDRESS, 15);
-        gen.addLoadImmediate(R0, IPV6_DEST_ADDR_OFFSET);
-        gen.addJumpIfBytesAtR0NotEqual(unsolicitedNaDropPrefix, skipUnsolicitedMulticastNALabel);
-
-        maybeSetupCounter(gen, Counter.DROPPED_IPV6_MULTICAST_NA);
-        gen.addJump(mCountAndDropLabel);
-        gen.defineLabel(skipUnsolicitedMulticastNALabel);
-    }
-
-    /** Encodes qname in TLV pattern. */
-    @VisibleForTesting
-    public static byte[] encodeQname(String[] labels) {
-        final ByteArrayOutputStream out = new ByteArrayOutputStream();
-        for (String label : labels) {
-            byte[] labelBytes = label.getBytes(StandardCharsets.UTF_8);
-            out.write(labelBytes.length);
-            out.write(labelBytes, 0, labelBytes.length);
-        }
-        out.write(0);
-        return out.toByteArray();
-    }
-
-    /**
-     * Generate filter code to process mDNS packets. Execution of this code ends in * DROP_LABEL
-     * or PASS_LABEL if the packet is mDNS packets. Otherwise, skip this check.
-     */
-    @GuardedBy("this")
-    private void generateMdnsFilterLocked(ApfV4Generator gen)
-            throws IllegalInstructionException {
-        final String skipMdnsv4Filter = "skip_mdns_v4_filter";
-        final String skipMdnsFilter = "skip_mdns_filter";
-        final String checkMdnsUdpPort = "check_mdns_udp_port";
-        final String mDnsAcceptPacket = "mdns_accept_packet";
-        final String mDnsDropPacket = "mdns_drop_packet";
-
-        // Only turn on the filter if multicast filter is on and the qname allowlist is non-empty.
-        if (!mMulticastFilter || mMdnsAllowList.isEmpty()) {
-            return;
-        }
-
-        // Here's a basic summary of what the mDNS filter program does:
-        //
-        // if it is a multicast mDNS packet
-        //    if QDCOUNT != 1
-        //       pass
-        //    else if the QNAME is in the allowlist
-        //       pass
-        //    else:
-        //       drop
-        //
-        // A packet is considered as a multicast mDNS packet if it matches all the following
-        // conditions
-        //   1. its destination MAC address matches 01:00:5E:00:00:FB or 33:33:00:00:00:FB, for
-        //   v4 and v6 respectively.
-        //   2. it is an IPv4/IPv6 packet
-        //   3. it is a UDP packet with port 5353
-
-        // Check it's L2 mDNS multicast address.
-        gen.addLoadImmediate(R0, ETH_DEST_ADDR_OFFSET);
-        gen.addJumpIfBytesAtR0NotEqual(ETH_MULTICAST_MDNS_V4_MAC_ADDRESS,
-                skipMdnsv4Filter);
-
-        // Checks it's IPv4.
-        gen.addLoad16(R0, ETH_ETHERTYPE_OFFSET);
-        gen.addJumpIfR0NotEquals(ETH_P_IP, skipMdnsFilter);
-
-        // Checks it's UDP.
-        gen.addLoad8(R0, IPV4_PROTOCOL_OFFSET);
-        gen.addJumpIfR0NotEquals(IPPROTO_UDP, skipMdnsFilter);
-        // Set R1 to IPv4 header.
-        gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE);
-        gen.addJump(checkMdnsUdpPort);
-
-        gen.defineLabel(skipMdnsv4Filter);
-
-        // Checks it's L2 mDNS multicast address.
-        // Relies on R0 containing the ethernet destination mac address offset.
-        gen.addJumpIfBytesAtR0NotEqual(ETH_MULTICAST_MDNS_V6_MAC_ADDRESS, skipMdnsFilter);
-
-        // Checks it's IPv6.
-        gen.addLoad16(R0, ETH_ETHERTYPE_OFFSET);
-        gen.addJumpIfR0NotEquals(ETH_P_IPV6, skipMdnsFilter);
-
-        // Checks it's UDP.
-        gen.addLoad8(R0, IPV6_NEXT_HEADER_OFFSET);
-        gen.addJumpIfR0NotEquals(IPPROTO_UDP, skipMdnsFilter);
-
-        // Set R1 to IPv6 header.
-        gen.addLoadImmediate(R1, IPV6_HEADER_LEN);
-
-        // Checks it's mDNS UDP port
-        gen.defineLabel(checkMdnsUdpPort);
-        gen.addLoad16Indexed(R0, UDP_DESTINATION_PORT_OFFSET);
-        gen.addJumpIfR0NotEquals(MDNS_PORT, skipMdnsFilter);
-
-        gen.addLoad16Indexed(R0, MDNS_QDCOUNT_OFFSET);
-        // If QDCOUNT != 1, pass the packet
-        gen.addJumpIfR0NotEquals(1, mDnsAcceptPacket);
-
-        // If QDCOUNT == 1, matches the QNAME with allowlist.
-        // Load offset for the first QNAME.
-        gen.addLoadImmediate(R0, MDNS_QNAME_OFFSET);
-        gen.addAddR1ToR0();
-
-        // Check first QNAME against allowlist
-        for (int i = 0; i < mMdnsAllowList.size(); ++i) {
-            final String mDnsNextAllowedQnameCheck = "mdns_next_allowed_qname_check" + i;
-            final byte[] encodedQname = encodeQname(mMdnsAllowList.get(i));
-            gen.addJumpIfBytesAtR0NotEqual(encodedQname, mDnsNextAllowedQnameCheck);
-            // QNAME matched
-            gen.addJump(mDnsAcceptPacket);
-            // QNAME not matched
-            gen.defineLabel(mDnsNextAllowedQnameCheck);
-        }
-        // If QNAME doesn't match any entries in allowlist, drop the packet.
-        gen.defineLabel(mDnsDropPacket);
-        maybeSetupCounter(gen, Counter.DROPPED_MDNS);
-        gen.addJump(mCountAndDropLabel);
-
-        gen.defineLabel(mDnsAcceptPacket);
-        maybeSetupCounter(gen, Counter.PASSED_MDNS);
-        gen.addJump(mCountAndPassLabel);
-
-
-        gen.defineLabel(skipMdnsFilter);
-    }
-
-    @GuardedBy("this")
-    private void generateV6KeepaliveFilters(ApfV4Generator gen) throws IllegalInstructionException {
-        generateKeepaliveFilters(gen, TcpKeepaliveAckV6.class, IPPROTO_TCP, IPV6_NEXT_HEADER_OFFSET,
-                "skip_v6_keepalive_filter");
-    }
-
-    /**
-     * Begin generating an APF program to:
-     * <ul>
-     * <li>Drop/Pass 802.3 frames (based on policy)
-     * <li>Drop packets with EtherType within the Black List
-     * <li>Drop ARP requests not for us, if mIPv4Address is set,
-     * <li>Drop IPv4 broadcast packets, except DHCP destined to our MAC,
-     * <li>Drop IPv4 multicast packets, if mMulticastFilter,
-     * <li>Pass all other IPv4 packets,
-     * <li>Drop all broadcast non-IP non-ARP packets.
-     * <li>Pass all non-ICMPv6 IPv6 packets,
-     * <li>Pass all non-IPv4 and non-IPv6 packets,
-     * <li>Drop IPv6 ICMPv6 NAs to anything in ff02::/120.
-     * <li>Drop IPv6 ICMPv6 RSs.
-     * <li>Filter IPv4 packets (see generateIPv4FilterLocked())
-     * <li>Filter IPv6 packets (see generateIPv6FilterLocked())
-     * <li>Let execution continue off the end of the program for IPv6 ICMPv6 packets. This allows
-     *     insertion of RA filters here, or if there aren't any, just passes the packets.
-     * </ul>
-     */
-    @GuardedBy("this")
-    protected ApfV4Generator emitPrologueLocked() throws IllegalInstructionException {
-        // This is guaranteed to succeed because of the check in maybeCreate.
-        ApfV4Generator gen = new ApfV4Generator(mApfVersionSupported, mMaximumApfProgramSize,
-                mMaximumApfProgramSize);
-
-        if (hasDataAccess(mApfVersionSupported)) {
-            // Increment TOTAL_PACKETS
-            maybeSetupCounter(gen, Counter.TOTAL_PACKETS);
-            gen.addLoadData(R0, 0);  // load counter
-            gen.addAdd(1);
-            gen.addStoreData(R0, 0);  // write-back counter
-
-            maybeSetupCounter(gen, Counter.FILTER_AGE_SECONDS);
-            gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_SECONDS);
-            gen.addStoreData(R0, 0);  // store 'counter'
-
-            // requires a new enough APFv5+ interpreter, otherwise will be 0
-            maybeSetupCounter(gen, Counter.FILTER_AGE_16384THS);
-            gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_16384THS);
-            gen.addStoreData(R0, 0);  // store 'counter'
-
-            // requires a new enough APFv5+ interpreter, otherwise will be 0
-            maybeSetupCounter(gen, Counter.APF_VERSION);
-            gen.addLoadFromMemory(R0, MemorySlot.APF_VERSION);
-            gen.addStoreData(R0, 0);  // store 'counter'
-
-            // store this program's sequential id, for later comparison
-            maybeSetupCounter(gen, Counter.APF_PROGRAM_ID);
-            gen.addLoadImmediate(R0, mNumProgramUpdates);
-            gen.addStoreData(R0, 0);  // store 'counter'
-        }
-
-        // Here's a basic summary of what the initial program does:
-        //
-        // if it's a 802.3 Frame (ethtype < 0x0600):
-        //    drop or pass based on configurations
-        // if it has a ether-type that belongs to the black list
-        //    drop
-        // if it's ARP:
-        //   insert ARP filter to drop or pass these appropriately
-        // if it's IPv4:
-        //   insert IPv4 filter to drop or pass these appropriately
-        // if it's not IPv6:
-        //   if it's broadcast:
-        //     drop
-        //   pass
-        // insert IPv6 filter to drop, pass, or fall off the end for ICMPv6 packets
-
-        gen.addLoad16(R0, ETH_ETHERTYPE_OFFSET);
-
-        if (mDrop802_3Frames) {
-            // drop 802.3 frames (ethtype < 0x0600)
-            maybeSetupCounter(gen, Counter.DROPPED_802_3_FRAME);
-            gen.addJumpIfR0LessThan(ETH_TYPE_MIN, mCountAndDropLabel);
-        }
-
-        // Handle ether-type black list
-        maybeSetupCounter(gen, Counter.DROPPED_ETHERTYPE_NOT_ALLOWED);
-        for (int p : mEthTypeBlackList) {
-            gen.addJumpIfR0Equals(p, mCountAndDropLabel);
-        }
-
-        // Add ARP filters:
-        String skipArpFiltersLabel = "skipArpFilters";
-        gen.addJumpIfR0NotEquals(ETH_P_ARP, skipArpFiltersLabel);
-        generateArpFilterLocked(gen);
-        gen.defineLabel(skipArpFiltersLabel);
-
-        // Add mDNS filter:
-        generateMdnsFilterLocked(gen);
-        gen.addLoad16(R0, ETH_ETHERTYPE_OFFSET);
-
-        // Add IPv4 filters:
-        String skipIPv4FiltersLabel = "skipIPv4Filters";
-        gen.addJumpIfR0NotEquals(ETH_P_IP, skipIPv4FiltersLabel);
-        generateIPv4FilterLocked(gen);
-        gen.defineLabel(skipIPv4FiltersLabel);
-
-        // Check for IPv6:
-        // NOTE: Relies on R0 containing ethertype. This is safe because if we got here, we did
-        // not execute the IPv4 filter, since this filter do not fall through, but either drop or
-        // pass.
-        String ipv6FilterLabel = "IPv6Filters";
-        gen.addJumpIfR0Equals(ETH_P_IPV6, ipv6FilterLabel);
-
-        // Drop non-IP non-ARP broadcasts, pass the rest
-        gen.addLoadImmediate(R0, ETH_DEST_ADDR_OFFSET);
-        maybeSetupCounter(gen, Counter.PASSED_NON_IP_UNICAST);
-        gen.addJumpIfBytesAtR0NotEqual(ETHER_BROADCAST, mCountAndPassLabel);
-        maybeSetupCounter(gen, Counter.DROPPED_ETH_BROADCAST);
-        gen.addJump(mCountAndDropLabel);
-
-        // Add IPv6 filters:
-        gen.defineLabel(ipv6FilterLabel);
-        generateIPv6FilterLocked(gen);
-        return gen;
-    }
-
-    /**
-     * Append packet counting epilogue to the APF program.
-     *
-     * Currently, the epilogue consists of two trampolines which count passed and dropped packets
-     * before jumping to the actual PASS and DROP labels.
-     */
-    @GuardedBy("this")
-    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(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.
-        maybeSetupCounter(gen, Counter.PASSED_IPV6_ICMP);
-
-        // Append the count & pass trampoline, which increments the counter at the data address
-        // pointed to by R1, then jumps to the pass label. This saves a few bytes over inserting
-        // the entire sequence inline for every counter.
-        gen.defineLabel(mCountAndPassLabel);
-        gen.addLoadData(R0, 0);   // R0 = *(R1 + 0)
-        gen.addAdd(1);                     // R0++
-        gen.addStoreData(R0, 0);  // *(R1 + 0) = R0
-        gen.addJump(gen.PASS_LABEL);
-
-        // Same as above for the count & drop trampoline.
-        gen.defineLabel(mCountAndDropLabel);
-        gen.addLoadData(R0, 0);   // R0 = *(R1 + 0)
-        gen.addAdd(1);                     // R0++
-        gen.addStoreData(R0, 0);  // *(R1 + 0) = R0
-        gen.addJump(gen.DROP_LABEL);
-    }
-
-    /**
-     * Generate and install a new filter program.
-     */
-    @GuardedBy("this")
-    // errorprone false positive on ra#shouldFilter and ra#generateFilterLocked
-    @SuppressWarnings("GuardedBy")
-    @VisibleForTesting
-    public void installNewProgramLocked() {
-        purgeExpiredRasLocked();
-        ArrayList<Ra> rasToFilter = new ArrayList<>();
-        final byte[] program;
-        long programMinLifetime = Long.MAX_VALUE;
-        long maximumApfProgramSize = mMaximumApfProgramSize;
-        if (hasDataAccess(mApfVersionSupported)) {
-            // Reserve space for the counters.
-            maximumApfProgramSize -= Counter.totalSize();
-        }
-
-        mProgramBaseTime = currentTimeSeconds();
-        try {
-            // Step 1: Determine how many RA filters we can fit in the program.
-            ApfV4Generator gen = emitPrologueLocked();
-
-            // The epilogue normally goes after the RA filters, but add it early to include its
-            // length when estimating the total.
-            emitEpilogue(gen);
-
-            // Can't fit the program even without any RA filters?
-            if (gen.programLengthOverEstimate() > maximumApfProgramSize) {
-                Log.e(TAG, "Program exceeds maximum size " + maximumApfProgramSize);
-                sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_OVER_SIZE_FAILURE);
-                return;
-            }
-
-            for (Ra ra : mRas) {
-                if (!ra.shouldFilter()) continue;
-                ra.generateFilterLocked(gen);
-                // Stop if we get too big.
-                if (gen.programLengthOverEstimate() > maximumApfProgramSize) {
-                    if (VDBG) Log.d(TAG, "Past maximum program size, skipping RAs");
-                    sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_OVER_SIZE_FAILURE);
-                    break;
-                }
-
-                rasToFilter.add(ra);
-            }
-
-            // Step 2: Actually generate the program
-            gen = emitPrologueLocked();
-            for (Ra ra : rasToFilter) {
-                programMinLifetime = Math.min(programMinLifetime, ra.generateFilterLocked(gen));
-            }
-            emitEpilogue(gen);
-            program = gen.generate();
-        } catch (IllegalInstructionException|IllegalStateException e) {
-            Log.e(TAG, "Failed to generate APF program.", e);
-            sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_GENERATE_FILTER_EXCEPTION);
-            return;
-        }
-        if (mIsRunning && !mIpClientCallback.installPacketFilter(program)) {
-            sendNetworkQuirkMetrics(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
-        }
-        mLastTimeInstalledProgram = mProgramBaseTime;
-        mLastInstalledProgramMinLifetime = programMinLifetime;
-        mLastInstalledProgram = program;
-        mNumProgramUpdates++;
-        mMaxProgramSize = Math.max(mMaxProgramSize, program.length);
-
-        if (VDBG) {
-            hexDump("Installing filter: ", program, program.length);
-        }
-        logApfProgramEventLocked(mProgramBaseTime);
-        mLastInstallEvent = new ApfProgramEvent.Builder()
-                .setLifetime(programMinLifetime)
-                .setFilteredRas(rasToFilter.size())
-                .setCurrentRas(mRas.size())
-                .setProgramLength(program.length)
-                .setFlags(mIPv4Address != null, mMulticastFilter);
-    }
-
-    @GuardedBy("this")
-    private void logApfProgramEventLocked(long now) {
-        if (mLastInstallEvent == null) {
-            return;
-        }
-        ApfProgramEvent.Builder ev = mLastInstallEvent;
-        mLastInstallEvent = null;
-        final long actualLifetime = now - mLastTimeInstalledProgram;
-        ev.setActualLifetime(actualLifetime);
-        if (actualLifetime < APF_PROGRAM_EVENT_LIFETIME_THRESHOLD) {
-            return;
-        }
-        mMetricsLog.log(ev.build());
-    }
-
-    /**
-     * Returns {@code true} if a new program should be installed because the current one dies soon.
-     */
-    @GuardedBy("this")
-    private boolean shouldInstallnewProgram() {
-        long expiry = mLastTimeInstalledProgram + mLastInstalledProgramMinLifetime;
-        return expiry < currentTimeSeconds() + MAX_PROGRAM_LIFETIME_WORTH_REFRESHING;
-    }
-
-    private void hexDump(String msg, byte[] packet, int length) {
-        log(msg + HexDump.toHexString(packet, 0, length, false /* lowercase */));
-    }
-
-    @GuardedBy("this")
-    private void purgeExpiredRasLocked() {
-        for (int i = 0; i < mRas.size();) {
-            if (mRas.get(i).isExpired()) {
-                log("Expiring " + mRas.get(i));
-                mRas.remove(i);
-            } else {
-                i++;
-            }
-        }
-    }
-
-    // Get the minimum value excludes zero. This is used for calculating the lowest lifetime values
-    // in RA packets. Zero lifetimes are excluded because we want to detect whether there is any
-    // unusually small lifetimes but zero lifetime is actually valid (cease to be a default router
-    // or the option is no longer be used). Number of zero lifetime RAs is collected in a different
-    // Metrics.
-    private long getMinForPositiveValue(long oldMinValue, long value) {
-        if (value < 1) return oldMinValue;
-        return Math.min(oldMinValue, value);
-    }
-
-    private int getMinForPositiveValue(int oldMinValue, int value) {
-        return (int) getMinForPositiveValue((long) oldMinValue, (long) value);
-    }
-
-    /**
-     * Process an RA packet, updating the list of known RAs and installing a new APF program
-     * if the current APF program should be updated.
-     * @return a ProcessRaResult enum describing what action was performed.
-     */
-    @VisibleForTesting
-    public synchronized ProcessRaResult processRa(byte[] packet, int length) {
-        if (VDBG) hexDump("Read packet = ", packet, length);
-
-        // Have we seen this RA before?
-        for (int i = 0; i < mRas.size(); i++) {
-            Ra ra = mRas.get(i);
-            if (ra.matches(packet, length)) {
-                if (VDBG) log("matched RA " + ra);
-                // Update lifetimes.
-                ra.mLastSeen = currentTimeSeconds();
-                ra.seenCount++;
-
-                // Keep mRas in LRU order so as to prioritize generating filters for recently seen
-                // RAs. LRU prioritizes this because RA filters are generated in order from mRas
-                // until the filter program exceeds the maximum filter program size allowed by the
-                // chipset, so RAs appearing earlier in mRas are more likely to make it into the
-                // filter program.
-                // TODO: consider sorting the RAs in order of increasing expiry time as well.
-                // Swap to front of array.
-                mRas.add(0, mRas.remove(i));
-
-                // If the current program doesn't expire for a while, don't update.
-                if (shouldInstallnewProgram()) {
-                    installNewProgramLocked();
-                    return ProcessRaResult.UPDATE_EXPIRY;
-                }
-                return ProcessRaResult.MATCH;
-            }
-        }
-        purgeExpiredRasLocked();
-
-        mMaxDistinctRas = Math.max(mMaxDistinctRas, mRas.size() + 1);
-
-        // TODO: figure out how to proceed when we've received more than MAX_RAS RAs.
-        if (mRas.size() >= MAX_RAS) {
-            return ProcessRaResult.DROPPED;
-        }
-        final Ra ra;
-        try {
-            ra = new Ra(packet, length);
-        } catch (Exception e) {
-            Log.e(TAG, "Error parsing RA", e);
-            mNumParseErrorRas++;
-            return ProcessRaResult.PARSE_ERROR;
-        }
-
-        // Update info for Metrics
-        mLowestRouterLifetimeSeconds = getMinForPositiveValue(
-                mLowestRouterLifetimeSeconds, ra.routerLifetime());
-        mLowestPioValidLifetimeSeconds = getMinForPositiveValue(
-                mLowestPioValidLifetimeSeconds, ra.minPioValidLifetime());
-        mLowestRioRouteLifetimeSeconds = getMinForPositiveValue(
-                mLowestRioRouteLifetimeSeconds, ra.minRioRouteLifetime());
-        mLowestRdnssLifetimeSeconds = getMinForPositiveValue(
-                mLowestRdnssLifetimeSeconds, ra.minRdnssLifetime());
-
-        // Ignore 0 lifetime RAs.
-        if (ra.isExpired()) {
-            mNumZeroLifetimeRas++;
-            return ProcessRaResult.ZERO_LIFETIME;
-        }
-        log("Adding " + ra);
-        mRas.add(ra);
-        installNewProgramLocked();
-        return ProcessRaResult.UPDATE_NEW_RA;
-    }
-
-    /**
-     * Create an {@link LegacyApfFilter} if {@code apfCapabilities} indicates support for packet
-     * filtering using APF programs.
-     */
-    public static LegacyApfFilter maybeCreate(Context context, ApfFilter.ApfConfiguration config,
-            InterfaceParams ifParams, IpClientCallbacksWrapper ipClientCallback,
-            NetworkQuirkMetrics networkQuirkMetrics) {
-        if (context == null || config == null || ifParams == null) return null;
-        if (!ApfV4Generator.supportsVersion(config.apfVersionSupported)) {
-            return null;
-        }
-        if (config.apfRamSize < 512) {
-            Log.e(TAG, "Unacceptably small APF limit: " + config.apfRamSize);
-            return null;
-        }
-
-        return new LegacyApfFilter(context, config, ifParams, ipClientCallback,
-                new IpConnectivityLog(), networkQuirkMetrics);
-    }
-
-    private synchronized void collectAndSendMetrics() {
-        if (mIpClientRaInfoMetrics == null || mApfSessionInfoMetrics == null) return;
-        final long sessionDurationMs = mClock.elapsedRealtime() - mSessionStartMs;
-        if (sessionDurationMs < mMinMetricsSessionDurationMs) return;
-
-        // Collect and send IpClientRaInfoMetrics.
-        mIpClientRaInfoMetrics.setMaxNumberOfDistinctRas(mMaxDistinctRas);
-        mIpClientRaInfoMetrics.setNumberOfZeroLifetimeRas(mNumZeroLifetimeRas);
-        mIpClientRaInfoMetrics.setNumberOfParsingErrorRas(mNumParseErrorRas);
-        mIpClientRaInfoMetrics.setLowestRouterLifetimeSeconds(mLowestRouterLifetimeSeconds);
-        mIpClientRaInfoMetrics.setLowestPioValidLifetimeSeconds(mLowestPioValidLifetimeSeconds);
-        mIpClientRaInfoMetrics.setLowestRioRouteLifetimeSeconds(mLowestRioRouteLifetimeSeconds);
-        mIpClientRaInfoMetrics.setLowestRdnssLifetimeSeconds(mLowestRdnssLifetimeSeconds);
-        mIpClientRaInfoMetrics.statsWrite();
-
-        // Collect and send ApfSessionInfoMetrics.
-        mApfSessionInfoMetrics.setVersion(mApfVersionSupported);
-        mApfSessionInfoMetrics.setMemorySize(mMaximumApfProgramSize);
-        mApfSessionInfoMetrics.setApfSessionDurationSeconds(
-                (int) (sessionDurationMs / DateUtils.SECOND_IN_MILLIS));
-        mApfSessionInfoMetrics.setNumOfTimesApfProgramUpdated(mNumProgramUpdates);
-        mApfSessionInfoMetrics.setMaxProgramSize(mMaxProgramSize);
-        for (Map.Entry<Counter, Long> entry : mApfCounterTracker.getCounters().entrySet()) {
-            if (entry.getValue() > 0) {
-                mApfSessionInfoMetrics.addApfCounter(entry.getKey(), entry.getValue());
-            }
-        }
-        mApfSessionInfoMetrics.statsWrite();
-    }
-
-    public synchronized void shutdown() {
-        collectAndSendMetrics();
-        if (mReceiveThread != null) {
-            log("shutting down");
-            mReceiveThread.halt();  // Also closes socket.
-            mReceiveThread = null;
-        }
-        mRas.clear();
-        mContext.unregisterReceiver(mDeviceIdleReceiver);
-    }
-
-    public synchronized void setMulticastFilter(boolean isEnabled) {
-        if (mMulticastFilter == isEnabled) return;
-        mMulticastFilter = isEnabled;
-        if (!isEnabled) {
-            mNumProgramUpdatesAllowingMulticast++;
-        }
-        installNewProgramLocked();
-    }
-
-    /** Adds qname to the mDNS allowlist */
-    public synchronized void addToMdnsAllowList(String[] labels) {
-        mMdnsAllowList.add(labels);
-        if (mMulticastFilter) {
-            installNewProgramLocked();
-        }
-    }
-
-    /** Removes qname from the mDNS allowlist */
-    public synchronized void removeFromAllowList(String[] labels) {
-        mMdnsAllowList.removeIf(e -> Arrays.equals(labels, e));
-        if (mMulticastFilter) {
-            installNewProgramLocked();
-        }
-    }
-
-    @VisibleForTesting
-    public synchronized void setDozeMode(boolean isEnabled) {
-        if (mInDozeMode == isEnabled) return;
-        mInDozeMode = isEnabled;
-        installNewProgramLocked();
-    }
-
-    /** Find the single IPv4 LinkAddress if there is one, otherwise return null. */
-    private static LinkAddress findIPv4LinkAddress(LinkProperties lp) {
-        LinkAddress ipv4Address = null;
-        for (LinkAddress address : lp.getLinkAddresses()) {
-            if (!(address.getAddress() instanceof Inet4Address)) {
-                continue;
-            }
-            if (ipv4Address != null && !ipv4Address.isSameAddressAs(address)) {
-                // More than one IPv4 address, abort.
-                return null;
-            }
-            ipv4Address = address;
-        }
-        return ipv4Address;
-    }
-
-    public synchronized void setLinkProperties(LinkProperties lp) {
-        // NOTE: Do not keep a copy of LinkProperties as it would further duplicate state.
-        final LinkAddress ipv4Address = findIPv4LinkAddress(lp);
-        final byte[] addr = (ipv4Address != null) ? ipv4Address.getAddress().getAddress() : null;
-        final int prefix = (ipv4Address != null) ? ipv4Address.getPrefixLength() : 0;
-        if ((prefix == mIPv4PrefixLength) && Arrays.equals(addr, mIPv4Address)) {
-            return;
-        }
-        mIPv4Address = addr;
-        mIPv4PrefixLength = prefix;
-        installNewProgramLocked();
-    }
-
-    /**
-     * Add TCP keepalive ack packet filter.
-     * This will add a filter to drop acks to the keepalive packet passed as an argument.
-     *
-     * @param slot The index used to access the filter.
-     * @param sentKeepalivePacket The attributes of the sent keepalive packet.
-     */
-    public synchronized void addTcpKeepalivePacketFilter(final int slot,
-            final TcpKeepalivePacketDataParcelable sentKeepalivePacket) {
-        log("Adding keepalive ack(" + slot + ")");
-        if (null != mKeepalivePackets.get(slot)) {
-            throw new IllegalArgumentException("Keepalive slot " + slot + " is occupied");
-        }
-        final int ipVersion = sentKeepalivePacket.srcAddress.length == 4 ? 4 : 6;
-        mKeepalivePackets.put(slot, (ipVersion == 4)
-                ? new TcpKeepaliveAckV4(sentKeepalivePacket)
-                : new TcpKeepaliveAckV6(sentKeepalivePacket));
-        installNewProgramLocked();
-    }
-
-    /**
-     * Add NAT-T keepalive packet filter.
-     * This will add a filter to drop NAT-T keepalive packet which is passed as an argument.
-     *
-     * @param slot The index used to access the filter.
-     * @param sentKeepalivePacket The attributes of the sent keepalive packet.
-     */
-    public synchronized void addNattKeepalivePacketFilter(final int slot,
-            final NattKeepalivePacketDataParcelable sentKeepalivePacket) {
-        log("Adding NAT-T keepalive packet(" + slot + ")");
-        if (null != mKeepalivePackets.get(slot)) {
-            throw new IllegalArgumentException("NAT-T Keepalive slot " + slot + " is occupied");
-        }
-
-        // TODO : update ApfFilter to support dropping v6 keepalives
-        if (sentKeepalivePacket.srcAddress.length != 4) {
-            return;
-        }
-
-        mKeepalivePackets.put(slot, new NattKeepaliveResponse(sentKeepalivePacket));
-        installNewProgramLocked();
-    }
-
-    /**
-     * Remove keepalive packet filter.
-     *
-     * @param slot The index used to access the filter.
-     */
-    public synchronized void removeKeepalivePacketFilter(int slot) {
-        log("Removing keepalive packet(" + slot + ")");
-        mKeepalivePackets.remove(slot);
-        installNewProgramLocked();
-    }
-
-    public synchronized void dump(IndentingPrintWriter pw) {
-        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"));
-        pw.println("Minimum RDNSS lifetime: " + mMinRdnssLifetimeSec);
-        try {
-            pw.println("IPv4 address: " + InetAddress.getByAddress(mIPv4Address).getHostAddress());
-        } catch (UnknownHostException|NullPointerException e) {}
-
-        if (mLastTimeInstalledProgram == 0) {
-            pw.println("No program installed.");
-            return;
-        }
-        pw.println("Program updates: " + mNumProgramUpdates);
-        pw.println(String.format(
-                "Last program length %d, installed %ds ago, lifetime %ds",
-                mLastInstalledProgram.length, currentTimeSeconds() - mLastTimeInstalledProgram,
-                mLastInstalledProgramMinLifetime));
-
-        pw.print("Denylisted Ethertypes:");
-        for (int p : mEthTypeBlackList) {
-            pw.print(String.format(" %04x", p));
-        }
-        pw.println();
-        pw.println("RA filters:");
-        pw.increaseIndent();
-        for (Ra ra: mRas) {
-            pw.println(ra);
-            pw.increaseIndent();
-            pw.println(String.format(
-                    "Seen: %d, last %ds ago", ra.seenCount, currentTimeSeconds() - ra.mLastSeen));
-            if (DBG) {
-                pw.println("Last match:");
-                pw.increaseIndent();
-                pw.println(ra.getLastMatchingPacket());
-                pw.decreaseIndent();
-            }
-            pw.decreaseIndent();
-        }
-        pw.decreaseIndent();
-
-        pw.println("TCP Keepalive filters:");
-        pw.increaseIndent();
-        for (int i = 0; i < mKeepalivePackets.size(); ++i) {
-            final KeepalivePacket keepalivePacket = mKeepalivePackets.valueAt(i);
-            if (keepalivePacket instanceof TcpKeepaliveAck) {
-                pw.print("Slot ");
-                pw.print(mKeepalivePackets.keyAt(i));
-                pw.print(": ");
-                pw.println(keepalivePacket);
-            }
-        }
-        pw.decreaseIndent();
-
-        pw.println("NAT-T Keepalive filters:");
-        pw.increaseIndent();
-        for (int i = 0; i < mKeepalivePackets.size(); ++i) {
-            final KeepalivePacket keepalivePacket = mKeepalivePackets.valueAt(i);
-            if (keepalivePacket instanceof NattKeepaliveResponse) {
-                pw.print("Slot ");
-                pw.print(mKeepalivePackets.keyAt(i));
-                pw.print(": ");
-                pw.println(keepalivePacket);
-            }
-        }
-        pw.decreaseIndent();
-
-        if (DBG) {
-            pw.println("Last program:");
-            pw.increaseIndent();
-            pw.println(HexDump.toHexString(mLastInstalledProgram, false /* lowercase */));
-            pw.decreaseIndent();
-        }
-
-        pw.println("APF packet counters: ");
-        pw.increaseIndent();
-        if (!hasDataAccess(mApfVersionSupported)) {
-            pw.println("APF counters not supported");
-        } else if (mDataSnapshot == null) {
-            pw.println("No last snapshot.");
-        } else {
-            try {
-                Counter[] counters = Counter.class.getEnumConstants();
-                for (Counter c : Arrays.asList(counters).subList(1, counters.length)) {
-                    long value = ApfCounterTracker.getCounterValue(mDataSnapshot, c);
-                    // Only print non-zero counters
-                    if (value != 0) {
-                        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.");
-                    }
-                }
-            } catch (ArrayIndexOutOfBoundsException e) {
-                pw.println("Uh-oh: " + e);
-            }
-            if (VDBG) {
-                pw.println("Raw data dump: ");
-                pw.println(HexDump.dumpHexString(mDataSnapshot));
-            }
-        }
-        pw.decreaseIndent();
-    }
-
-    // TODO: move to android.net.NetworkUtils
-    @VisibleForTesting
-    public static int ipv4BroadcastAddress(byte[] addrBytes, int prefixLength) {
-        return bytesToBEInt(addrBytes) | (int) (Integer.toUnsignedLong(-1) >>> prefixLength);
-    }
-
-    private static int uint8(byte b) {
-        return b & 0xff;
-    }
-
-    private static int getUint16(ByteBuffer buffer, int position) {
-        return buffer.getShort(position) & 0xffff;
-    }
-
-    private static long getUint32(ByteBuffer buffer, int position) {
-        return Integer.toUnsignedLong(buffer.getInt(position));
-    }
-
-    private static int getUint8(ByteBuffer buffer, int position) {
-        return uint8(buffer.get(position));
-    }
-
-    private static int bytesToBEInt(byte[] bytes) {
-        return (uint8(bytes[0]) << 24)
-                + (uint8(bytes[1]) << 16)
-                + (uint8(bytes[2]) << 8)
-                + (uint8(bytes[3]));
-    }
-
-    private static byte[] concatArrays(final byte[]... arr) {
-        int size = 0;
-        for (byte[] a : arr) {
-            size += a.length;
-        }
-        final byte[] result = new byte[size];
-        int offset = 0;
-        for (byte[] a : arr) {
-            System.arraycopy(a, 0, result, offset, a.length);
-            offset += a.length;
-        }
-        return result;
-    }
-
-    private void sendNetworkQuirkMetrics(final NetworkQuirkEvent event) {
-        if (mNetworkQuirkMetrics == null) return;
-        mNetworkQuirkMetrics.setEvent(event);
-        mNetworkQuirkMetrics.statsWrite();
-    }
-
-    /**
-     * Indicates whether the ApfFilter is currently running / paused for test and debugging
-     * purposes.
-     */
-    public boolean isRunning() {
-        return mIsRunning;
-    }
-
-    /** Pause ApfFilter updates for testing purposes. */
-    public void pause() {
-        mIsRunning = false;
-    }
-
-    /** Resume ApfFilter updates for testing purposes. */
-    public void resume() {
-        mIsRunning = true;
-    }
-
-    /** Return hex string of current APF snapshot for testing purposes. */
-    public synchronized @Nullable String getDataSnapshotHexString() {
-        if (mDataSnapshot == null) {
-            return null;
-        }
-        return HexDump.toHexString(mDataSnapshot, 0, mDataSnapshot.length, false /* lowercase */);
-    }
-}
diff --git a/src/android/net/apf/MdnsOffloadRule.java b/src/android/net/apf/MdnsOffloadRule.java
index 454f35a..06d875f 100644
--- a/src/android/net/apf/MdnsOffloadRule.java
+++ b/src/android/net/apf/MdnsOffloadRule.java
@@ -41,10 +41,15 @@
     @NonNull
     public final List<Matcher> mMatchers;
 
+    @NonNull
+    public final String mFullServiceName;
+
     /**
      * Construct an mDNS offload rule.
      */
-    public MdnsOffloadRule(@NonNull List<Matcher> matchers, @Nullable byte[] offloadPayload) {
+    public MdnsOffloadRule(@NonNull String fullServiceName, @NonNull List<Matcher> matchers,
+            @Nullable byte[] offloadPayload) {
+        mFullServiceName = fullServiceName;
         mMatchers = matchers;
         mOffloadPayload = offloadPayload;
     }
@@ -86,34 +91,32 @@
         /**
          * The QTYPE from the mDNS query that this rule matches.
          */
-        public final int mQtype;
+        public final int[] mQtypes;
 
         /**
          * Creates a new Matcher.
          */
-        public Matcher(byte[] qnames, int qtype) {
+        public Matcher(byte[] qnames, int[] qtypes) {
             mQnames = qnames;
-            mQtype = qtype;
+            mQtypes = qtypes;
         }
 
         @Override
         public boolean equals(Object o) {
-            if (this == o) return true;
-            if (!(o instanceof Matcher that)) return false;
-            return mQtype == that.mQtype && Arrays.equals(mQnames, that.mQnames);
+            if (!(o instanceof Matcher matcher)) return false;
+            return Objects.deepEquals(mQnames, matcher.mQnames) && Objects.deepEquals(
+                    mQtypes, matcher.mQtypes);
         }
 
         @Override
         public int hashCode() {
-            int result = Objects.hash(mQtype);
-            result = 31 * result + Arrays.hashCode(mQnames);
-            return result;
+            return Objects.hash(Arrays.hashCode(mQnames), Arrays.hashCode(mQtypes));
         }
 
         @Override
         public String toString() {
-            return "Matcher{" + "mQnames=" + HexDump.toHexString(mQnames) + ", mQtype="
-                    + mQtype + '}';
+            return "Matcher{" + "mQnames=" + HexDump.toHexString(mQnames) + ", mQtypes="
+                    + Arrays.toString(mQtypes) + '}';
         }
     }
 
diff --git a/src/android/net/apf/ProcfsParsingUtils.java b/src/android/net/apf/ProcfsParsingUtils.java
index 4bac0f8..16fd4b1 100644
--- a/src/android/net/apf/ProcfsParsingUtils.java
+++ b/src/android/net/apf/ProcfsParsingUtils.java
@@ -15,18 +15,23 @@
  */
 package android.net.apf;
 
+import static com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ALL_HOST_MULTICAST;
+
 import android.annotation.NonNull;
 import android.net.MacAddress;
 import android.util.Log;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.HexDump;
+import com.android.net.module.util.HexDump;
 
 import java.io.BufferedReader;
 import java.io.IOException;
+import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Paths;
@@ -39,7 +44,9 @@
     private static final String IPV6_CONF_PATH = "/proc/sys/net/ipv6/conf/";
     private static final String IPV6_ANYCAST_PATH = "/proc/net/anycast6";
     private static final String ETHER_MCAST_PATH = "/proc/net/dev_mcast";
+    private static final String IPV4_MCAST_PATH = "/proc/net/igmp";
     private static final String IPV6_MCAST_PATH = "/proc/net/igmp6";
+    private static final String IPV4_DEFAULT_TTL_PATH = "/proc/sys/net/ipv4/ip_default_ttl";
 
     private ProcfsParsingUtils() {
     }
@@ -85,6 +92,23 @@
     }
 
     /**
+     * Parses the default TTL value from the procfs file lines.
+     */
+    @VisibleForTesting
+    public static int parseDefaultTtl(final List<String> lines) {
+        if (lines.size() != 1) {
+            return 64;  // default ttl value as per rfc1700
+        }
+        try {
+            // ttl must be in the range [1, 255]
+            return Math.max(1, Math.min(255, Integer.parseInt(lines.get(0))));
+        } catch (NumberFormatException e) {
+            Log.e(TAG, "failed to parse default ttl.", e);
+            return 64; // default ttl value as per rfc1700
+        }
+    }
+
+    /**
      * Parses anycast6 addresses associated with a specific interface from a list of strings.
      *
      * This function searches the input list for a line containing the specified interface name.
@@ -172,6 +196,90 @@
 
         return addresses;
     }
+
+    /**
+     * Parses IPv4 multicast addresses associated with a specific interface from a list of strings.
+     *
+     * @param lines A list of strings, each containing interface and IPv4 address information.
+     * @param ifname The name of the network interface for which to extract multicast addresses.
+     * @param endian The byte order of the address, almost always use native order.
+     * @return A list of Inet4Address objects representing the parsed IPv4 multicast addresses.
+     *         If an error occurs during parsing,
+     *         a list contains IPv4 all host (224.0.0.1) is returned.
+     */
+    @VisibleForTesting
+    public static List<Inet4Address> parseIPv4MulticastAddresses(
+            @NonNull List<String> lines, @NonNull String ifname, @NonNull ByteOrder endian) {
+        final List<Inet4Address> ipAddresses = new ArrayList<>();
+
+        try {
+            String name = "";
+            // parse output similar to `ip maddr` command (iproute2/ip/ipmaddr.c#read_igmp())
+            for (String line : lines) {
+                final String[] parts = line.trim().split("\\s+");
+                if (!line.startsWith("\t")) {
+                    name = parts[1];
+                    if (name.endsWith(":")) {
+                        name = name.substring(0, name.length() - 1);
+                    }
+                    continue;
+                }
+
+                if (!name.equals(ifname)) {
+                    continue;
+                }
+
+                final String hexIp = parts[0];
+                final byte[] ipArray = HexDump.hexStringToByteArray(hexIp);
+                final byte[] convertArray =
+                    (endian == ByteOrder.LITTLE_ENDIAN)
+                        ? convertIPv4BytesToBigEndian(ipArray) : ipArray;
+                final Inet4Address ipv4Address =
+                        (Inet4Address) InetAddress.getByAddress(convertArray);
+
+                ipAddresses.add(ipv4Address);
+            }
+        } catch (Exception e) {
+            Log.wtf(TAG, "failed to convert to Inet4Address.", e);
+            // always return IPv4 all host address (224.0.0.1) if any error during parsing.
+            // this aligns with kernel behavior, it will join 224.0.0.1 when the interface is up.
+            ipAddresses.clear();
+            ipAddresses.add(IPV4_ADDR_ALL_HOST_MULTICAST);
+        }
+
+        return ipAddresses;
+    }
+
+    /**
+     * Converts an IPv4 address from little-endian byte order to big-endian byte order.
+     *
+     * @param bytes The IPv4 address in little-endian byte order.
+     * @return The IPv4 address in big-endian byte order.
+     */
+    private static byte[] convertIPv4BytesToBigEndian(byte[] bytes) {
+        final ByteBuffer buffer = ByteBuffer.wrap(bytes);
+        buffer.order(ByteOrder.LITTLE_ENDIAN);
+        final ByteBuffer bigEndianBuffer = ByteBuffer.allocate(4);
+        bigEndianBuffer.order(ByteOrder.BIG_ENDIAN);
+        bigEndianBuffer.putInt(buffer.getInt());
+        return bigEndianBuffer.array();
+    }
+
+    /**
+     * Returns the default TTL value for IPv4 packets.
+     */
+    public static int getIpv4DefaultTtl() {
+        return parseDefaultTtl(readFile(IPV4_DEFAULT_TTL_PATH));
+    }
+
+    /**
+     * Returns the default HopLimit value for IPv6 packets.
+     */
+    public static int getIpv6DefaultHopLimit(@NonNull String ifname) {
+        final String hopLimitPath = IPV6_CONF_PATH + ifname + "/hop_limit";
+        return parseDefaultTtl(readFile(hopLimitPath));
+    }
+
     /**
      * Returns the traffic class for the specified interface.
      * The function loads the existing traffic class from the file
@@ -228,4 +336,19 @@
         final List<String> lines = readFile(IPV6_MCAST_PATH);
         return parseIPv6MulticastAddresses(lines, ifname);
     }
+
+    /**
+     * The function loads the existing IPv4 multicast addresses from the file `/proc/net/igmp6`.
+     * If the file does not exist or the interface is not found, the function returns empty list.
+     *
+     * @param ifname The name of the network interface to query.
+     * @return A list of Inet4Address objects representing the IPv4 multicast addresses
+     *         found for the interface.
+     *         If the file cannot be read or there are no addresses, an empty list is returned.
+     */
+    public static List<Inet4Address> getIPv4MulticastAddresses(@NonNull String ifname) {
+        final List<String> lines = readFile(IPV4_MCAST_PATH);
+        // follow the same pattern as NetlinkMonitor#handlePacket() for device's endian order
+        return parseIPv4MulticastAddresses(lines, ifname, ByteOrder.nativeOrder());
+    }
 }
diff --git a/src/android/net/ip/ConnectivityPacketTracker.java b/src/android/net/ip/ConnectivityPacketTracker.java
index ce4f6ae..35a71c7 100644
--- a/src/android/net/ip/ConnectivityPacketTracker.java
+++ b/src/android/net/ip/ConnectivityPacketTracker.java
@@ -48,6 +48,7 @@
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.Locale;
 import java.util.Objects;
 
 
@@ -79,14 +80,23 @@
         }
 
         /**
-         * Create a socket to read RAs.
+         * Creates a raw packet socket for reading network packets.
+         *
+         * This method creates a socket and binds it to the specified network interface index,
+         * and optionally attaches a control packet filter.
+         *
+         * @param ifIndex      The index of the network interface to bind the socket to.
+         * @param attachFilter If true, attaches a control packet filter to the socket.
+         * @return The FileDescriptor of the created socket, or null if an error occurred.
          */
         @Nullable
-        public FileDescriptor createPacketReaderSocket(int ifIndex) {
+        public FileDescriptor createPacketReaderSocket(int ifIndex, boolean attachFilter) {
             FileDescriptor socket = null;
             try {
                 socket = Os.socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, 0);
-                NetworkStackUtils.attachControlPacketFilter(socket);
+                if (attachFilter) {
+                    NetworkStackUtils.attachControlPacketFilter(socket);
+                }
                 Os.bind(socket, makePacketSocketAddress(ETH_P_ALL, ifIndex));
             } catch (ErrnoException | IOException e) {
                 final String msg = "Failed to create packet tracking socket: ";
@@ -98,6 +108,12 @@
             return socket;
         }
 
+
+        /**
+         * Gets the maximum size of a captured packet.
+         *
+         * @return The maximum capture packet size.
+         */
         public int getMaxCapturePktSize() {
             return MAX_CAPTURE_PACKET_SIZE;
         }
@@ -129,13 +145,18 @@
     // store packet hex string in uppercase as key, receive packet count as value
     private final LruCache<String, Integer> mPacketCache;
     private final Dependencies mDependencies;
+    private final boolean mAttachFilter;
     private long mLastRateLimitLogTimeMs = 0;
     private boolean mRunning;
     private boolean mCapturing;
     private String mDisplayName;
 
-    public ConnectivityPacketTracker(Handler h, InterfaceParams ifParams, LocalLog log) {
-        this(h, ifParams, log, new Dependencies(log));
+    public ConnectivityPacketTracker(
+            Handler h,
+            InterfaceParams ifParams,
+            LocalLog log,
+            boolean attachFilter) {
+        this(h, ifParams, log, new Dependencies(log), attachFilter);
     }
 
     /**
@@ -163,7 +184,9 @@
      * @return The count of packets matching the pattern, or 0 if no matches are found
      */
     public int getMatchedPacketCount(String packet) {
-        final Integer count = mPacketCache.get(packet);
+        // always convert to upper case since we use upper case when capturing
+        final String packetPattern = packet.toUpperCase(Locale.ROOT);
+        final Integer count = mPacketCache.get(packetPattern);
         return (count != null) ? count : 0;
     }
 
@@ -189,9 +212,12 @@
             @NonNull Handler handler,
             @NonNull InterfaceParams ifParams,
             @NonNull LocalLog log,
-            @NonNull Dependencies dependencies) {
+            @NonNull Dependencies dependencies,
+            boolean attachFilter
+    ) {
         mTag = TAG + "." + Objects.requireNonNull(ifParams).name;
         mLog = log;
+        mAttachFilter = attachFilter;
         mPacketListener = new PacketListener(handler, ifParams);
         mDependencies = dependencies;
         mPacketCache = new LruCache<>(mDependencies.getMaxCapturePktSize());
@@ -207,7 +233,7 @@
 
         @Override
         protected FileDescriptor createFd() {
-            return mDependencies.createPacketReaderSocket(mInterface.index);
+            return mDependencies.createPacketReaderSocket(mInterface.index, mAttachFilter);
         }
 
         @Override
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java
index 493f36f..7631a1c 100644
--- a/src/android/net/ip/IpClient.java
+++ b/src/android/net/ip/IpClient.java
@@ -16,7 +16,7 @@
 
 package android.net.ip;
 
-import static android.content.pm.PackageManager.FEATURE_WATCH;
+import static android.content.pm.PackageManager.FEATURE_LEANBACK;
 import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM;
 import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_CONFIRM;
 import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC;
@@ -52,11 +52,13 @@
 import static android.net.ip.IpClient.IpClientCommands.EVENT_NUD_FAILURE_QUERY_FAILURE;
 import static android.net.ip.IpClient.IpClientCommands.EVENT_NUD_FAILURE_QUERY_SUCCESS;
 import static android.net.ip.IpClient.IpClientCommands.EVENT_NUD_FAILURE_QUERY_TIMEOUT;
+import static android.net.ip.IpClient.IpClientCommands.EVENT_PIO_PREFIX_UPDATE;
 import static android.net.ip.IpClient.IpClientCommands.EVENT_PRE_DHCP_ACTION_COMPLETE;
 import static android.net.ip.IpClient.IpClientCommands.EVENT_PROVISIONING_TIMEOUT;
 import static android.net.ip.IpClient.IpClientCommands.EVENT_READ_PACKET_FILTER_COMPLETE;
 import static android.net.ip.IpClientLinkObserver.IpClientNetlinkMonitor;
 import static android.net.ip.IpClientLinkObserver.IpClientNetlinkMonitor.INetlinkMessageProcessor;
+import static android.net.ip.IpClientLinkObserver.PrefixInfo;
 import static android.net.ip.IpReachabilityMonitor.INVALID_REACHABILITY_LOSS_TYPE;
 import static android.net.ip.IpReachabilityMonitor.nudEventTypeToInt;
 import static android.net.util.SocketUtils.makePacketSocketAddress;
@@ -80,16 +82,25 @@
 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_ENABLE;
 import static com.android.networkstack.util.NetworkStackUtils.APF_HANDLE_ARP_OFFLOAD;
+import static com.android.networkstack.util.NetworkStackUtils.APF_HANDLE_IGMP_OFFLOAD;
+import static com.android.networkstack.util.NetworkStackUtils.APF_HANDLE_IGMP_OFFLOAD_VERSION;
+import static com.android.networkstack.util.NetworkStackUtils.APF_HANDLE_MLD_OFFLOAD;
+import static com.android.networkstack.util.NetworkStackUtils.APF_HANDLE_MLD_OFFLOAD_VERSION;
 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_HANDLE_PING4_OFFLOAD;
+import static com.android.networkstack.util.NetworkStackUtils.APF_HANDLE_PING4_OFFLOAD_VERSION;
+import static com.android.networkstack.util.NetworkStackUtils.APF_HANDLE_PING6_OFFLOAD;
+import static com.android.networkstack.util.NetworkStackUtils.APF_HANDLE_PING6_OFFLOAD_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.APF_POLLING_COUNTERS_VERSION;
-import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION;
-import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION;
+import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_DHCPV6_PD_PREFERRED_FLAG_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_IGNORE_LOW_RA_LIFETIME_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION;
+import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_REPLACE_NETD_WITH_NETLINK_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.createInet6AddressFromEui64;
+import static com.android.networkstack.util.NetworkStackUtils.isAtLeast25Q2;
 import static com.android.networkstack.util.NetworkStackUtils.macAddressToEui64;
 import static com.android.server.util.PermissionUtil.enforceNetworkStackCallingPermission;
 
@@ -115,10 +126,8 @@
 import android.net.RouteInfo;
 import android.net.TcpKeepalivePacketDataParcelable;
 import android.net.Uri;
-import android.net.apf.AndroidPacketFilter;
 import android.net.apf.ApfCapabilities;
 import android.net.apf.ApfFilter;
-import android.net.apf.LegacyApfFilter;
 import android.net.dhcp.DhcpClient;
 import android.net.dhcp.DhcpPacket;
 import android.net.dhcp6.Dhcp6Client;
@@ -185,6 +194,7 @@
 import com.android.networkstack.apishim.common.ShimUtils;
 import com.android.networkstack.metrics.IpProvisioningMetrics;
 import com.android.networkstack.metrics.NetworkQuirkMetrics;
+import com.android.networkstack.metrics.NetworkStackStatsLog;
 import com.android.networkstack.packets.NeighborAdvertisement;
 import com.android.networkstack.packets.NeighborSolicitation;
 import com.android.networkstack.util.NetworkStackUtils;
@@ -210,6 +220,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Random;
 import java.util.Set;
 import java.util.StringJoiner;
 import java.util.concurrent.CompletableFuture;
@@ -322,6 +333,7 @@
         private final NetworkInformationShim mShim;
 
         private final boolean mApfDebug;
+        private final Random mRandom = new Random();
 
         @VisibleForTesting
         protected IpClientCallbacksWrapper(IIpClientCallbacks callback, @NonNull SharedLog log,
@@ -385,6 +397,13 @@
          */
         public void onProvisioningSuccess(LinkProperties newLp) {
             log("onProvisioningSuccess({" + newLp + "})");
+            // We log this error, which has a 1 in 1,000,000 probability, as a heartbeat for
+            // terrible error reporting.
+            if (mRandom.nextInt(1000000) == 0) {
+                NetworkStackStatsLog.write(
+                        NetworkStackStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED,
+                        NetworkStackStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_UNKNOWN);
+            }
             try {
                 mCallback.onProvisioningSuccess(mShim.makeSensitiveFieldsParcelingCopy(newLp));
             } catch (RemoteException e) {
@@ -449,8 +468,9 @@
         /**
          * Called to indicate that a new APF program must be installed to filter incoming packets.
          */
-        public boolean installPacketFilter(byte[] filter) {
-            log("installPacketFilter(byte[" + filter.length + "])");
+        public boolean installPacketFilter(byte[] filter, @NonNull String filterConfig) {
+            log("installPacketFilter(byte[" + filter.length + "])" + " config: "
+                    + filterConfig);
             try {
                 if (mApfDebug) {
                     mApfLog.log("updated APF program: " + HexDump.toHexString(filter));
@@ -608,6 +628,7 @@
         static final int EVENT_NUD_FAILURE_QUERY_TIMEOUT = 21;
         static final int EVENT_NUD_FAILURE_QUERY_SUCCESS = 22;
         static final int EVENT_NUD_FAILURE_QUERY_FAILURE = 23;
+        static final int EVENT_PIO_PREFIX_UPDATE = 24;
         // Internal commands to use instead of trying to call transitionTo() inside
         // a given State's enter() method. Calling transitionTo() from enter/exit
         // encounters a Log.wtf() that can cause trouble on eng builds.
@@ -633,6 +654,8 @@
 
     @VisibleForTesting
     static final String CONFIG_ACCEPT_RA_MIN_LFT = "ipclient_accept_ra_min_lft";
+    @VisibleForTesting
+    static final int DEFAULT_ACCEPT_RA_MIN_LFT = 180;
 
     @VisibleForTesting
     static final String CONFIG_APF_COUNTER_POLLING_INTERVAL_SECS =
@@ -689,7 +712,7 @@
 
     private static final int IPMEMORYSTORE_TIMEOUT_MS = 1000;
     @VisibleForTesting
-    static final long SIX_HOURS_IN_MS = 6 * 3600 * 1000L;
+    public static final long SIX_HOURS_IN_MS = 6 * 3600 * 1000L;
     @VisibleForTesting
     public static final long ONE_DAY_IN_MS = 4 * SIX_HOURS_IN_MS;
     @VisibleForTesting
@@ -768,6 +791,7 @@
     private final String mInterfaceName;
     @VisibleForTesting
     protected final IpClientCallbacksWrapper mCallback;
+    private final ApfFilter.IApfController mIpClientApfController;
     private final Dependencies mDependencies;
     private final ConnectivityManager mCm;
     private final INetd mNetd;
@@ -798,16 +822,21 @@
     private final int mNudFailureCountDailyThreshold;
     private final int mNudFailureCountWeeklyThreshold;
 
-    // Experiment flag read from device config.
-    private final boolean mDhcp6PrefixDelegationEnabled;
-    private final boolean mUseNewApfFilter;
+    // Experiment flags read from device config.
     private final boolean mIsAcceptRaMinLftEnabled;
     private final boolean mEnableApfPollingCounters;
     private final boolean mPopulateLinkAddressLifetime;
-    private final boolean mApfShouldHandleArpOffload;
-    private final boolean mApfShouldHandleNdOffload;
-    private final boolean mApfShouldHandleMdnsOffload;
+    private final boolean mEnableApf;
+    private final boolean mApfHandleArpOffload;
+    private final boolean mApfHandleNdOffload;
+    private final boolean mApfHandleMdnsOffload;
+    private final boolean mApfHandleIgmpOffload;
+    private final boolean mApfHandleMldOffload;
+    private final boolean mApfHandleIpv4PingOffload;
+    private final boolean mApfHandleIpv6PingOffload;
     private final boolean mIgnoreNudFailureEnabled;
+    private final boolean mDhcp6PdPreferredFlagEnabled;
+    private final boolean mReplaceNetdWithNetlinkEnabled;
 
     private InterfaceParams mInterfaceParams;
 
@@ -822,7 +851,7 @@
     private DhcpResults mDhcpResults;
     private String mTcpBufferSizes;
     private ProxyInfo mHttpProxy;
-    private AndroidPacketFilter mApfFilter;
+    private ApfFilter mApfFilter;
     private String mL2Key; // The L2 key for this network, for writing into the memory store
     private String mCluster; // The cluster for this network, for writing into the memory store
     private int mCreatorUid; // Uid of app creating the wifi configuration
@@ -978,17 +1007,12 @@
          * APF programs.
          * @see ApfFilter#maybeCreate
          */
-        public AndroidPacketFilter maybeCreateApfFilter(Handler handler, Context context,
+        public ApfFilter maybeCreateApfFilter(Handler handler, Context context,
                 ApfFilter.ApfConfiguration config, InterfaceParams ifParams,
-                IpClientCallbacksWrapper cb, NetworkQuirkMetrics networkQuirkMetrics,
-                boolean useNewApfFilter) {
-            if (useNewApfFilter) {
-                return ApfFilter.maybeCreate(handler, context, config, ifParams, cb,
-                        networkQuirkMetrics);
-            } else {
-                return LegacyApfFilter.maybeCreate(context, config, ifParams, cb,
-                        networkQuirkMetrics);
-            }
+                ApfFilter.IApfController apfController,
+                NetworkQuirkMetrics networkQuirkMetrics) {
+            return ApfFilter.maybeCreate(handler, context, config, ifParams, apfController,
+                    networkQuirkMetrics);
         }
 
         /**
@@ -1012,8 +1036,10 @@
          * Create an IpClientNetlinkMonitor instance.
          */
         public IpClientNetlinkMonitor makeIpClientNetlinkMonitor(Handler h, SharedLog log,
-                String tag, int sockRcvbufSize, INetlinkMessageProcessor p) {
-            return new IpClientNetlinkMonitor(h, log, tag, sockRcvbufSize, p);
+                String tag, int sockRcvbufSize, boolean isDhcp6PdPreferredFlagEnabled,
+                INetlinkMessageProcessor p) {
+            return new IpClientNetlinkMonitor(h, log, tag, sockRcvbufSize,
+                    isDhcp6PdPreferredFlagEnabled, p);
         }
     }
 
@@ -1050,34 +1076,61 @@
         mApfDebug = Log.isLoggable(ApfFilter.class.getSimpleName(), Log.DEBUG);
         mMsgStateLogger = new MessageHandlingLogger();
         mCallback = new IpClientCallbacksWrapper(callback, mLog, mApfLog, mShim, mApfDebug);
+        mIpClientApfController = new ApfFilter.IApfController() {
+            @Override
+            public boolean installPacketFilter(byte[] filter, String filterConfig) {
+                return mCallback.installPacketFilter(filter, filterConfig);
+            }
+
+            @Override
+            public void readPacketFilterRam(String event) {
+                mCallback.startReadPacketFilter(event);
+            }
+        };
 
         // TODO: Consider creating, constructing, and passing in some kind of
         // InterfaceController.Dependencies class.
         mNetd = deps.getNetd(mContext);
         mInterfaceCtrl = new InterfaceController(mInterfaceName, mNetd, mLog);
 
-        mDhcp6PrefixDelegationEnabled = mDependencies.isFeatureEnabled(mContext,
-                IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION);
-
-        final boolean isWatch = mContext.getPackageManager().hasSystemFeature(FEATURE_WATCH);
         mAcceptRaMinLft = mDependencies.getDeviceConfigPropertyInt(CONFIG_ACCEPT_RA_MIN_LFT,
-                isWatch ? 900 : 180);
+                DEFAULT_ACCEPT_RA_MIN_LFT);
         mApfCounterPollingIntervalMs = mDependencies.getDeviceConfigPropertyInt(
                 CONFIG_APF_COUNTER_POLLING_INTERVAL_SECS,
                 DEFAULT_APF_COUNTER_POLLING_INTERVAL_SECS) * DateUtils.SECOND_IN_MILLIS;
-        mUseNewApfFilter = SdkLevel.isAtLeastV() || mDependencies.isFeatureNotChickenedOut(context,
-                APF_NEW_RA_FILTER_VERSION);
         mEnableApfPollingCounters = mDependencies.isFeatureEnabled(context,
                 APF_POLLING_COUNTERS_VERSION);
         mIsAcceptRaMinLftEnabled =
                 SdkLevel.isAtLeastV() || mDependencies.isFeatureEnabled(context,
                         IPCLIENT_IGNORE_LOW_RA_LIFETIME_VERSION);
-        mApfShouldHandleArpOffload = mDependencies.isFeatureNotChickenedOut(
+        mEnableApf = mDependencies.isFeatureNotChickenedOut(mContext, APF_ENABLE);
+        mApfHandleArpOffload = mDependencies.isFeatureNotChickenedOut(
                 mContext, APF_HANDLE_ARP_OFFLOAD);
-        mApfShouldHandleNdOffload = mDependencies.isFeatureNotChickenedOut(
+        mApfHandleNdOffload = mDependencies.isFeatureNotChickenedOut(
                 mContext, APF_HANDLE_ND_OFFLOAD);
-        // TODO: turn on APF mDNS offload.
-        mApfShouldHandleMdnsOffload = false;
+        // TODO: turn on APF mDNS offload on handhelds.
+        mApfHandleMdnsOffload = isAtLeast25Q2() && context.getPackageManager().hasSystemFeature(
+                FEATURE_LEANBACK);
+        mApfHandleIgmpOffload =
+                mDependencies.isFeatureNotChickenedOut(mContext, APF_HANDLE_IGMP_OFFLOAD)
+                    && (isAtLeast25Q2()
+                        || mDependencies.isFeatureEnabled(context, APF_HANDLE_IGMP_OFFLOAD_VERSION)
+                    );
+        mApfHandleMldOffload =
+                mDependencies.isFeatureNotChickenedOut(mContext, APF_HANDLE_MLD_OFFLOAD)
+                    && (isAtLeast25Q2()
+                        || mDependencies.isFeatureEnabled(context, APF_HANDLE_MLD_OFFLOAD_VERSION)
+                    );
+        mApfHandleIpv4PingOffload =
+                mDependencies.isFeatureNotChickenedOut(mContext, APF_HANDLE_PING4_OFFLOAD)
+                    && (isAtLeast25Q2()
+                        || mDependencies.isFeatureEnabled(context, APF_HANDLE_PING4_OFFLOAD_VERSION)
+                    );
+        mApfHandleIpv6PingOffload =
+                mDependencies.isFeatureNotChickenedOut(mContext, APF_HANDLE_PING6_OFFLOAD)
+                    && (isAtLeast25Q2()
+                        || mDependencies.isFeatureEnabled(context, APF_HANDLE_PING6_OFFLOAD_VERSION)
+                );
         mPopulateLinkAddressLifetime = mDependencies.isFeatureEnabled(context,
                 IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION);
         mIgnoreNudFailureEnabled = mDependencies.isFeatureEnabled(mContext,
@@ -1088,9 +1141,12 @@
         mNudFailureCountWeeklyThreshold = mDependencies.getDeviceConfigPropertyInt(
                 CONFIG_NUD_FAILURE_COUNT_WEEKLY_THRESHOLD,
                 DEFAULT_NUD_FAILURE_COUNT_WEEKLY_THRESHOLD);
-
+        mDhcp6PdPreferredFlagEnabled =
+                mDependencies.isFeatureEnabled(mContext, IPCLIENT_DHCPV6_PD_PREFERRED_FLAG_VERSION);
+        mReplaceNetdWithNetlinkEnabled = mDependencies.isFeatureEnabled(mContext,
+                IPCLIENT_REPLACE_NETD_WITH_NETLINK_VERSION);
         IpClientLinkObserver.Configuration config = new IpClientLinkObserver.Configuration(
-                mAcceptRaMinLft, mPopulateLinkAddressLifetime);
+                mAcceptRaMinLft, mPopulateLinkAddressLifetime, mDhcp6PdPreferredFlagEnabled);
 
         mLinkObserver = new IpClientLinkObserver(
                 mContext, getHandler(),
@@ -1125,7 +1181,7 @@
                             // 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()) {
+                            if (mApfFilter == null || !mApfFilter.enableNdOffload()) {
                                 // Clat interface information is spliced into LinkProperties by
                                 // ConnectivityService, so it cannot be added to the LinkProperties
                                 // here as those propagate back to ConnectivityService.
@@ -1137,6 +1193,12 @@
                             }
                         });
                     }
+
+                    @Override
+                    public void onNewPrefix(PrefixInfo info) {
+                        if (!mDhcp6PdPreferredFlagEnabled) return;
+                        sendMessage(EVENT_PIO_PREFIX_UPDATE, info);
+                    }
                 },
                 config, mLog, mDependencies
         );
@@ -1288,10 +1350,6 @@
         mLinkObserver.shutdown();
     }
 
-    private boolean isGratuitousArpNaRoamingEnabled() {
-        return mDependencies.isFeatureNotChickenedOut(mContext, IPCLIENT_GARP_NA_ROAMING_VERSION);
-    }
-
     @VisibleForTesting
     static MacAddress getInitialBssid(final Layer2Information layer2Info,
             final ScanResultInfo scanResultInfo, boolean isAtLeastS) {
@@ -1337,15 +1395,6 @@
             doImmediateProvisioningFailure(IpManagerEvent.ERROR_INVALID_PROVISIONING);
             return;
         }
-
-        mCurrentBssid = getInitialBssid(req.mLayer2Info, req.mScanResultInfo,
-                ShimUtils.isAtLeastS());
-        mCurrentApfCapabilities = req.mApfCapabilities;
-        mCreatorUid = req.mCreatorUid;
-        if (req.mLayer2Info != null) {
-            mL2Key = req.mLayer2Info.mL2Key;
-            mCluster = req.mLayer2Info.mCluster;
-        }
         sendMessage(CMD_START, new android.net.shared.ProvisioningConfiguration(req));
     }
 
@@ -1477,7 +1526,7 @@
         }
 
         // Thread-unsafe access to mApfFilter but just used for debugging.
-        final AndroidPacketFilter apfFilter = mApfFilter;
+        final ApfFilter apfFilter = mApfFilter;
         final android.net.shared.ProvisioningConfiguration provisioningConfig = mConfiguration;
         final ApfCapabilities apfCapabilities = mCurrentApfCapabilities;
 
@@ -1489,7 +1538,12 @@
                     apfCapabilities.apfVersionSupported)) {
                 // Request a new snapshot, then wait for it.
                 mApfDataSnapshotComplete.close();
-                mCallback.startReadPacketFilter("dumpsys");
+                // To ensure long-term flexibility and support for different APF controller
+                // implementations (e.g., Ethtool-based), we use apfFilter.getApfController()
+                // instead of directly accessing mIpClientApfController. This approach makes
+                // code reusable and simplifies future transitions to alternative APF
+                // controllers.
+                apfFilter.getApfController().readPacketFilterRam("dumpsys");
                 if (!mApfDataSnapshotComplete.block(1000)) {
                     pw.print("TIMEOUT: DUMPING STALE APF SNAPSHOT");
                 }
@@ -1570,7 +1624,7 @@
             }
             // Request a new snapshot, then wait for it.
             mApfDataSnapshotComplete.close();
-            mCallback.startReadPacketFilter("shell command");
+            mApfFilter.getApfController().readPacketFilterRam("shell command");
             if (!mApfDataSnapshotComplete.block(5000 /* ms */)) {
                 throw new RuntimeException("Error: Failed to read APF program");
             }
@@ -1601,7 +1655,8 @@
                         if (mApfFilter.isRunning()) {
                             throw new IllegalStateException("APF filter must first be paused");
                         }
-                        mCallback.installPacketFilter(HexDump.hexStringToByteArray(optarg));
+                        mApfFilter.getApfController().installPacketFilter(
+                                HexDump.hexStringToByteArray(optarg), "program from shell command");
                         result.complete("success");
                         break;
                     case "capabilities":
@@ -1981,7 +2036,7 @@
         // removed list before checking the added list(e.g. anyway we can add the removed prefix
         // back again).
         for (LinkAddress la : results.removed) {
-            if (mDhcp6PrefixDelegationEnabled && isIpv6StableDelegatedAddress(la)) {
+            if (isIpv6StableDelegatedAddress(la)) {
                 final IpPrefix prefix = new IpPrefix(la.getAddress(), RFC7421_PREFIX_LENGTH);
                 mDelegatedPrefixes.remove(prefix);
             }
@@ -1989,7 +2044,7 @@
         }
 
         for (LinkAddress la : results.added) {
-            if (mDhcp6PrefixDelegationEnabled && isIpv6StableDelegatedAddress(la)) {
+            if (isIpv6StableDelegatedAddress(la)) {
                 final IpPrefix prefix = new IpPrefix(la.getAddress(), RFC7421_PREFIX_LENGTH);
                 mDelegatedPrefixes.add(prefix);
             }
@@ -2032,22 +2087,20 @@
         }
 
         // [4] Add route with delegated prefix according to the global address update.
-        if (mDhcp6PrefixDelegationEnabled) {
-            for (IpPrefix destination : mDelegatedPrefixes) {
-                // Direct-connected route to delegated prefix. Add RTN_UNREACHABLE to
-                // this route based on the delegated prefix. To prevent the traffic loop
-                // between host and upstream delegated router. Because we specify the
-                // IFA_F_NOPREFIXROUTE when adding the IPv6 address, the kernel does not
-                // create a delegated prefix route, as a result, the user space won't
-                // receive any RTM_NEWROUTE message about the delegated prefix, we still
-                // need to install an unreachable route for the delegated prefix manually
-                // in LinkProperties to notify the caller this update.
-                // TODO: support RTN_BLACKHOLE in netd and use that on newer Android
-                // versions.
-                final RouteInfo route = new RouteInfo(destination,
-                        null /* gateway */, mInterfaceName, RTN_UNREACHABLE);
-                newLp.addRoute(route);
-            }
+        for (IpPrefix destination : mDelegatedPrefixes) {
+            // Direct-connected route to delegated prefix. Add RTN_UNREACHABLE to
+            // this route based on the delegated prefix. To prevent the traffic loop
+            // between host and upstream delegated router. Because we specify the
+            // IFA_F_NOPREFIXROUTE when adding the IPv6 address, the kernel does not
+            // create a delegated prefix route, as a result, the user space won't
+            // receive any RTM_NEWROUTE message about the delegated prefix, we still
+            // need to install an unreachable route for the delegated prefix manually
+            // in LinkProperties to notify the caller this update.
+            // TODO: support RTN_BLACKHOLE in netd and use that on newer Android
+            // versions.
+            final RouteInfo route = new RouteInfo(destination,
+                    null /* gateway */, mInterfaceName, RTN_UNREACHABLE);
+            newLp.addRoute(route);
         }
 
         // [5] Add in TCP buffer sizes and HTTP Proxy config, if available.
@@ -2290,8 +2343,7 @@
         // doesn't complete with success after timeout. This check also handles IPv6-only link
         // local mode case, since there will be no IPv6 default route in that mode even with Prefix
         // Delegation experiment flag enabled.
-        if (mDhcp6PrefixDelegationEnabled
-                && newLp.hasIpv6DefaultRoute()
+        if (newLp.hasIpv6DefaultRoute()
                 && mIpv6AutoconfTimeoutAlarm == null) {
             mIpv6AutoconfTimeoutAlarm = new WakeupMessage(mContext, getHandler(),
                     mTag + ".EVENT_IPV6_AUTOCONF_TIMEOUT", EVENT_IPV6_AUTOCONF_TIMEOUT);
@@ -2507,7 +2559,6 @@
     }
 
     private void startDhcp6PrefixDelegation() {
-        if (!mDhcp6PrefixDelegationEnabled) return;
         if (mDhcp6Client != null) {
             Log.wtf(mTag, "Dhcp6Client should never be non-null in startDhcp6PrefixDelegation");
             return;
@@ -2630,7 +2681,7 @@
         setIpv6Sysctl(ACCEPT_RA, 2);
         setIpv6Sysctl(ACCEPT_RA_DEFRTR, 1);
         maybeRestoreDadTransmits();
-        if (mUseNewApfFilter && mIsAcceptRaMinLftEnabled
+        if (mIsAcceptRaMinLftEnabled
                 && mDependencies.hasIpv6Sysctl(mInterfaceName, ACCEPT_RA_MIN_LFT)) {
             setIpv6Sysctl(ACCEPT_RA_MIN_LFT, 0 /* sysctl default */);
         }
@@ -2658,11 +2709,18 @@
 
         if (params.defaultMtu == mInterfaceParams.defaultMtu) return;
 
-        try {
-            mNetd.interfaceSetMtu(mInterfaceName, mInterfaceParams.defaultMtu);
-        } catch (RemoteException | ServiceSpecificException e) {
-            logError("Couldn't reset MTU on " + mInterfaceName + " from "
-                    + params.defaultMtu + " to " + mInterfaceParams.defaultMtu, e);
+        if (mReplaceNetdWithNetlinkEnabled) {
+            if (!NetlinkUtils.setInterfaceMtu(mInterfaceName, mInterfaceParams.defaultMtu)) {
+                logError("Couldn't reset MTU on " + mInterfaceName + " from "
+                        + params.defaultMtu + " to " + mInterfaceParams.defaultMtu);
+            }
+        } else {
+            try {
+                mNetd.interfaceSetMtu(mInterfaceName, mInterfaceParams.defaultMtu);
+            } catch (RemoteException | ServiceSpecificException e) {
+                logError("Couldn't reset MTU on " + mInterfaceName + " from "
+                        + params.defaultMtu + " to " + mInterfaceParams.defaultMtu, e);
+            }
         }
     }
 
@@ -2695,10 +2753,8 @@
 
         // Before trigger probing to the critical neighbors, send Gratuitous ARP
         // and Neighbor Advertisment in advance to propgate host's IPv4/v6 addresses.
-        if (isGratuitousArpNaRoamingEnabled()) {
-            maybeSendGratuitousARP(mLinkProperties);
-            maybeSendGratuitousNAs(mLinkProperties, true /* isGratuitousNaAfterRoaming */);
-        }
+        maybeSendGratuitousARP(mLinkProperties);
+        maybeSendGratuitousNAs(mLinkProperties, true /* isGratuitousNaAfterRoaming */);
 
         // Check whether attempting to refresh previous IP lease on specific networks or need to
         // probe the critical neighbors proactively on L2 roaming happened. The NUD probe on the
@@ -2721,15 +2777,20 @@
     }
 
     @Nullable
-    private AndroidPacketFilter maybeCreateApfFilter(final ApfCapabilities apfCaps) {
+    private ApfFilter maybeCreateApfFilter(final ApfCapabilities apfCaps) {
         ApfFilter.ApfConfiguration apfConfig = new ApfFilter.ApfConfiguration();
-        if (apfCaps == null) {
+        if (apfCaps == null || !mEnableApf) {
             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 (apfCaps.apfPacketFormat != ARPHRD_ETHER) return null;
-        if (SdkLevel.isAtLeastS()) {
+        // For devices declare APFv3+ support but have less than 1024 bytes of RAM available for
+        // the APF, set the APF version to v2. The counter region will use a few hundred bytes of
+        // RAM. If the RAM size is too small, we should reserve that region for program use.
+        if (apfCaps.apfVersionSupported >= 3 && apfCaps.maximumApfProgramSize < 1024) {
+            apfConfig.apfVersionSupported = 2;
+        } else if (SdkLevel.isAtLeastS()) {
             apfConfig.apfVersionSupported = apfCaps.apfVersionSupported;
         } else {
             // In Android R, ApfCapabilities#hasDataAccess() can be modified by OEMs. The
@@ -2761,7 +2822,7 @@
         // Check the feature flag first before reading IPv6 sysctl, which can prevent from
         // triggering a potential kernel bug about the sysctl.
         // TODO: add unit test to check if the setIpv6Sysctl() is called or not.
-        if (mIsAcceptRaMinLftEnabled && mUseNewApfFilter
+        if (mIsAcceptRaMinLftEnabled
                 && mDependencies.hasIpv6Sysctl(mInterfaceName, ACCEPT_RA_MIN_LFT)) {
             setIpv6Sysctl(ACCEPT_RA_MIN_LFT, mAcceptRaMinLft);
             final Integer acceptRaMinLft = getIpv6Sysctl(ACCEPT_RA_MIN_LFT);
@@ -2769,13 +2830,19 @@
         } else {
             apfConfig.acceptRaMinLft = 0;
         }
-        apfConfig.shouldHandleArpOffload = mApfShouldHandleArpOffload;
-        apfConfig.shouldHandleNdOffload = mApfShouldHandleNdOffload;
-        apfConfig.shouldHandleMdnsOffload = mApfShouldHandleMdnsOffload;
+        apfConfig.handleArpOffload = mApfHandleArpOffload;
+        apfConfig.handleNdOffload = mApfHandleNdOffload;
+        apfConfig.handleMdnsOffload = mApfHandleMdnsOffload;
+        apfConfig.handleIgmpOffload = mApfHandleIgmpOffload;
+        // TODO: Turn on MLD offload on devices with 2048 ~ 2999 bytes of APF RAM.
+        apfConfig.handleMldOffload = mApfHandleMldOffload && apfConfig.apfRamSize >= 3000;
+        apfConfig.handleIpv4PingOffload = mApfHandleIpv4PingOffload;
+        // TODO: Turn on Ping6 offload on devices with 2048 ~ 2999 bytes of APF RAM.
+        apfConfig.handleIpv6PingOffload = mApfHandleIpv6PingOffload && apfConfig.apfRamSize >= 3000;
         apfConfig.minMetricsSessionDurationMs = mApfCounterPollingIntervalMs;
         apfConfig.hasClatInterface = mHasSeenClatInterface;
         return mDependencies.maybeCreateApfFilter(getHandler(), mContext, apfConfig,
-                mInterfaceParams, mCallback, mNetworkQuirkMetrics, mUseNewApfFilter);
+                mInterfaceParams, mIpClientApfController, mNetworkQuirkMetrics);
     }
 
     private boolean handleUpdateApfCapabilities(@NonNull final ApfCapabilities apfCapabilities) {
@@ -2796,6 +2863,17 @@
         return apfCapabilities != null;
     }
 
+    private void handleProvisioningConfiguration(@NonNull final ProvisioningConfiguration config) {
+        mCurrentBssid = getInitialBssid(config.mLayer2Info, config.mScanResultInfo,
+                ShimUtils.isAtLeastS());
+        mCurrentApfCapabilities = config.mApfCapabilities;
+        mCreatorUid = config.mCreatorUid;
+        if (config.mLayer2Info != null) {
+            mL2Key = config.mLayer2Info.mL2Key;
+            mCluster = config.mLayer2Info.mCluster;
+        }
+    }
+
     class StoppedState extends State {
         @Override
         public void enter() {
@@ -2830,6 +2908,7 @@
 
                 case CMD_START:
                     mConfiguration = (android.net.shared.ProvisioningConfiguration) msg.obj;
+                    handleProvisioningConfiguration(mConfiguration);
                     transitionTo(mIgnoreNudFailureEnabled
                             ? mNudFailureQueryState
                             : mClearingIpAddressesState);
@@ -3328,7 +3407,7 @@
             mHasSeenClatInterface = false;
             mApfFilter = maybeCreateApfFilter(mCurrentApfCapabilities);
             // If Apf supports ND offload, then turn off the vendor ND offload feature.
-            if (mApfFilter != null && mApfFilter.supportNdOffload()) {
+            if (mApfFilter != null && mApfFilter.enableNdOffload()) {
                 mCallback.setNeighborDiscoveryOffload(false);
             }
             // TODO: investigate the effects of any multicast filtering racing/interfering with the
@@ -3407,7 +3486,10 @@
         private ConnectivityPacketTracker createPacketTracker() {
             try {
                 return new ConnectivityPacketTracker(
-                        getHandler(), mInterfaceParams, mConnectivityPacketLog);
+                        getHandler(),
+                        mInterfaceParams,
+                        mConnectivityPacketLog,
+                        true /* attachFilter */);
             } catch (IllegalArgumentException e) {
                 return null;
             }
@@ -3789,14 +3871,23 @@
                     if (handleUpdateApfCapabilities(apfCapabilities)) {
                         mApfFilter = maybeCreateApfFilter(apfCapabilities);
                         // If Apf supports ND offload, then turn off the vendor ND offload feature.
-                        if (mApfFilter != null && mApfFilter.supportNdOffload()) {
+                        if (mApfFilter != null && mApfFilter.enableNdOffload()) {
                             mCallback.setNeighborDiscoveryOffload(false);
                         }
                     }
                     break;
 
                 case CMD_UPDATE_APF_DATA_SNAPSHOT:
-                    mCallback.startReadPacketFilter("polling");
+                    if (mApfFilter != null) {
+                        // We prevents calls to readPacketFilterRam() when  mApfFilter is null.
+                        // This is correct because any data read would be discarded when
+                        // processing the EVENT_READ_PACKET_FILTER_COMPLETE event if no
+                        // ApfFilter exists.
+                        mApfFilter.getApfController().readPacketFilterRam("polling");
+                    }
+                    // Even if mApfFilter is currently null, periodic checks are necessary to
+                    // read APF RAM when an ApfFilter becomes available, as APF capabilities can
+                    // be updated which result in mApfFilter being created.
                     sendMessageDelayed(CMD_UPDATE_APF_DATA_SNAPSHOT, mApfCounterPollingIntervalMs);
                     break;
 
@@ -3928,7 +4019,11 @@
         return coll.stream().map(Object::toString).collect(Collectors.joining(delimiter));
     }
 
-    static <T> T find(Iterable<T> coll, Predicate<T> fn) {
+    /**
+     * Find a specific element which satisfies the predicate in a collection.
+     */
+    @VisibleForTesting
+    public static <T> T find(Iterable<T> coll, Predicate<T> fn) {
         for (T t: coll) {
             if (fn.test(t)) {
                 return t;
diff --git a/src/android/net/ip/IpClientLinkObserver.java b/src/android/net/ip/IpClientLinkObserver.java
index 8f2856f..516ab01 100644
--- a/src/android/net/ip/IpClientLinkObserver.java
+++ b/src/android/net/ip/IpClientLinkObserver.java
@@ -20,6 +20,7 @@
 import static android.system.OsConstants.AF_UNSPEC;
 import static android.system.OsConstants.IFF_LOOPBACK;
 
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
 import static com.android.net.module.util.NetworkStackConstants.INFINITE_LEASE;
 import static com.android.net.module.util.netlink.NetlinkConstants.IFF_LOWER_UP;
@@ -55,12 +56,14 @@
 import com.android.net.module.util.netlink.NetlinkMessage;
 import com.android.net.module.util.netlink.RtNetlinkAddressMessage;
 import com.android.net.module.util.netlink.RtNetlinkLinkMessage;
+import com.android.net.module.util.netlink.RtNetlinkPrefixMessage;
 import com.android.net.module.util.netlink.RtNetlinkRouteMessage;
 import com.android.net.module.util.netlink.StructIfacacheInfo;
 import com.android.net.module.util.netlink.StructIfaddrMsg;
 import com.android.net.module.util.netlink.StructIfinfoMsg;
 import com.android.net.module.util.netlink.StructNdOptPref64;
 import com.android.net.module.util.netlink.StructNdOptRdnss;
+import com.android.net.module.util.netlink.StructPrefixMsg;
 import com.android.networkstack.apishim.NetworkInformationShimImpl;
 import com.android.networkstack.apishim.common.NetworkInformationShim;
 
@@ -131,16 +134,41 @@
          *            False: clat interface was removed.
          */
         void onClatInterfaceStateUpdate(boolean add);
+
+        /**
+         * Called when the prefix information was updated via RTM_NEWPREFIX netlink message.
+         *
+         * @param info prefix information.
+         */
+        void onNewPrefix(PrefixInfo info);
     }
 
     /** Configuration parameters for IpClientLinkObserver. */
     public static class Configuration {
         public final int minRdnssLifetime;
         public final boolean populateLinkAddressLifetime;
+        public final boolean isDhcp6PdPreferredFlagEnabled;
 
-        public Configuration(int minRdnssLifetime, boolean populateLinkAddressLifetime) {
+        public Configuration(int minRdnssLifetime, boolean populateLinkAddressLifetime,
+                boolean isDhcp6PdPreferredFlagEnabled) {
             this.minRdnssLifetime = minRdnssLifetime;
             this.populateLinkAddressLifetime = populateLinkAddressLifetime;
+            this.isDhcp6PdPreferredFlagEnabled = isDhcp6PdPreferredFlagEnabled;
+        }
+    }
+
+    /** Prefix information received from RTM_NEWPREFIX netlink message. */
+    public static class PrefixInfo {
+        public final IpPrefix prefix;
+        public short flags;
+        public long preferred;
+        public long valid;
+
+        public PrefixInfo(@NonNull final IpPrefix prefix, short flags, long preferred, long valid) {
+            this.prefix = prefix;
+            this.flags = flags;
+            this.preferred = preferred;
+            this.valid = valid;
         }
     }
 
@@ -200,6 +228,7 @@
         mDependencies = deps;
         mNetlinkMonitor = deps.makeIpClientNetlinkMonitor(h, log, mTag,
                 getSocketReceiveBufferSize(),
+                config.isDhcp6PdPreferredFlagEnabled,
                 (nlMsg, whenMs) -> processNetlinkMessage(nlMsg, whenMs));
         mShim = NetworkInformationShimImpl.newInstance();
         mExpirePref64Alarm = new IpClientObserverAlarmListener();
@@ -381,15 +410,19 @@
 
         private final Handler mHandler;
         private final INetlinkMessageProcessor mNetlinkMessageProcessor;
+        private static final int NETLINK_MONITOR_BIND_GROUPS =
+                NetlinkConstants.RTMGRP_ND_USEROPT
+                        | NetlinkConstants.RTMGRP_LINK
+                        | NetlinkConstants.RTMGRP_IPV4_IFADDR
+                        | NetlinkConstants.RTMGRP_IPV6_IFADDR
+                        | NetlinkConstants.RTMGRP_IPV6_ROUTE;
 
         IpClientNetlinkMonitor(Handler h, SharedLog log, String tag, int sockRcvbufSize,
-                INetlinkMessageProcessor p) {
+                boolean isDhcp6PdPreferredFlagEnabled, INetlinkMessageProcessor p) {
             super(h, log, tag, OsConstants.NETLINK_ROUTE,
-                    (NetlinkConstants.RTMGRP_ND_USEROPT
-                            | NetlinkConstants.RTMGRP_LINK
-                            | NetlinkConstants.RTMGRP_IPV4_IFADDR
-                            | NetlinkConstants.RTMGRP_IPV6_IFADDR
-                            | NetlinkConstants.RTMGRP_IPV6_ROUTE),
+                    isDhcp6PdPreferredFlagEnabled
+                            ? NETLINK_MONITOR_BIND_GROUPS | NetlinkConstants.RTMGRP_IPV6_PREFIX
+                            : NETLINK_MONITOR_BIND_GROUPS,
                     sockRcvbufSize);
             mHandler = h;
             mNetlinkMessageProcessor = p;
@@ -630,6 +663,18 @@
         }
     }
 
+    private void processRtNetlinkPrefixMessage(RtNetlinkPrefixMessage msg) {
+        final StructPrefixMsg prefixmsg = msg.getPrefixMsg();
+        if (prefixmsg.prefix_family != AF_INET6) return;
+        if (prefixmsg.prefix_ifindex != mIfindex) return;
+        if (prefixmsg.prefix_type != ICMPV6_ND_OPTION_PIO) return;
+        final PrefixInfo info = new PrefixInfo(msg.getPrefix(),
+                prefixmsg.prefix_flags,
+                msg.getPreferredLifetime(),
+                msg.getValidLifetime());
+        mCallback.onNewPrefix(info);
+    }
+
     private void processNetlinkMessage(NetlinkMessage nlMsg, long whenMs) {
         if (nlMsg instanceof NduseroptMessage) {
             processNduseroptMessage((NduseroptMessage) nlMsg, whenMs);
@@ -639,6 +684,8 @@
             processRtNetlinkAddressMessage((RtNetlinkAddressMessage) nlMsg);
         } else if (nlMsg instanceof RtNetlinkRouteMessage) {
             processRtNetlinkRouteMessage((RtNetlinkRouteMessage) nlMsg);
+        } else if (nlMsg instanceof RtNetlinkPrefixMessage) {
+            processRtNetlinkPrefixMessage((RtNetlinkPrefixMessage) nlMsg);
         } else {
             Log.e(mTag, "Unknown netlink message: " + nlMsg);
         }
diff --git a/src/android/net/ip/IpReachabilityMonitor.java b/src/android/net/ip/IpReachabilityMonitor.java
index 462de90..7676f76 100644
--- a/src/android/net/ip/IpReachabilityMonitor.java
+++ b/src/android/net/ip/IpReachabilityMonitor.java
@@ -21,9 +21,6 @@
 import static android.net.metrics.IpReachabilityEvent.PROVISIONING_LOST;
 import static android.net.metrics.IpReachabilityEvent.PROVISIONING_LOST_ORGANIC;
 
-import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION;
-import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION;
-import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_ROUTER_MAC_CHANGE_FAILURE_ONLY_AFTER_ROAM_VERSION;
@@ -243,11 +240,8 @@
     @NonNull
     private final Callback mCallback;
     private final boolean mMulticastResolicitEnabled;
-    private final boolean mIgnoreIncompleteIpv6DnsServerEnabled;
-    private final boolean mIgnoreIncompleteIpv6DefaultRouterEnabled;
     private final boolean mMacChangeFailureOnlyAfterRoam;
     private final boolean mIgnoreOrganicNudFailure;
-    private final boolean mIgnoreNeverReachableNeighbor;
     // A set to track whether a neighbor has ever entered NUD_REACHABLE state before.
     private final Set<InetAddress> mEverReachableNeighbors = new ArraySet<>();
 
@@ -273,16 +267,10 @@
         mDependencies = dependencies;
         mMulticastResolicitEnabled = dependencies.isFeatureNotChickenedOut(context,
                 IP_REACHABILITY_MCAST_RESOLICIT_VERSION);
-        mIgnoreIncompleteIpv6DnsServerEnabled = dependencies.isFeatureNotChickenedOut(context,
-                IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION);
-        mIgnoreIncompleteIpv6DefaultRouterEnabled = dependencies.isFeatureEnabled(context,
-                IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION);
         mMacChangeFailureOnlyAfterRoam = dependencies.isFeatureNotChickenedOut(context,
                 IP_REACHABILITY_ROUTER_MAC_CHANGE_FAILURE_ONLY_AFTER_ROAM_VERSION);
         mIgnoreOrganicNudFailure = dependencies.isFeatureEnabled(context,
                 IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION);
-        mIgnoreNeverReachableNeighbor = dependencies.isFeatureEnabled(context,
-                IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION);
         mMetricsLog = metricsLog;
         mNetd = netd;
         Preconditions.checkNotNull(mNetd);
@@ -473,19 +461,6 @@
         maybeRestoreNeighborParameters();
     }
 
-    private boolean shouldIgnoreIncompleteNeighbor(@Nullable final NeighborEvent prev,
-            @NonNull final NeighborEvent event) {
-        // mIgnoreNeverReachableNeighbor already takes care of incomplete IPv6 neighbors, so do not
-        // apply this logic.
-        if (mIgnoreNeverReachableNeighbor) return false;
-
-        // For on-link IPv4/v6 DNS server or default router that never ever responds to
-        // address resolution(e.g. ARP or NS), kernel will send RTM_NEWNEIGH with NUD_FAILED
-        // to user space directly, and there is no netlink neighbor events related to this
-        // neighbor received before.
-        return (prev == null && event.nudState == StructNdMsg.NUD_FAILED);
-    }
-
     private void handleNeighborLost(@Nullable final NeighborEvent prev,
             @NonNull final NeighborEvent event) {
         final LinkProperties whatIfLp = new LinkProperties(mLinkProperties);
@@ -508,7 +483,7 @@
             // Pretend neighbors that have never been reachable are still there. Leaving them
             // inside whatIfLp has the benefit that the logic that compares provisioning loss
             // below works consistently independent of the current event being processed.
-            if (mIgnoreNeverReachableNeighbor && !mEverReachableNeighbors.contains(ip)) continue;
+            if (!mEverReachableNeighbors.contains(ip)) continue;
 
             for (RouteInfo route : mLinkProperties.getRoutes()) {
                 if (ip.equals(route.getGateway())) {
@@ -522,42 +497,10 @@
             }
         }
 
-        // TODO: cleanup below code(checking if the incomplete IPv6 neighbor should be ignored)
-        // once the feature of ignoring the neighbor was never ever reachable rolls out.
-        final boolean ignoreIncompleteIpv6DnsServer =
-                mIgnoreIncompleteIpv6DnsServerEnabled
-                        && isNeighborDnsServer(event)
-                        && shouldIgnoreIncompleteNeighbor(prev, event);
-
-        // Generally Router Advertisement should take SLLA option, then device won't do address
-        // resolution for default router's IPv6 link-local address automatically. But sometimes
-        // it may miss SLLA option, also add a flag to check these cases.
-        final boolean ignoreIncompleteIpv6DefaultRouter =
-                mIgnoreIncompleteIpv6DefaultRouterEnabled
-                        && isNeighborDefaultRouter(event)
-                        && shouldIgnoreIncompleteNeighbor(prev, event);
-
-        // Only ignore the incomplete IPv6 neighbor iff IPv4 is still provisioned. For IPv6-only
-        // networks, we MUST not ignore any incomplete IPv6 neighbor.
-        final boolean ignoreIncompleteIpv6Neighbor =
-                (ignoreIncompleteIpv6DnsServer || ignoreIncompleteIpv6DefaultRouter)
-                        && whatIfLp.isIpv4Provisioned();
-
-        // It's better to remove the incompleted on-link IPv6 DNS server or default router from
-        // watch list, otherwise, when wifi invokes probeAll later (e.g. post roam) to send probe
-        // to an incompleted on-link DNS server or default router, it should fail to send netlink
-        // message to kernel as there is no neighbor cache entry for it at all.
-        if (ignoreIncompleteIpv6Neighbor) {
-            Log.d(TAG, "remove incomplete IPv6 neighbor " + event.ip
-                    + " which fails to respond to address resolution from watch list.");
-            mNeighborWatchList.remove(event.ip);
-        }
-
         final boolean lostIpv4Provisioning =
                 mLinkProperties.isIpv4Provisioned() && !whatIfLp.isIpv4Provisioned();
         final boolean lostIpv6Provisioning =
-                mLinkProperties.isIpv6Provisioned() && !whatIfLp.isIpv6Provisioned()
-                        && !ignoreIncompleteIpv6Neighbor;
+                mLinkProperties.isIpv6Provisioned() && !whatIfLp.isIpv6Provisioned();
         final boolean lostProvisioning = lostIpv4Provisioning || lostIpv6Provisioning;
         final NudEventType type = getNudFailureEventType(isFromProbe(),
                 isNudFailureDueToRoam(), lostProvisioning);
@@ -593,7 +536,7 @@
             // Skip the neighbor which is never ever reachable, we ignore the NUD failure for it,
             // pretend neighbor that has never been reachable is still there no matter of neighbor
             // event state.
-            if (mIgnoreNeverReachableNeighbor && !mEverReachableNeighbors.contains(ip)) continue;
+            if (!mEverReachableNeighbors.contains(ip)) continue;
 
             // If an entry is null, consider that probing for that neighbour has completed.
             if (val == null || val.nudState != StructNdMsg.NUD_REACHABLE) return;
diff --git a/src/android/net/ip/MulticastReportMonitor.java b/src/android/net/ip/MulticastReportMonitor.java
new file mode 100644
index 0000000..537d563
--- /dev/null
+++ b/src/android/net/ip/MulticastReportMonitor.java
@@ -0,0 +1,101 @@
+/*
+ * 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.ip;
+
+import android.os.Handler;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.InterfaceParams;
+import com.android.net.module.util.PacketReader;
+
+import java.io.FileDescriptor;
+
+/**
+ * Monitor IGMP/MLD report packets and notify listeners the multicast address changes.
+ *
+ * <p>This class uses a {@link PacketReader} to listen for IGMP/MLD report packets on a given
+ * interface. When a packet is received, it notifies the provided {@link Callback} of the change
+ * in the multicast address.
+ *
+ * <p>To use this class, create a new instance with the desired {@link Handler},
+ * {@link InterfaceParams}, {@link Callback}, and {@link FileDescriptor}. Then, call {@link #start()}
+ * to start listening for packets. To stop listening, call {@link #stop()}.
+ */
+public class MulticastReportMonitor {
+    public interface Callback {
+        /**
+         * Notifies the system or other components about a change in the multicast address.
+         */
+        void notifyMulticastAddrChange();
+    }
+
+    private static final String TAG = MulticastReportMonitor.class.getSimpleName();
+    private final PacketReader mPacketListener;
+
+    /**
+     * Creates a new {@link MulticastReportMonitor}.
+     *
+     * @param handler The {@link Handler} to use for the {@link PacketReader}.
+     * @param ifParams The {@link InterfaceParams} for the interface to listen on.
+     * @param callback The {@link Callback} to notify the multicast address changes.
+     * @param fd The {@link FileDescriptor} to use for the {@link PacketReader}.
+     */
+    public MulticastReportMonitor(
+            @NonNull Handler handler,
+            @NonNull InterfaceParams ifParams,
+            @NonNull Callback callback,
+            @NonNull FileDescriptor fd) {
+        mPacketListener = new PacketListener(handler, ifParams, callback, fd);
+    }
+
+    /**
+     * Starts the packet listener.
+     */
+    public void start() {
+        mPacketListener.start();
+    }
+
+    /**
+     * Stops the packet listener.
+     */
+    public void stop() {
+        mPacketListener.stop();
+    }
+
+    private static final class PacketListener extends PacketReader {
+        private final Callback mCallback;
+        private final FileDescriptor mFd;
+
+        PacketListener(Handler h, InterfaceParams ifParams, Callback callback, FileDescriptor fd) {
+            super(h, ifParams.defaultMtu);
+            mCallback = callback;
+            mFd = fd;
+        }
+
+        @Override
+        protected FileDescriptor createFd() {
+            return mFd;
+        }
+
+        @Override
+        protected void handlePacket(@NonNull byte[] recvbuf, int length) {
+            mCallback.notifyMulticastAddrChange();
+        }
+    }
+}
diff --git a/src/android/net/util/RawPacketTracker.java b/src/android/net/util/RawPacketTracker.java
index e73834b..b5adcd1 100644
--- a/src/android/net/util/RawPacketTracker.java
+++ b/src/android/net/util/RawPacketTracker.java
@@ -52,8 +52,10 @@
     static class Dependencies {
         public @NonNull ConnectivityPacketTracker createPacketTracker(
                 Handler handler, InterfaceParams ifParams, int maxPktRecords) {
+            // A BPF filter is unnecessary here, as the caller uses this device to send packets
+            // and verify the APF offload reply packets received from the remote device.
             return new ConnectivityPacketTracker(
-                    handler, ifParams, new LocalLog(maxPktRecords));
+                    handler, ifParams, new LocalLog(maxPktRecords), false /* attachFilter */);
         }
 
         public @NonNull HandlerThread createHandlerThread() {
diff --git a/src/com/android/networkstack/NetworkStackNotifier.java b/src/com/android/networkstack/NetworkStackNotifier.java
index acf3c95..909336c 100644
--- a/src/com/android/networkstack/NetworkStackNotifier.java
+++ b/src/com/android/networkstack/NetworkStackNotifier.java
@@ -287,7 +287,8 @@
             @NonNull CharSequence networkIdentifier) {
         return new Notification.Builder(mContext, channelId)
                 .setContentTitle(networkIdentifier)
-                .setSmallIcon(R.drawable.icon_wifi);
+                .setSmallIcon(R.drawable.icon_wifi)
+                .setLocalOnly(true);
     }
 
     /**
diff --git a/src/com/android/networkstack/ipmemorystore/IpMemoryStoreDatabase.java b/src/com/android/networkstack/ipmemorystore/IpMemoryStoreDatabase.java
index b00c03d..6f99fa4 100644
--- a/src/com/android/networkstack/ipmemorystore/IpMemoryStoreDatabase.java
+++ b/src/com/android/networkstack/ipmemorystore/IpMemoryStoreDatabase.java
@@ -471,21 +471,24 @@
         final ContentValues cv = toContentValues(key, attributes, expiry);
         db.beginTransaction();
         try {
-            // Unfortunately SQLite does not have any way to do INSERT OR UPDATE. Options are
-            // to either insert with on conflict ignore then update (like done here), or to
-            // construct a custom SQL INSERT statement with nested select.
-            final long resultId = db.insertWithOnConflict(NetworkAttributesContract.TABLENAME,
-                    null, cv, SQLiteDatabase.CONFLICT_IGNORE);
-            if (resultId < 0) {
-                db.update(NetworkAttributesContract.TABLENAME, cv, SELECT_L2KEY, new String[]{key});
+            try {
+                // Unfortunately SQLite does not have any way to do INSERT OR UPDATE. Options are
+                // to either insert with on conflict ignore then update (like done here), or to
+                // construct a custom SQL INSERT statement with nested select.
+                final long resultId = db.insertWithOnConflict(NetworkAttributesContract.TABLENAME,
+                        null, cv, SQLiteDatabase.CONFLICT_IGNORE);
+                if (resultId < 0) {
+                    db.update(NetworkAttributesContract.TABLENAME,
+                            cv, SELECT_L2KEY, new String[]{key});
+                }
+                db.setTransactionSuccessful();
+                return Status.SUCCESS;
+            } finally {
+                db.endTransaction();
             }
-            db.setTransactionSuccessful();
-            return Status.SUCCESS;
         } catch (SQLiteException e) {
             // No space left on disk or something
             Log.e(TAG, "Could not write to the memory store", e);
-        } finally {
-            db.endTransaction();
         }
         return Status.ERROR_STORAGE;
     }
@@ -548,53 +551,55 @@
         for (int remainingRetries = 3; remainingRetries > 0; --remainingRetries) {
             db.beginTransaction();
             try {
-                db.delete(NetworkAttributesContract.TABLENAME, null, null);
-                db.delete(PrivateDataContract.TABLENAME, null, null);
-                db.delete(NetworkEventsContract.TABLENAME, null, null);
-                try (Cursor cursorNetworkAttributes = db.query(
-                        // table name
-                        NetworkAttributesContract.TABLENAME,
-                        // column name
-                        new String[] { NetworkAttributesContract.COLNAME_L2KEY },
-                        null, // selection
-                        null, // selectionArgs
-                        null, // groupBy
-                        null, // having
-                        null, // orderBy
-                        "1")) { // limit
-                    if (0 != cursorNetworkAttributes.getCount()) continue;
+                try {
+                    db.delete(NetworkAttributesContract.TABLENAME, null, null);
+                    db.delete(PrivateDataContract.TABLENAME, null, null);
+                    db.delete(NetworkEventsContract.TABLENAME, null, null);
+                    try (Cursor cursorNetworkAttributes = db.query(
+                            // table name
+                            NetworkAttributesContract.TABLENAME,
+                            // column name
+                            new String[] { NetworkAttributesContract.COLNAME_L2KEY },
+                            null, // selection
+                            null, // selectionArgs
+                            null, // groupBy
+                            null, // having
+                            null, // orderBy
+                            "1")) { // limit
+                        if (0 != cursorNetworkAttributes.getCount()) continue;
+                    }
+                    try (Cursor cursorPrivateData = db.query(
+                            // table name
+                            PrivateDataContract.TABLENAME,
+                            // column name
+                            new String[] { PrivateDataContract.COLNAME_L2KEY },
+                            null, // selection
+                            null, // selectionArgs
+                            null, // groupBy
+                            null, // having
+                            null, // orderBy
+                            "1")) { // limit
+                        if (0 != cursorPrivateData.getCount()) continue;
+                    }
+                    try (Cursor cursorNetworkEvents = db.query(
+                            // table name
+                            NetworkEventsContract.TABLENAME,
+                            // column name
+                            new String[] { NetworkEventsContract.COLNAME_CLUSTER },
+                            null, // selection
+                            null, // selectionArgs
+                            null, // groupBy
+                            null, // having
+                            null, // orderBy
+                            "1")) { // limit
+                        if (0 != cursorNetworkEvents.getCount()) continue;
+                    }
+                    db.setTransactionSuccessful();
+                } finally {
+                    db.endTransaction();
                 }
-                try (Cursor cursorPrivateData = db.query(
-                        // table name
-                        PrivateDataContract.TABLENAME,
-                        // column name
-                        new String[] { PrivateDataContract.COLNAME_L2KEY },
-                        null, // selection
-                        null, // selectionArgs
-                        null, // groupBy
-                        null, // having
-                        null, // orderBy
-                        "1")) { // limit
-                    if (0 != cursorPrivateData.getCount()) continue;
-                }
-                try (Cursor cursorNetworkEvents = db.query(
-                        // table name
-                        NetworkEventsContract.TABLENAME,
-                        // column name
-                        new String[] { NetworkEventsContract.COLNAME_CLUSTER },
-                        null, // selection
-                        null, // selectionArgs
-                        null, // groupBy
-                        null, // having
-                        null, // orderBy
-                        "1")) { // limit
-                    if (0 != cursorNetworkEvents.getCount()) continue;
-                }
-                db.setTransactionSuccessful();
             } catch (SQLiteException e) {
                 Log.e(TAG, "Could not wipe the data in database", e);
-            } finally {
-                db.endTransaction();
             }
         }
     }
@@ -714,6 +719,10 @@
     /**
      * Delete a single entry by key.
      *
+     * The NetworkAttributes table is indexed by a L2 key although it also has a cluster column,
+     * so this API only targets the NetworkAttributes table. For deleting the entries by cluster,
+     * see {@link deleteCluster}.
+     *
      * If |needWipe| is true, the data will be wiped from disk immediately. Otherwise, it will
      * only be marked deleted, and overwritten by subsequent writes or reclaimed during the next
      * maintenance window.
@@ -724,11 +733,21 @@
     static StatusAndCount delete(@NonNull final SQLiteDatabase db, @NonNull final String l2key,
             final boolean needWipe) {
         return deleteEntriesWithColumn(db,
-                NetworkAttributesContract.COLNAME_L2KEY, l2key, needWipe);
+                NetworkAttributesContract.TABLENAME,     // table
+                NetworkAttributesContract.COLNAME_L2KEY, // column
+                l2key,                                   // value
+                needWipe);
     }
 
     /**
-     * Delete all entries that have a particular cluster value.
+     * Delete all entries that have a particular cluster value in NetworkAttributes and
+     * NetworkEvents tables.
+     *
+     * So far the cluster column exists in both the NetworkAttributes and NetworkEvents
+     * tables, and this API is only called when WiFi attempts to remove a network, see
+     * {@link WifiHealthMonitor.OnNetworkUpdateListener#onNetworkRemoved} and
+     * {@link WifiNetworkSuggestionManager#remove}. It makes more sense to delete the
+     * cluster column from both tables when this API is called.
      *
      * If |needWipe| is true, the data will be wiped from disk immediately. Otherwise, it will
      * only be marked deleted, and overwritten by subsequent writes or reclaimed during the next
@@ -739,26 +758,50 @@
      */
     static StatusAndCount deleteCluster(@NonNull final SQLiteDatabase db,
             @NonNull final String cluster, final boolean needWipe) {
-        return deleteEntriesWithColumn(db,
-                NetworkAttributesContract.COLNAME_CLUSTER, cluster, needWipe);
+        // Delete all entries that have cluster value from NetworkAttributes table.
+        final StatusAndCount naDeleteResult = deleteEntriesWithColumn(db,
+                NetworkAttributesContract.TABLENAME,       // table
+                NetworkAttributesContract.COLNAME_CLUSTER, // column
+                cluster,                                   // value
+                needWipe);
+        // And then delete all entries that have cluster value from NetworkEvents table.
+        final StatusAndCount neDeleteResult = deleteEntriesWithColumn(db,
+                NetworkEventsContract.TABLENAME,           // table
+                NetworkEventsContract.COLNAME_CLUSTER,     // column
+                cluster,                                   // value
+                needWipe);
+        int status = Status.ERROR_GENERIC;
+        if (naDeleteResult.status == Status.SUCCESS && neDeleteResult.status == Status.SUCCESS) {
+            status = Status.SUCCESS;
+        } else if (naDeleteResult.status != Status.SUCCESS) {
+            // If deleteCluster fails on both tables, return the status code on deleting the entries
+            // from the NetworkAttributes table, keep consistent with previous behavior.
+            status = naDeleteResult.status;
+        } else {
+            status = neDeleteResult.status;
+        }
+        return new StatusAndCount(status, naDeleteResult.count + neDeleteResult.count);
     }
 
     // Delete all entries where the given column has the given value.
     private static StatusAndCount deleteEntriesWithColumn(@NonNull final SQLiteDatabase db,
-            @NonNull final String column, @NonNull final String value, final boolean needWipe) {
+            @NonNull final String table, @NonNull final String column, @NonNull final String value,
+            final boolean needWipe) {
         db.beginTransaction();
         int deleted = 0;
         try {
-            deleted = db.delete(NetworkAttributesContract.TABLENAME,
-                    column + "= ?", new String[] { value });
-            db.setTransactionSuccessful();
+            try {
+                deleted = db.delete(table,
+                        column + "= ?", new String[] { value });
+                db.setTransactionSuccessful();
+            } finally {
+                db.endTransaction();
+            }
         } catch (SQLiteException e) {
             Log.e(TAG, "Could not delete from the memory store", e);
             // Unclear what might have happened ; deleting records is not supposed to be able
             // to fail barring a syntax error in the SQL query.
             return new StatusAndCount(Status.ERROR_UNKNOWN, 0);
-        } finally {
-            db.endTransaction();
         }
 
         if (needWipe) {
@@ -774,21 +817,23 @@
     static int dropAllExpiredRecords(@NonNull final SQLiteDatabase db) {
         db.beginTransaction();
         try {
-            final long currentTimestamp = System.currentTimeMillis();
-            // Deletes NetworkAttributes that have expired.
-            db.delete(NetworkAttributesContract.TABLENAME,
-                    NetworkAttributesContract.COLNAME_EXPIRYDATE + " < ?",
-                    new String[]{Long.toString(currentTimestamp)});
-            // Deletes NetworkEvents that have expired.
-            db.delete(NetworkEventsContract.TABLENAME,
-                    NetworkEventsContract.COLNAME_EXPIRY + " < ?",
-                    new String[]{Long.toString(currentTimestamp)});
-            db.setTransactionSuccessful();
+            try {
+                final long currentTimestamp = System.currentTimeMillis();
+                // Deletes NetworkAttributes that have expired.
+                db.delete(NetworkAttributesContract.TABLENAME,
+                        NetworkAttributesContract.COLNAME_EXPIRYDATE + " < ?",
+                        new String[]{Long.toString(currentTimestamp)});
+                // Deletes NetworkEvents that have expired.
+                db.delete(NetworkEventsContract.TABLENAME,
+                        NetworkEventsContract.COLNAME_EXPIRY + " < ?",
+                        new String[]{Long.toString(currentTimestamp)});
+                db.setTransactionSuccessful();
+            } finally {
+                db.endTransaction();
+            }
         } catch (SQLiteException e) {
             Log.e(TAG, "Could not delete data from memory store", e);
             return Status.ERROR_STORAGE;
-        } finally {
-            db.endTransaction();
         }
 
         // Execute vacuuming here if above operation has no exception. If above operation got
@@ -827,16 +872,18 @@
 
         db.beginTransaction();
         try {
-            // Deletes NetworkAttributes that expiryDate are lower than given value.
-            db.delete(NetworkAttributesContract.TABLENAME,
-                    NetworkAttributesContract.COLNAME_EXPIRYDATE + " <= ?",
-                    new String[]{Long.toString(expiryDate)});
-            db.setTransactionSuccessful();
+            try {
+                // Deletes NetworkAttributes that expiryDate are lower than given value.
+                db.delete(NetworkAttributesContract.TABLENAME,
+                        NetworkAttributesContract.COLNAME_EXPIRYDATE + " <= ?",
+                        new String[]{Long.toString(expiryDate)});
+                db.setTransactionSuccessful();
+            } finally {
+                db.endTransaction();
+            }
         } catch (SQLiteException e) {
             Log.e(TAG, "Could not delete data from memory store", e);
             return Status.ERROR_STORAGE;
-        } finally {
-            db.endTransaction();
         }
 
         // Execute vacuuming here if above operation has no exception. If above operation got
@@ -868,20 +915,22 @@
         final ContentValues cv = toContentValues(cluster, timestamp, expiry, eventType);
         db.beginTransaction();
         try {
-            final long resultId = db.insertOrThrow(NetworkEventsContract.TABLENAME,
-                    null /* nullColumnHack */, cv);
-            if (resultId < 0) {
-                // Should not fail to insert a row to NetworkEvents table which doesn't have
-                // uniqueness constraint.
-                return Status.ERROR_STORAGE;
+            try {
+                final long resultId = db.insertOrThrow(NetworkEventsContract.TABLENAME,
+                        null /* nullColumnHack */, cv);
+                if (resultId < 0) {
+                    // Should not fail to insert a row to NetworkEvents table which doesn't have
+                    // uniqueness constraint.
+                    return Status.ERROR_STORAGE;
+                }
+                db.setTransactionSuccessful();
+                return Status.SUCCESS;
+            } finally {
+                db.endTransaction();
             }
-            db.setTransactionSuccessful();
-            return Status.SUCCESS;
         } catch (SQLiteException e) {
             // No space left on disk or something
             Log.e(TAG, "Could not write to the memory store", e);
-        } finally {
-            db.endTransaction();
         }
         return Status.ERROR_STORAGE;
     }
diff --git a/src/com/android/networkstack/metrics/ApfSessionInfoMetrics.java b/src/com/android/networkstack/metrics/ApfSessionInfoMetrics.java
index c2c51f6..14cb6ff 100644
--- a/src/com/android/networkstack/metrics/ApfSessionInfoMetrics.java
+++ b/src/com/android/networkstack/metrics/ApfSessionInfoMetrics.java
@@ -20,83 +20,97 @@
 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_ETHER_OUR_SRC_MAC;
 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_IGMP_INVALID;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IGMP_REPORT;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IGMP_V2_GENERAL_QUERY_REPLIED;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IGMP_V3_GENERAL_QUERY_REPLIED;
 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_ICMP_INVALID;
 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_PING_REQUEST_REPLIED;
 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_ICMP6_ECHO_REQUEST_INVALID;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_ICMP6_ECHO_REQUEST_REPLIED;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MLD_INVALID;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MLD_REPORT;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MLD_V1_GENERAL_QUERY_REPLIED;
+import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MLD_V2_GENERAL_QUERY_REPLIED;
 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_MDNS_REPLIED;
 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_ETHER_OUR_SRC_MAC;
 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_HOPOPTS;
 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.RESERVED_OOB;
 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_ETHER_OUR_SRC_MAC;
 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_IGMP_INVALID;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IGMP_REPORT;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IGMP_V2_GENERAL_QUERY_REPLIED;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IGMP_V3_GENERAL_QUERY_REPLIED;
 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_ICMP_INVALID;
 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_IPV4_PING_REQUEST_REPLIED;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV4_TCP_PORT7_UNICAST;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_ICMP6_ECHO_REQUEST_INVALID;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_ICMP6_ECHO_REQUEST_REPLIED;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_MLD_INVALID;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_MLD_REPORT;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_MLD_V1_GENERAL_QUERY_REPLIED;
+import static android.stats.connectivity.CounterName.CN_DROPPED_IPV6_MLD_V2_GENERAL_QUERY_REPLIED;
 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_MDNS_REPLIED;
 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;
@@ -104,16 +118,12 @@
 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_HOPOPTS;
 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_PASSED_OUR_SRC_MAC;
 import static android.stats.connectivity.CounterName.CN_TOTAL_PACKETS;
 import static android.stats.connectivity.CounterName.CN_UNKNOWN;
 
@@ -137,41 +147,44 @@
     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(RESERVED_OOB, CN_UNKNOWN),
                 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_ETHER_OUR_SRC_MAC, CN_PASSED_OUR_SRC_MAC),
                 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_HOPOPTS, CN_PASSED_IPV6_HOPOPTS),
                 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_ETHER_OUR_SRC_MAC, CN_DROPPED_ETHER_OUR_SRC_MAC),
                 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_ICMP_INVALID, CN_DROPPED_IPV4_ICMP_INVALID),
                 Map.entry(DROPPED_IPV4_MULTICAST, CN_DROPPED_IPV4_MULTICAST),
                 Map.entry(DROPPED_IPV4_NON_DHCP4, CN_DROPPED_IPV4_NON_DHCP4),
+                Map.entry(DROPPED_IPV4_PING_REQUEST_REPLIED, CN_DROPPED_IPV4_PING_REQUEST_REPLIED),
+                Map.entry(DROPPED_IPV6_ICMP6_ECHO_REQUEST_INVALID,
+                    CN_DROPPED_IPV6_ICMP6_ECHO_REQUEST_INVALID),
+                Map.entry(DROPPED_IPV6_ICMP6_ECHO_REQUEST_REPLIED,
+                    CN_DROPPED_IPV6_ICMP6_ECHO_REQUEST_REPLIED),
                 Map.entry(DROPPED_IPV6_ROUTER_SOLICITATION, CN_DROPPED_IPV6_ROUTER_SOLICITATION),
+                Map.entry(DROPPED_IPV6_MLD_INVALID, CN_DROPPED_IPV6_MLD_INVALID),
+                Map.entry(DROPPED_IPV6_MLD_REPORT, CN_DROPPED_IPV6_MLD_REPORT),
+                Map.entry(DROPPED_IPV6_MLD_V1_GENERAL_QUERY_REPLIED,
+                    CN_DROPPED_IPV6_MLD_V1_GENERAL_QUERY_REPLIED),
+                Map.entry(DROPPED_IPV6_MLD_V2_GENERAL_QUERY_REPLIED,
+                    CN_DROPPED_IPV6_MLD_V2_GENERAL_QUERY_REPLIED),
                 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),
@@ -179,18 +192,22 @@
                 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_MDNS_REPLIED, CN_DROPPED_MDNS_REPLIED),
+                Map.entry(DROPPED_IPV4_TCP_PORT7_UNICAST, CN_DROPPED_IPV4_TCP_PORT7_UNICAST),
                 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_IGMP_V2_GENERAL_QUERY_REPLIED,
+                    CN_DROPPED_IGMP_V2_GENERAL_QUERY_REPLIED),
+                Map.entry(DROPPED_IGMP_V3_GENERAL_QUERY_REPLIED,
+                    CN_DROPPED_IGMP_V3_GENERAL_QUERY_REPLIED),
+                Map.entry(DROPPED_IGMP_INVALID, CN_DROPPED_IGMP_INVALID),
+                Map.entry(DROPPED_IGMP_REPORT, CN_DROPPED_IGMP_REPORT),
                 Map.entry(DROPPED_GARP_REPLY, CN_DROPPED_GARP_REPLY)
             )
     );
diff --git a/src/com/android/networkstack/metrics/stats.proto b/src/com/android/networkstack/metrics/stats.proto
index 43bc414..972e1f1 100644
--- a/src/com/android/networkstack/metrics/stats.proto
+++ b/src/com/android/networkstack/metrics/stats.proto
@@ -239,8 +239,7 @@
 /**
  * Logs APF session information event.
  * Logged from:
- * packages/modules/NetworkStack/src/android/net/apf/ApfFilter.java or
- * packages/modules/NetworkStack/src/android/net/apf/LegacyApfFilter.java
+ * packages/modules/NetworkStack/src/android/net/apf/ApfFilter.java
  */
 message ApfSessionInfoReported {
     // The version of APF, where version = -1 equals APF disable.
diff --git a/src/com/android/networkstack/util/NetworkStackUtils.java b/src/com/android/networkstack/util/NetworkStackUtils.java
index fce06e4..b0252dc 100755
--- a/src/com/android/networkstack/util/NetworkStackUtils.java
+++ b/src/com/android/networkstack/util/NetworkStackUtils.java
@@ -16,13 +16,21 @@
 
 package com.android.networkstack.util;
 
+import static android.net.apf.ApfConstants.IPV6_SOLICITED_NODES_PREFIX;
+import static android.os.Build.VERSION.CODENAME;
+import static android.os.Build.VERSION.SDK_INT;
+import static android.system.OsConstants.IFA_F_DEPRECATED;
+import static android.system.OsConstants.IFA_F_TENTATIVE;
+
 import android.content.Context;
 import android.net.IpPrefix;
 import android.net.LinkAddress;
+import android.net.LinkProperties;
 import android.net.MacAddress;
 import android.system.ErrnoException;
 import android.util.Log;
 
+import androidx.annotation.ChecksSdkIntAtLeast;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
@@ -64,7 +72,7 @@
     public static final String CAPTIVE_PORTAL_OTHER_HTTP_URLS = "captive_portal_other_http_urls";
 
     /**
-     * A comma separated list of URLs used for network validation. in addition to the HTTPS url
+     * A comma separated list of URLs used for network validation in addition to the HTTPS url
      * associated with the CAPTIVE_PORTAL_HTTPS_URL settings.
      */
     public static final String CAPTIVE_PORTAL_OTHER_HTTPS_URLS = "captive_portal_other_https_urls";
@@ -186,13 +194,6 @@
     public static final String VALIDATION_METRICS_VERSION = "validation_metrics_version";
 
     /**
-     * Experiment flag to enable sending Gratuitous APR and Gratuitous Neighbor Advertisement for
-     * all assigned IPv4 and IPv6 GUAs after completing L2 roaming.
-     */
-    public static final String IPCLIENT_GARP_NA_ROAMING_VERSION =
-            "ipclient_garp_na_roaming_version";
-
-    /**
      * Experiment flag to enable "mcast_resolicit" neighbor parameter in IpReachabilityMonitor,
      * set it to 3 by default.
      */
@@ -200,20 +201,6 @@
             "ip_reachability_mcast_resolicit_version";
 
     /**
-     * Experiment flag to attempt to ignore the on-link IPv6 DNS server which fails to respond to
-     * address resolution.
-     */
-    public static final String IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION =
-            "ip_reachability_ignore_incompleted_ipv6_dns_server_version";
-
-    /**
-     * Experiment flag to attempt to ignore the IPv6 default router which fails to respond to
-     * address resolution.
-     */
-    public static final String IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION =
-            "ip_reachability_ignore_incompleted_ipv6_default_router_version";
-
-    /**
      * Experiment flag to treat router MAC address changes as a failure only on roam.
      */
     public static final String IP_REACHABILITY_ROUTER_MAC_CHANGE_FAILURE_ONLY_AFTER_ROAM_VERSION =
@@ -226,24 +213,6 @@
             "ip_reachability_ignore_organic_nud_failure_version";
 
     /**
-     * Experiment flag to ignore all NUD failures from the neighbor that has never ever entered the
-     * reachable state.
-     */
-    public static final String IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION =
-            "ip_reachability_ignore_never_reachable_neighbor_version";
-
-    /**
-     * Experiment flag to enable DHCPv6 Prefix Delegation(RFC8415) in IpClient.
-     */
-    public static final String IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION =
-            "ipclient_dhcpv6_prefix_delegation_version";
-
-    /**
-     * Experiment flag to enable new ra filter.
-     */
-    public static final String APF_NEW_RA_FILTER_VERSION = "apf_new_ra_filter_version";
-
-    /**
      * Experiment flag to enable the feature of polling counters in Apf.
      */
     public static final String APF_POLLING_COUNTERS_VERSION = "apf_polling_counters_version";
@@ -275,6 +244,12 @@
             "ipclient_dhcpv6_pd_preferred_flag_version";
 
     /**
+     * Experiment flag to replace INetd usage with netlink in IpClient.
+     */
+    public static final String IPCLIENT_REPLACE_NETD_WITH_NETLINK_VERSION =
+            "ipclient_replace_netd_with_netlink_version";
+
+    /**
      * Experiment flag to enable Discovery of Designated Resolvers (DDR).
      * This flag requires networkmonitor_async_privdns_resolution flag.
      */
@@ -286,6 +261,30 @@
     public static final String IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION =
             "ip_reachability_ignore_nud_failure_version";
 
+    /**
+     * Experiment flag to enable the feature of handle IPv4 ping offload in Apf.
+     */
+    public static final String APF_HANDLE_PING4_OFFLOAD_VERSION =
+            "apf_handle_ping_offload_version";
+
+    /**
+     * Experiment flag to enable the feature of handle IPv6 ping offload in Apf.
+     */
+    public static final String APF_HANDLE_PING6_OFFLOAD_VERSION =
+            "apf_handle_ping6_offload_version";
+
+    /**
+     * Experiment flag to enable the feature of handle IGMP offload in Apf.
+     */
+    public static final String APF_HANDLE_IGMP_OFFLOAD_VERSION =
+            "apf_handle_igmp_offload_version";
+
+    /**
+     * Experiment flag to enable the feature of handle MLD offload in Apf.
+     */
+    public static final String APF_HANDLE_MLD_OFFLOAD_VERSION =
+            "apf_handle_mld_offload_version";
+
     /**** BEGIN Feature Kill Switch Flags ****/
 
     /**
@@ -306,6 +305,9 @@
     public static final String IGNORE_TCP_INFO_FOR_BLOCKED_UIDS =
             "ignore_tcp_info_for_blocked_uids";
 
+    /** Kill switch to force disable APF */
+    public static final String APF_ENABLE = "apf_enable";
+
     /**
      * 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
@@ -318,11 +320,45 @@
      */
     public static final String APF_HANDLE_ND_OFFLOAD = "apf_handle_nd_offload";
 
+    /**
+     * Kill switch flag to disable the feature of handle IGMP offload in Apf.
+     */
+    public static final String APF_HANDLE_IGMP_OFFLOAD = "apf_handle_igmp_offload";
+
+    /**
+     * Kill switch flag to disable the feature of handle MLD offload in Apf.
+     */
+    public static final String APF_HANDLE_MLD_OFFLOAD = "apf_handle_mld_offload";
+
+    /**
+     * Kill switch flag to disable the feature of handle IPv4 ping offload in Apf.
+     */
+    public static final String APF_HANDLE_PING4_OFFLOAD = "apf_handle_ping4_offload";
+
+    /**
+     * Kill switch flag to disable the feature of handle IPv6 ping offload in Apf.
+     */
+    public static final String APF_HANDLE_PING6_OFFLOAD = "apf_handle_ping6_offload";
     static {
         System.loadLibrary("networkstackutilsjni");
     }
 
     /**
+     * Convert IPv4 multicast address to ethernet multicast address in network order.
+     */
+    public static MacAddress ipv4MulticastToEthernetMulticast(@NonNull final Inet4Address addr) {
+        final byte[] etherMulticast = new byte[6];
+        final byte[] ipv4Multicast = addr.getAddress();
+        etherMulticast[0] = (byte) 0x01;
+        etherMulticast[1] = (byte) 0x00;
+        etherMulticast[2] = (byte) 0x5e;
+        etherMulticast[3] = (byte) (ipv4Multicast[1] & 0x7f);
+        etherMulticast[4] = ipv4Multicast[2];
+        etherMulticast[5] = ipv4Multicast[3];
+        return MacAddress.fromBytes(etherMulticast);
+    }
+
+    /**
      * Convert IPv6 multicast address to ethernet multicast address in network order.
      */
     public static MacAddress ipv6MulticastToEthernetMulticast(@NonNull final Inet6Address addr) {
@@ -361,6 +397,24 @@
     }
 
     /**
+     * Checks if the given IPv6 address is a solicited-node multicast address.
+     *
+     * <p>Solicited-node multicast addresses are used for Neighbor Discovery in IPv6.
+     * They have a specific prefix (FF02::1:FFxx:xxxx) where the last 64 bits are derived
+     * from the interface's link-layer address. This function only checks if the address
+     * has the correct prefix; it does *not* verify the lower 64 bits.
+     */
+    public static boolean isIPv6AddressSolicitedNodeMulticast(@NonNull final Inet6Address addr) {
+        for (int i = 0; i < IPV6_SOLICITED_NODES_PREFIX.length; i++) {
+            if (addr.getAddress()[i] != IPV6_SOLICITED_NODES_PREFIX[i]) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
      * Check whether a link address is IPv6 global preferred unicast address.
      */
     public static boolean isIPv6GUA(@NonNull final LinkAddress address) {
@@ -409,6 +463,58 @@
         }
     }
 
+    /** Checks if the device is running on a release version of Android Baklava or newer */
+    @ChecksSdkIntAtLeast(api = 36 /* BUILD_VERSION_CODES.Baklava */)
+    public static boolean isAtLeast25Q2() {
+        return SDK_INT >= 36 || (SDK_INT == 35 && isAtLeastPreReleaseCodename("Baklava"));
+    }
+
+    private static boolean isAtLeastPreReleaseCodename(@NonNull String codename) {
+        // Special case "REL", which means the build is not a pre-release build.
+        if ("REL".equals(CODENAME)) {
+            return false;
+        }
+
+        // Otherwise lexically compare them. Return true if the build codename is equal to or
+        // greater than the requested codename.
+        return CODENAME.compareTo(codename) >= 0;
+    }
+
+    /**
+     * Select the preferred IPv6 link-local address based on the rules defined in rfc3484,
+     * Section 5.
+     * <p>
+     * The address selection criteria are as follows:
+     * 1. Select a non-tentative, non-deprecated address, if available.
+     * 2. If no such address exists, select any non-tentative address.
+     */
+    public static Inet6Address selectPreferredIPv6LinkLocalAddress(@NonNull LinkProperties lp) {
+        Inet6Address preferredAddress = null;
+        for (LinkAddress linkAddress : lp.getLinkAddresses()) {
+            final InetAddress inetAddress = linkAddress.getAddress();
+            final int flags = linkAddress.getFlags();
+
+            if (!(inetAddress instanceof Inet6Address)) {
+                continue;
+            }
+
+            if (!inetAddress.isLinkLocalAddress()) {
+                continue;
+            }
+
+            if ((flags & IFA_F_TENTATIVE) != 0) {
+                continue;
+            }
+
+            preferredAddress = (Inet6Address) inetAddress;
+            if ((flags & IFA_F_DEPRECATED) == 0L) {
+                return preferredAddress;
+            }
+        }
+
+        return preferredAddress;
+    }
+
     /**
      * Attaches a socket filter that accepts DHCP packets to the given socket.
      */
@@ -437,6 +543,27 @@
         addArpEntry(ethAddr.toByteArray(), ipv4Addr.getAddress(), ifname, fd);
     }
 
+    /**
+     * Attaches a socket filter that accepts egress IGMPv2/IGMPv3 reports to the given socket.
+     *
+     * This filter doesn't include IGMPv1 report since device will not send out IGMPv1 report
+     * when the device leaves a multicast address group.
+     *
+     * @param fd the socket's {@link FileDescriptor}.
+     */
+    public static native void attachEgressIgmpReportFilter(FileDescriptor fd) throws ErrnoException;
+
+    /**
+     * Attaches a socket filter that accepts egress IGMPv2/v3, MLDv1/v2 reports to the given socket.
+     *
+     * This filter doesn't include IGMPv1 report since device will not send out IGMPv1 report
+     * when the device leaves a multicast address group.
+     *
+     * @param fd the socket's {@link FileDescriptor}.
+     */
+    public static native void attachEgressMulticastReportFilter(
+            FileDescriptor fd) throws ErrnoException;
+
     private static native void addArpEntry(byte[] ethAddr, byte[] netAddr, String ifname,
             FileDescriptor fd) throws IOException;
 
diff --git a/src/com/android/server/NetworkStackService.java b/src/com/android/server/NetworkStackService.java
index 686a399..68d5a21 100644
--- a/src/com/android/server/NetworkStackService.java
+++ b/src/com/android/server/NetworkStackService.java
@@ -107,9 +107,6 @@
 
     /**
      * Create a binder connector for the system server to communicate with the network stack.
-     *
-     * <p>On platforms where the network stack runs in the system server process, this method may
-     * be called directly instead of obtaining the connector by binding to the service.
      */
     public static synchronized IBinder makeConnector(Context context) {
         if (sConnector == null) {
@@ -364,8 +361,8 @@
         }
 
         @Override
-        public void makeNetworkMonitor(Network network, String name, INetworkMonitorCallbacks cb)
-                throws RemoteException {
+        public void makeNetworkMonitor(Network network, @Nullable String name,
+                INetworkMonitorCallbacks cb) throws RemoteException {
             mPermChecker.enforceNetworkStackCallingPermission();
             updateNetworkStackAidlVersion(cb.getInterfaceVersion(), cb.getInterfaceHash());
             final SharedLog log = addValidationLogs(network, name);
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java
index 197ed69..3ae8557 100755
--- a/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -883,7 +883,8 @@
             } else {
                 mCallback.notifyNetworkTestedWithExtras(result);
             }
-        } catch (RemoteException e) {
+        } catch (RemoteException | RuntimeException e) {
+            // TODO: stop catching RuntimeException once all mainline devices use the tethering APEX
             Log.e(TAG, "Error sending network test result", e);
         }
     }
@@ -914,7 +915,8 @@
     private void notifyProbeStatusChanged(int probesCompleted, int probesSucceeded) {
         try {
             mCallback.notifyProbeStatusChanged(probesCompleted, probesSucceeded);
-        } catch (RemoteException e) {
+        } catch (RemoteException | RuntimeException e) {
+            // TODO: stop catching RuntimeException once all mainline devices use the tethering APEX
             Log.e(TAG, "Error sending probe status", e);
         }
     }
@@ -922,7 +924,8 @@
     private void showProvisioningNotification(String action) {
         try {
             mCallback.showProvisioningNotification(action, mContext.getPackageName());
-        } catch (RemoteException e) {
+        } catch (RemoteException | RuntimeException e) {
+            // TODO: stop catching RuntimeException once all mainline devices use the tethering APEX
             Log.e(TAG, "Error showing provisioning notification", e);
         }
     }
@@ -930,7 +933,8 @@
     private void hideProvisioningNotification() {
         try {
             mCallback.hideProvisioningNotification();
-        } catch (RemoteException e) {
+        } catch (RemoteException | RuntimeException e) {
+            // TODO: stop catching RuntimeException once all mainline devices use the tethering APEX
             Log.e(TAG, "Error hiding provisioning notification", e);
         }
     }
@@ -938,7 +942,8 @@
     private void notifyDataStallSuspected(@NonNull DataStallReportParcelable p) {
         try {
             mCallback.notifyDataStallSuspected(p);
-        } catch (RemoteException e) {
+        } catch (RemoteException | RuntimeException e) {
+            // TODO: stop catching RuntimeException once all mainline devices use the tethering APEX
             Log.e(TAG, "Error sending notification for suspected data stall", e);
         }
     }
@@ -2089,7 +2094,8 @@
     private void notifyPrivateDnsConfigResolved(@NonNull PrivateDnsConfig config) {
         try {
             mCallback.notifyPrivateDnsConfigResolved(config.toParcel());
-        } catch (RemoteException e) {
+        } catch (RemoteException | RuntimeException e) {
+            // TODO: stop catching RuntimeException once all mainline devices use the tethering APEX
             Log.e(TAG, "Error sending private DNS config resolved notification", e);
         }
     }
@@ -4154,7 +4160,8 @@
         if (data == null) return;
         try {
             data.notifyChanged(mCallback);
-        } catch (RemoteException e) {
+        } catch (RemoteException | RuntimeException e) {
+            // TODO: stop catching RuntimeException once all mainline devices use the tethering APEX
             Log.e(TAG, "Error notifying ConnectivityService of new capport data", e);
         }
     }
diff --git a/tests/integration/AndroidTest_Coverage.xml b/tests/integration/AndroidTest_Coverage.xml
index 5e5fbfb..eb0ba16 100644
--- a/tests/integration/AndroidTest_Coverage.xml
+++ b/tests/integration/AndroidTest_Coverage.xml
@@ -32,5 +32,7 @@
         <option name="test-filter-dir" value="/data/data/{PACKAGE}/cache" />
         <option name="hidden-api-checks" value="false"/>
         <option name="device-listeners" value="com.android.modules.utils.testing.NativeCoverageHackInstrumentationListener" />
+        <!-- b/393391701 -->
+        <option name="exclude-filter" value="com.android.net.module.util.RealtimeSchedulerTest" />
     </test>
 </configuration>
diff --git a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
index ee23c99..7ec958a 100644
--- a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
+++ b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
@@ -45,6 +45,7 @@
 import static android.net.ip.IpClient.DEFAULT_APF_COUNTER_POLLING_INTERVAL_SECS;
 import static android.net.ip.IpClient.DEFAULT_NUD_FAILURE_COUNT_DAILY_THRESHOLD;
 import static android.net.ip.IpClient.DEFAULT_NUD_FAILURE_COUNT_WEEKLY_THRESHOLD;
+import static android.net.ip.IpClient.NETWORK_EVENT_NUD_FAILURE_TYPES;
 import static android.net.ip.IpClient.ONE_DAY_IN_MS;
 import static android.net.ip.IpClient.ONE_WEEK_IN_MS;
 import static android.net.ip.IpClient.SIX_HOURS_IN_MS;
@@ -85,9 +86,6 @@
 import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_AUTONOMOUS;
 import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_ON_LINK;
 import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION;
-import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION;
-import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION;
-import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_ROUTER_MAC_CHANGE_FAILURE_ONLY_AFTER_ROAM_VERSION;
@@ -116,7 +114,6 @@
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
@@ -126,7 +123,6 @@
 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;
 
 import android.app.AlarmManager;
@@ -176,6 +172,7 @@
 import android.net.ipmemorystore.NetworkAttributes;
 import android.net.ipmemorystore.OnNetworkAttributesRetrievedListener;
 import android.net.ipmemorystore.OnNetworkEventCountRetrievedListener;
+import android.net.ipmemorystore.OnStatusListener;
 import android.net.ipmemorystore.Status;
 import android.net.networkstack.TestNetworkStackServiceClient;
 import android.net.networkstack.aidl.dhcp.DhcpOption;
@@ -297,7 +294,7 @@
     private static final String TAG = IpClientIntegrationTestCommon.class.getSimpleName();
     private static final int DATA_BUFFER_LEN = 4096;
     private static final int PACKET_TIMEOUT_MS = 5_000;
-    private static final String TEST_CLUSTER = "some cluster";
+    protected static final String TEST_CLUSTER = "some cluster";
     private static final int TEST_LEASE_DURATION_S = 3_600; // 1 hour
     private static final int TEST_IPV6_ONLY_WAIT_S = 1_800; // 30 min
     private static final int TEST_LOWER_IPV6_ONLY_WAIT_S = (int) (MIN_V6ONLY_WAIT_MS / 1000 - 1);
@@ -321,6 +318,9 @@
     // should be enough between the timestamp when the IP provisioning completes successfully and
     // when IpClientLinkObserver sees the RTM_NEWADDR netlink events.
     private static final long TEST_LIFETIME_TOLERANCE_MS = 4_000L;
+    private static final long TEST_POLL_NEIGHBOR_PARAMETER_MS = 500L;
+    private static final int TEST_ARP_LOCKTIME_MS = 1500;
+    private static final int TEST_DELAY_FIRST_PROBE_TIME_S = 2;
 
     @Rule
     public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
@@ -426,7 +426,6 @@
     private static final int DHCP_HEADER_OFFSET = ETH_HEADER_LEN + IPV4_HEADER_LEN
             + UDP_HEADER_LEN;
     private static final int DHCP_MESSAGE_OP_CODE_OFFSET = DHCP_HEADER_OFFSET + 0;
-    private static final int DHCP_TRANSACTION_ID_OFFSET = DHCP_HEADER_OFFSET + 4;
     private static final int DHCP_OPTION_MAGIC_COOKIE_OFFSET = DHCP_HEADER_OFFSET + 236;
 
     // DHCPv6 header
@@ -445,12 +444,15 @@
     private static final String IPV4_TEST_SUBNET_PREFIX = "192.168.1.0/24";
     private static final String IPV4_ANY_ADDRESS_PREFIX = "0.0.0.0/0";
     private static final String HOSTNAME = "testhostname";
+    private static final String TEST_IPV6_PREFIX = "2001:db8:1::/64";
     private static final String IPV6_OFF_LINK_DNS_SERVER = "2001:4860:4860::64";
     private static final String IPV6_ON_LINK_DNS_SERVER = "2001:db8:1::64";
     private static final int TEST_DEFAULT_MTU = 1500;
     private static final int TEST_MIN_MTU = 1280;
     private static final MacAddress ROUTER_MAC = MacAddress.fromString("00:1A:11:22:33:44");
     private static final byte[] ROUTER_MAC_BYTES = ROUTER_MAC.toByteArray();
+    private static final MacAddress ON_LINK_DNS_SERVER_MAC =
+            MacAddress.fromString("00:1A:11:AA:BB:CC");
     private static final Inet6Address ROUTER_LINK_LOCAL = ipv6Addr("fe80::1");
     private static final byte[] ROUTER_DUID = new byte[] {
             // type: Link-layer address, hardware type: EUI64(27)
@@ -464,16 +466,12 @@
     private static final String TEST_HOST_NAME = "AOSP on Crosshatch";
     private static final String TEST_HOST_NAME_TRANSLITERATION = "AOSP-on-Crosshatch";
     private static final String TEST_CAPTIVE_PORTAL_URL = "https://example.com/capportapi";
-    private static final byte[] TEST_HOTSPOT_OUI = new byte[] {
-            (byte) 0x00, (byte) 0x17, (byte) 0xF2
-    };
     private static final byte LEGACY_TEST_VENDOR_SPECIFIC_IE_TYPE = 0x11;
     private static final byte TEST_VENDOR_SPECIFIC_IE_TYPE = 0x21;
     private static final int TEST_VENDOR_SPECIFIC_IE_ID = 0xdd;
 
     private static final String TEST_DEFAULT_SSID = "test_ssid";
     private static final String TEST_DEFAULT_BSSID = "00:11:22:33:44:55";
-    private static final String TEST_DHCP_ROAM_SSID = "0001docomo";
     private static final String TEST_DHCP_ROAM_BSSID = "00:4e:35:17:98:55";
     private static final String TEST_DHCP_ROAM_L2KEY = "roaming_l2key";
     private static final String TEST_DHCP_ROAM_CLUSTER = "roaming_cluster";
@@ -752,9 +750,9 @@
         mIsSignatureRequiredTest = testMethod.getAnnotation(SignatureRequiredTest.class) != null;
         assumeFalse(testSkipped());
 
-        // Enable DHCPv6 Prefix Delegation.
-        setFeatureEnabled(NetworkStackUtils.IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION,
-                true /* isDhcp6PrefixDelegationEnabled */);
+        // Enable replacement of netd usage with netlink in IpClient.
+        setFeatureEnabled(NetworkStackUtils.IPCLIENT_REPLACE_NETD_WITH_NETLINK_VERSION,
+                true /* isIpClientReplaceNetdWithNetlinkEnabled */);
 
         // Set flags based on test method annotations.
         final Flag[] flags = testMethod.getAnnotationsByType(Flag.class);
@@ -834,7 +832,19 @@
         when(mPackageManager.getPackagesForUid(TEST_DEVICE_OWNER_APP_UID)).thenReturn(
                 new String[] { TEST_DEVICE_OWNER_APP_PACKAGE });
 
-        // Retrieve the network event count.
+        // Store a network event to db.
+        doAnswer(invocation -> {
+            final String cluster = invocation.getArgument(0);
+            final long timestamp = invocation.getArgument(1);
+            final long expiry = invocation.getArgument(2);
+            final int eventType = invocation.getArgument(3);
+            storeNetworkEvent(cluster, timestamp, expiry, eventType);
+            ((OnStatusListener) invocation.getArgument(4)).onComplete(new Status(SUCCESS));
+            return null;
+        }).when(mIpMemoryStore).storeNetworkEvent(eq(TEST_CLUSTER), anyLong(), anyLong(), anyInt(),
+                any());
+
+        // Retrieve the network event count from db.
         doAnswer(invocation -> {
             final String cluster = invocation.getArgument(0);
             final long[] sinceTimes = invocation.getArgument(1);
@@ -949,6 +959,48 @@
         }
     }
 
+    /**
+     * Set the sysctl "delay_first_probe_time" by executing a shell command as root.
+     *
+     * Directly using "su root echo delay > /proc/sys/net/ipv4/neigh/delay_first_probe_time" fails
+     * because only the "echo delay" command runs as root; the redirection ">" and the following
+     * sysctl path string is not performed by the root shell, resulting in permission errors.
+     *
+     * Theoretically we want to run "su root sh -c 'echo delay > /proc/sys/net/ipv4(v6)/neigh/
+     * delay_first_probe_time'" to change the sysctl, however, the `executeShellCommand`
+     * function splits commands based on spaces, even within single quotes, so this doesn't work.
+     * Instead, use `executeShellCommandRw` to execute a shell command receiving from stdin.
+     */
+    private void setNudDelayFirstProbeTime(int delay, String family) throws Exception {
+        final ParcelFileDescriptor[] fds = InstrumentationRegistry.getInstrumentation()
+                .getUiAutomation().executeShellCommandRw("su root sh");
+
+        final ParcelFileDescriptor stdout = fds[0];
+        final ParcelFileDescriptor stdin = fds[1];
+        try (ParcelFileDescriptor.AutoCloseOutputStream output =
+                     new ParcelFileDescriptor.AutoCloseOutputStream(stdin)) {
+            final String cmd = "echo " + delay + " > /proc/sys/net/" + family + "/neigh/"
+                    + mIfaceName + "/delay_first_probe_time";
+            output.write(cmd.getBytes());
+        }
+        // Setting a value to sysctl doesn't have any output.
+        final BufferedReader reader =
+                new BufferedReader(new FileReader(stdout.getFileDescriptor()));
+        assertNull(reader.readLine());
+
+        // Check if the sysctl "delay_first_probe_time" has updated.
+        final String delay_first_probe_time = getOneLineCommandOutput(
+                "su root cat /proc/sys/net/" + family + "/neigh/" + mIfaceName
+                        + "/delay_first_probe_time");
+        assertEquals(delay, Integer.parseInt(delay_first_probe_time));
+    }
+
+    private int getNeighborParameterUcastSolicit(String ifaceName) throws IOException {
+        final String ucast_solicit = getOneLineCommandOutput(
+                "su root cat /proc/sys/net/ipv6/neigh/" + ifaceName + "/ucast_solicit");
+        return Integer.parseInt(ucast_solicit);
+    }
+
     private MacAddress getIfaceMacAddr(String ifaceName) throws IOException {
         // InterfaceParams.getByName requires CAP_NET_ADMIN: read the mac address with the shell
         final String strMacAddr = getOneLineCommandOutput(
@@ -1041,15 +1093,6 @@
         return expectAlarmSet(inOrder, tagMatch, (long) afterSeconds, mIpc.getHandler());
     }
 
-    private boolean packetContainsExpectedField(final byte[] packet, final int offset,
-            final byte[] expected) {
-        if (packet.length < offset + expected.length) return false;
-        for (int i = 0; i < expected.length; ++i) {
-            if (packet[offset + i] != expected[i]) return false;
-        }
-        return true;
-    }
-
     private boolean isDhcpPacket(final byte[] packet) {
         final ByteBuffer buffer = ByteBuffer.wrap(packet);
 
@@ -1228,6 +1271,11 @@
         mPacketReader.sendResponse(packet);
     }
 
+    private void sendGratuitousArp(MacAddress srcMac, Inet4Address targetIp) throws IOException {
+        sendArpReply(ETHER_BROADCAST /* dstMac */, srcMac.toByteArray() /* srcMac */, targetIp,
+                targetIp /* sender IP */);
+    }
+
     private void startIpClientProvisioning(final ProvisioningConfiguration cfg) throws Exception {
         mIIpClient.startProvisioning(cfg.toStableParcelable());
     }
@@ -1475,7 +1523,7 @@
 
         if (shouldChangeMtu) {
             // Pretend that ConnectivityService set the MTU.
-            mNetd.interfaceSetMtu(mIfaceName, mtu);
+            NetlinkUtils.setInterfaceMtu(mIfaceName, mtu);
             assertEquals(NetworkInterface.getByName(mIfaceName).getMTU(), mtu);
         }
 
@@ -1489,12 +1537,9 @@
         try {
             mIpc.shutdown();
             awaitIpClientShutdown();
-            if (shouldRemoveTestInterface) {
-                verify(mNetd, never()).interfaceSetMtu(mIfaceName, TEST_DEFAULT_MTU);
-            } else {
+            if (!shouldRemoveTestInterface) {
                 // Verify that MTU indeed has been restored or not.
-                verify(mNetd, times(shouldChangeMtu ? 1 : 0))
-                        .interfaceSetMtu(mIfaceName, TEST_DEFAULT_MTU);
+                assertEquals(NetworkInterface.getByName(mIfaceName).getMTU(), TEST_DEFAULT_MTU);
             }
             verifyAfterIpClientShutdown();
         } catch (Exception e) {
@@ -1658,7 +1703,6 @@
             verify(mCb, never()).onProvisioningFailure(any());
             assertIpMemoryNeverStoreNetworkAttributes();
         } else if (isDhcpIpConflictDetectEnabled) {
-            int arpPacketCount = 0;
             final List<ArpPacket> packetList = new ArrayList<ArpPacket>();
             // Total sent ARP packets should be 5 (3 ARP Probes + 2 ARP Announcements)
             ArpPacket packet;
@@ -1969,15 +2013,6 @@
     }
 
     @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
-    public void testRestoreInitialInterfaceMtu_WithException() throws Exception {
-        doThrow(new RemoteException("NetdNativeService::interfaceSetMtu")).when(mNetd)
-                .interfaceSetMtu(mIfaceName, TEST_DEFAULT_MTU);
-
-        doRestoreInitialMtuTest(true /* shouldChangeMtu */, false /* shouldRemoveTestInterface */);
-        assertEquals(NetworkInterface.getByName(mIfaceName).getMTU(), TEST_MIN_MTU);
-    }
-
-    @Test @SignatureRequiredTest(reason = "TODO: evaluate whether signature perms are required")
     public void testRestoreInitialInterfaceMtu_NotFoundInterfaceWhenStopping() throws Exception {
         doRestoreInitialMtuTest(true /* shouldChangeMtu */, true /* shouldRemoveTestInterface */);
     }
@@ -2007,7 +2042,7 @@
         assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_MIN_MTU);
 
         // Pretend that ConnectivityService set the MTU.
-        mNetd.interfaceSetMtu(mIfaceName, TEST_MIN_MTU);
+        NetlinkUtils.setInterfaceMtu(mIfaceName, TEST_MIN_MTU);
         assertEquals(NetworkInterface.getByName(mIfaceName).getMTU(), TEST_MIN_MTU);
 
         reset(mCb);
@@ -2099,9 +2134,19 @@
                 mPacketReader.popPacket(PACKET_TIMEOUT_MS, this::isRouterSolicitation));
     }
 
+    private void sendGratuitousNeighborAdvertisement(final MacAddress srcMac,
+            final Inet6Address srcIp, final Inet6Address targetIp) throws Exception {
+        int flags = NEIGHBOR_ADVERTISEMENT_FLAG_ROUTER | NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE;
+        final Inet6Address dstIp = IPV6_ADDR_ALL_NODES_MULTICAST;
+        final MacAddress dstMac = NetworkStackUtils.ipv6MulticastToEthernetMulticast(dstIp);
+        final ByteBuffer packet =
+                NeighborAdvertisement.build(srcMac, dstMac, srcIp, dstIp, flags, targetIp);
+        mPacketReader.sendResponse(packet);
+    }
+
     private void sendRouterAdvertisement(boolean waitForRs, short lifetime, int valid,
             int preferred) throws Exception {
-        final ByteBuffer pio = buildPioOption(valid, preferred, "2001:db8:1::/64");
+        final ByteBuffer pio = buildPioOption(valid, preferred, TEST_IPV6_PREFIX);
         final ByteBuffer rdnss = buildRdnssOption(3600, IPV6_OFF_LINK_DNS_SERVER);
         sendRouterAdvertisement(waitForRs, lifetime, pio, rdnss);
     }
@@ -2154,6 +2199,18 @@
         return buildRaPacket((short) 1800, options);
     }
 
+    private static ByteBuffer buildRaPacket(final String prefix, final String dnsServer,
+            int validLifetime, int preferredLifetime, int dnsLifetime, boolean shouldIncludeSlla)
+            throws Exception {
+        final ByteBuffer pio = buildPioOption(validLifetime, preferredLifetime, prefix);
+        final ByteBuffer rdnss = buildRdnssOption(dnsLifetime, dnsServer);
+        final List<ByteBuffer> options = new ArrayList<ByteBuffer>();
+        options.add(pio);
+        options.add(rdnss);
+        if (shouldIncludeSlla) options.add(buildSllaOption());
+        return buildRaPacket(options.toArray(new ByteBuffer[options.size()]));
+    }
+
     private void disableIpv6ProvisioningDelays() throws Exception {
         // Speed up the test by disabling DAD and removing router_solicitation_delay.
         // We don't need to restore the default value because the interface is removed in tearDown.
@@ -2186,11 +2243,9 @@
 
     private LinkProperties doIpv6OnlyProvisioning() throws Exception {
         final InOrder inOrder = inOrder(mCb);
-        final ByteBuffer pio = buildPioOption(3600, 1800, "2001:db8:1::/64");
-        final ByteBuffer rdnss = buildRdnssOption(3600, IPV6_OFF_LINK_DNS_SERVER);
-        final ByteBuffer slla = buildSllaOption();
-        final ByteBuffer ra = buildRaPacket(pio, rdnss, slla);
-
+        final ByteBuffer ra = buildRaPacket(TEST_IPV6_PREFIX, IPV6_OFF_LINK_DNS_SERVER,
+                3600 /* validLifetime */, 1800 /* preferredLifetime */, 3600 /* dnsLifetime */,
+                true /* shouldIncludeSlla */);
         return doIpv6OnlyProvisioning(inOrder, ra);
     }
 
@@ -2283,12 +2338,10 @@
                 .build();
         startIpClientProvisioning(config);
 
-        final ByteBuffer pio = buildPioOption(600, 300, "2001:db8:1::/64");
-        // put an IPv6 link-local DNS server
-        final ByteBuffer rdnss = buildRdnssOption(600, ROUTER_LINK_LOCAL.getHostAddress());
-        // put SLLA option to avoid address resolution for "fe80::1"
-        final ByteBuffer slla = buildSllaOption();
-        final ByteBuffer ra = buildRaPacket(pio, rdnss, slla);
+        final ByteBuffer ra = buildRaPacket(TEST_IPV6_PREFIX,
+                ROUTER_LINK_LOCAL.getHostAddress() /* an IPv6 link-local DNS server */,
+                600 /* validLifetime */, 300 /* preferredLifetime */, 600 /* dnsLifetime */,
+                true /* shouldIncludeSlla */);
 
         waitForRouterSolicitation();
         mPacketReader.sendResponse(ra);
@@ -3092,17 +3145,15 @@
 
     private LinkProperties performDualStackProvisioning() throws Exception {
         final Inet6Address dnsServer = ipv6Addr(IPV6_OFF_LINK_DNS_SERVER);
-        final ByteBuffer pio = buildPioOption(3600, 1800, "2001:db8:1::/64");
-        final ByteBuffer rdnss = buildRdnssOption(3600, IPV6_OFF_LINK_DNS_SERVER);
-        final ByteBuffer slla = buildSllaOption();
-        final ByteBuffer ra = buildRaPacket(pio, rdnss, slla);
+        final ByteBuffer ra = buildRaPacket(TEST_IPV6_PREFIX, IPV6_OFF_LINK_DNS_SERVER,
+                3600 /* validLifetime */, 1800 /* preferredLifetime */, 3600 /* dnsLifetime */,
+                true /* shouldIncludeSlla */);
 
         return performDualStackProvisioning(ra, dnsServer);
     }
 
     private LinkProperties performDualStackProvisioning(final ByteBuffer ra,
             final InetAddress dnsServer) throws Exception {
-        final InOrder inOrder = inOrder(mCb);
         final CompletableFuture<LinkProperties> lpFuture = new CompletableFuture<>();
 
         // Start IPv4 provisioning first and wait IPv4 provisioning to succeed, and then start
@@ -3834,8 +3885,8 @@
         assertEquals(2, naList.size()); // privacy address and stable privacy address
     }
 
-    private void startGratuitousArpAndNaAfterRoamingTest(boolean isGratuitousArpNaRoamingEnabled,
-            boolean hasIpv4, boolean hasIpv6) throws Exception {
+    private void startGratuitousArpAndNaAfterRoamingTest(boolean hasIpv4, boolean hasIpv6)
+            throws Exception {
         final Layer2Information layer2Info = new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
                 MacAddress.fromString(TEST_DEFAULT_BSSID));
         final ScanResultInfo scanResultInfo =
@@ -3852,12 +3903,6 @@
         // not strictly necessary.
         setDhcpFeatures(true /* isRapidCommitEnabled */,
                 false /* isDhcpIpConflictDetectEnabled */);
-
-        if (isGratuitousArpNaRoamingEnabled) {
-            setFeatureEnabled(NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION, true);
-        } else {
-            setFeatureEnabled(NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION, false);
-        }
         startIpClientProvisioning(prov.build());
     }
 
@@ -3881,8 +3926,7 @@
 
     @Test
     public void testGratuitousArpAndNaAfterRoaming() throws Exception {
-        startGratuitousArpAndNaAfterRoamingTest(true /* isGratuitousArpNaRoamingEnabled */,
-                true /* hasIpv4 */, true /* hasIpv6 */);
+        startGratuitousArpAndNaAfterRoamingTest(true /* hasIpv4 */, true /* hasIpv6 */);
         performDualStackProvisioning();
         forceLayer2Roaming();
 
@@ -3895,23 +3939,8 @@
     }
 
     @Test
-    public void testGratuitousArpAndNaAfterRoaming_disableExpFlag() throws Exception {
-        startGratuitousArpAndNaAfterRoamingTest(false /* isGratuitousArpNaRoamingEnabled */,
-                true /* hasIpv4 */, true /* hasIpv6 */);
-        performDualStackProvisioning();
-        forceLayer2Roaming();
-
-        final List<ArpPacket> arpList = new ArrayList<>();
-        final List<NeighborAdvertisement> naList = new ArrayList<>();
-        waitForGratuitousArpAndNaPacket(arpList, naList);
-        assertEquals(2, naList.size()); // NAs sent due to RFC9131 implement, not from roam
-        assertEquals(0, arpList.size());
-    }
-
-    @Test
     public void testGratuitousArpAndNaAfterRoaming_IPv6OnlyNetwork() throws Exception {
-        startGratuitousArpAndNaAfterRoamingTest(true /* isGratuitousArpNaRoamingEnabled */,
-                false /* hasIpv4 */, true /* hasIpv6 */);
+        startGratuitousArpAndNaAfterRoamingTest(false /* hasIpv4 */, true /* hasIpv6 */);
         doIpv6OnlyProvisioning();
         forceLayer2Roaming();
 
@@ -3925,8 +3954,7 @@
 
     @Test
     public void testGratuitousArpAndNaAfterRoaming_IPv4OnlyNetwork() throws Exception {
-        startGratuitousArpAndNaAfterRoamingTest(true /* isGratuitousArpNaRoamingEnabled */,
-                true /* hasIpv4 */, false /* hasIpv6 */);
+        startGratuitousArpAndNaAfterRoamingTest(true /* hasIpv4 */, false /* hasIpv6 */);
 
         // Start IPv4 provisioning and wait until entire provisioning completes.
         handleDhcpPackets(true /* isSuccessLease */, TEST_LEASE_DURATION_S,
@@ -3946,7 +3974,6 @@
         assertEquals(ETH_P_IPV6, ns.ethHdr.etherType);
         assertEquals(IPPROTO_ICMPV6, ns.ipv6Hdr.nextHeader);
         assertEquals(0xff, ns.ipv6Hdr.hopLimit);
-        assertTrue(ns.ipv6Hdr.srcIp.isLinkLocalAddress());
         assertEquals(ICMPV6_NEIGHBOR_SOLICITATION, ns.icmpv6Hdr.type);
         assertEquals(0, ns.icmpv6Hdr.code);
         assertEquals(0, ns.nsHdr.reserved);
@@ -3986,15 +4013,32 @@
         return ns;
     }
 
-    private List<NeighborSolicitation> waitForMultipleNeighborSolicitations() throws Exception {
+    private NeighborSolicitation waitForMulticastNeighborSolicitation(final Inet6Address targetIp)
+            throws Exception {
+        NeighborSolicitation ns;
+        while ((ns = getNextNeighborSolicitation()) != null) {
+            if (ns.ipv6Hdr.dstIp.isMulticastAddress() // Solicited-node multicast address
+                    && ns.nsHdr.target.equals(targetIp)) {
+                break;
+            }
+        }
+        assertNotNull("No multicast Neighbor solicitation received on interface within timeout",
+                ns);
+        assertMulticastNeighborSolicitation(ns, targetIp);
+        return ns;
+    }
+
+    private List<NeighborSolicitation> waitForMultipleNeighborSolicitations(int expectedNsCount)
+            throws Exception {
         NeighborSolicitation ns;
         final List<NeighborSolicitation> nsList = new ArrayList<NeighborSolicitation>();
         while ((ns = getNextNeighborSolicitation()) != null) {
-            // Filter out the multicast NSes used for duplicate address detetction, the target
+            // Filter out the multicast NSes used for duplicate address detection, the target
             // address is the global IPv6 address inside these NSes, and multicast NSes sent from
             // device's GUAs to force first-hop router to update the neighbor cache entry.
             if (ns.ipv6Hdr.srcIp.isLinkLocalAddress() && ns.nsHdr.target.isLinkLocalAddress()) {
                 nsList.add(ns);
+                if (nsList.size() == expectedNsCount) break;
             }
         }
         assertFalse(nsList.isEmpty());
@@ -4056,8 +4100,80 @@
         verify(mCb, never()).onReachabilityLost(any());
     }
 
+    // If the UDP packet is sent to off-link address, the targetIp should be default gateway's IP,
+    // otherwise, it should be the on-link DNS server address.
+    private void expectAndRespondToMulticastNeighborSolicitation(final Inet6Address targetIp)
+            throws Exception {
+        final NeighborSolicitation ns = waitForMulticastNeighborSolicitation(targetIp);
+        final MacAddress srcMac =
+                targetIp.equals(ROUTER_LINK_LOCAL) ? ROUTER_MAC : ON_LINK_DNS_SERVER_MAC;
+        final Inet6Address srcIp = targetIp.equals(ROUTER_LINK_LOCAL)
+                ? ROUTER_LINK_LOCAL
+                : ipv6Addr(IPV6_ON_LINK_DNS_SERVER);
+        int flag = NEIGHBOR_ADVERTISEMENT_FLAG_ROUTER | NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED;
+        final ByteBuffer na = NeighborAdvertisement.build(srcMac,
+                ns.ethHdr.srcMac /* dstMac */, srcIp,
+                ns.ipv6Hdr.srcIp /* dstIp */, flag, targetIp);
+        mPacketReader.sendResponse(na);
+    }
+
+    private void verifyRestoringNeighborParametersToSteadyState() throws Exception {
+        final int expectedNudSolicitNum = readNudSolicitNumInSteadyStateFromResource();
+        final long startTime = System.currentTimeMillis();
+        // polling the "ucast_solicit" sysctl every 500ms to check if it's restored to steady
+        // state, i.e. probe count of 10.
+        while (getNeighborParameterUcastSolicit(mIfaceName) != expectedNudSolicitNum) {
+            if (System.currentTimeMillis() - startTime >= TEST_TIMEOUT_MS) {
+                final String msg = "neighbor parameter ucast_solicit isn't restored to "
+                        + expectedNudSolicitNum + " within " + TEST_TIMEOUT_MS + "ms";
+                fail(msg);
+            }
+            Thread.sleep(TEST_POLL_NEIGHBOR_PARAMETER_MS);
+        }
+    }
+
+    /**
+     *  A function helper to set up the steps to verify NUD (neighbor unreachable detection) probes.
+     *  This function helper intends to respond to the multicast NS for the default gateway during
+     *  address resolution, which makes the default gateway neighbor reachable, it ends up starting
+     *  an L2 roam, which will trigger kernel to probe all neighbors later then. The specific test
+     *  case may or may not respond to that probes, depending on whether it expectes an NUD failure
+     *  from that probe.
+     *
+     *  If a specific test case expects to see an NUD failure after an L2 roam, then it should not
+     *  respond to any unicast NS or multicast NS (if multicast_resolicit feature is enabled). The
+     *  packet order example as below, fe80::bf8e:de37:69d7:2b29 is the IPv6 link-local address of
+     *  a test tap interface.
+     *
+     * 7 fe80::bf8e:de37:69d7:2b29  ff02::2 ICMPv6  76  Router Solicitation from 0a:c9:06:70:77:b3
+     * 9 fe80::1                    ff02::1 ICMPv6  13  Router Advertisement
+     *
+     * tap interface does address resolution for default gateway fe80::1 due to a DNS look-up
+     * query to sent to off-link DNS server. And responding to NS with NA to make the default
+     * gateway as reachable.
+     * 13 2001:db8:1:0:babe:11e3:49d8:d4af  ff02::1:ff00:1     ICMPv6  92  Neighbor Solicitation for fe80::1 from 0a:c9:06:70:77:b3
+     * 16 fe80::1  2001:db8:1:0:babe:11e3:49d8:d4af            ICMPv6  92  Neighbor Advertisement fe80::1 (rtr, sol) is at 00:1a:11:22:33:44
+     * 17 2001:db8:1:0:1818:bf6:5e82:4378   2001:4860:4860::64 DNS     99  Standard query 0xa13e AAAA ipv4only.arpa
+     *
+     * Post an L2 roam, the tap interface forces kernel start another probe, it sends 5 unicast
+     * NSes, the tap interface eventually gets a NUD failure due to there is no response.
+     * 21 fe80::bf8e:de37:69d7:2b29   fe80::1   ICMPv6  92   Neighbor Solicitation for fe80::1 from 0a:c9:06:70:77:b3
+     * 23 fe80::bf8e:de37:69d7:2b29   fe80::1   ICMPv6  92   Neighbor Solicitation for fe80::1 from 0a:c9:06:70:77:b3
+     * 25 fe80::bf8e:de37:69d7:2b29   fe80::1   ICMPv6  92   Neighbor Solicitation for fe80::1 from 0a:c9:06:70:77:b3
+     * 26 fe80::bf8e:de37:69d7:2b29   fe80::1   ICMPv6  92   Neighbor Solicitation for fe80::1 from 0a:c9:06:70:77:b3
+     * 27 fe80::bf8e:de37:69d7:2b29   fe80::1   ICMPv6  92   Neighbor Solicitation for fe80::1 from 0a:c9:06:70:77:b3
+     *
+     * Extra 3 multicat NSes are sent if "mcast_resolitict" sysctl is enabled.
+     * 28 fe80::bf8e:de37:69d7:2b29   ff02::1:ff00:1   ICMPv6   92    Neighbor Solicitation for fe80::1 from 0a:c9:06:70:77:b3
+     * 29 fe80::bf8e:de37:69d7:2b29   ff02::1:ff00:1   ICMPv6   92    Neighbor Solicitation for fe80::1 from 0a:c9:06:70:77:b3
+     * 31 fe80::bf8e:de37:69d7:2b29   ff02::1:ff00:1   ICMPv6   92    Neighbor Solicitation for fe80::1 from 0a:c9:06:70:77:b3
+     */
     private void prepareIpReachabilityMonitorTest(boolean isMulticastResolicitEnabled)
             throws Exception {
+        mNetworkAgentThread =
+                new HandlerThread(IpClientIntegrationTestCommon.class.getSimpleName());
+        mNetworkAgentThread.start();
+
         final ScanResultInfo info = makeScanResultInfo(TEST_DEFAULT_SSID, TEST_DEFAULT_BSSID);
         ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                 .withLayer2Information(new Layer2Information(TEST_L2KEY, TEST_CLUSTER,
@@ -4070,18 +4186,44 @@
                 isMulticastResolicitEnabled);
         startIpClientProvisioning(config);
         verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true);
-        doIpv6OnlyProvisioning();
 
-        // Simulate the roaming.
+        final ByteBuffer ra = buildRaPacket(TEST_IPV6_PREFIX, IPV6_OFF_LINK_DNS_SERVER,
+                3600 /* validLifetime */, 1800 /* preferredLifetime */, 3600 /* dnsLifetime */,
+                false /* shouldIncludeSlla */);
+        // RA doesn't include SLLA option, thus device has no IPv6 link-local address and mac
+        // address mapping for default gateway. sending a UDP packet to off-line DNS server will
+        // trigger the address resolution for default gateway, simulate that default gateway is
+        // reachable by responding to those NSes.
+        final LinkProperties lp = doIpv6OnlyProvisioning(null /* inOrder */, ra);
+        runAsShell(MANAGE_TEST_NETWORKS, () -> createTestNetworkAgentAndRegister(lp));
+        sendPacketToPeer(ipv6Addr(IPV6_OFF_LINK_DNS_SERVER));
+        expectAndRespondToMulticastNeighborSolicitation(ROUTER_LINK_LOCAL);
+        assertNeverNotifyNeighborLost();
+
+        // There is a race between the IpReachabilityMonitor handling of reachable neighbors and the
+        // following test code that forces the start of L2 roaming. Once the neighbor is confirmed
+        // to be still reachable, IpReachabilityMonitor restores the NUD parameters such as probe
+        // count and probe interval to the steady state (probe count of 10 and interval of 750 ms).
+        // Starting L2 roaming sets the NUD parameters to the post-roaming mode (probe count of 5
+        // and interval of 750 ms). If the following test code occurs early, IpReachabilityMonitor
+        // will first set the NUD parameters to the post-roaming state and then restore to the
+        // stable state, which means that the device will send more probes, which may cause flaky
+        // test because we expect to see a NUD failure event after receiving all expected probes,
+        // but sometimes this does not happen because the device is still retransmitting more.
+        // Polling the "unicast_solicit" sysctl to check if the IpReachabilityMonitor has already
+        // restore the neighbor parameter to steady state.
+        verifyRestoringNeighborParametersToSteadyState();
+
+        // Simulate the L2 roaming, this will trigger kernel to probe all neighbors again.
         forceLayer2Roaming();
     }
 
     private void runIpReachabilityMonitorProbeFailedTest() throws Exception {
         prepareIpReachabilityMonitorTest();
 
-        final List<NeighborSolicitation> nsList = waitForMultipleNeighborSolicitations();
         final int expectedNudSolicitNum = readNudSolicitNumPostRoamingFromResource();
-        assertEquals(expectedNudSolicitNum, nsList.size());
+        final List<NeighborSolicitation> nsList =
+                waitForMultipleNeighborSolicitations(expectedNudSolicitNum);
         for (NeighborSolicitation ns : nsList) {
             assertUnicastNeighborSolicitation(ns, ROUTER_MAC /* dstMac */,
                     ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */);
@@ -4089,7 +4231,6 @@
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = false)
     public void testIpReachabilityMonitor_probeFailed() throws Exception {
         runIpReachabilityMonitorProbeFailedTest();
         assertNotifyNeighborLost(ROUTER_LINK_LOCAL /* targetIp */,
@@ -4124,10 +4265,10 @@
     private void runIpReachabilityMonitorMcastResolicitProbeFailedTest() throws Exception {
         prepareIpReachabilityMonitorTest(true /* isMulticastResolicitEnabled */);
 
-        final List<NeighborSolicitation> nsList = waitForMultipleNeighborSolicitations();
         final int expectedNudSolicitNum = readNudSolicitNumPostRoamingFromResource();
         int expectedSize = expectedNudSolicitNum + NUD_MCAST_RESOLICIT_NUM;
-        assertEquals(expectedSize, nsList.size());
+        final List<NeighborSolicitation> nsList =
+                waitForMultipleNeighborSolicitations(expectedSize);
         for (NeighborSolicitation ns : nsList.subList(0, expectedNudSolicitNum)) {
             assertUnicastNeighborSolicitation(ns, ROUTER_MAC /* dstMac */,
                     ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */);
@@ -4138,7 +4279,6 @@
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = false)
     public void testIpReachabilityMonitor_mcastResolicitProbeFailed() throws Exception {
         runIpReachabilityMonitorMcastResolicitProbeFailedTest();
         assertNotifyNeighborLost(ROUTER_LINK_LOCAL /* targetIp */,
@@ -4196,7 +4336,47 @@
                 NudEventType.NUD_POST_ROAMING_MAC_ADDRESS_CHANGED);
     }
 
-    private void prepareIpReachabilityMonitorIpv4AddressResolutionTest() throws Exception {
+    private ArpPacket expectAndRespondToArpRequest(MacAddress srcMac,
+            Inet4Address targetIp) throws Exception {
+        final ArpPacket request = getNextArpPacket();
+        assertArpRequest(request, targetIp);
+        sendArpReply(request.senderHwAddress.toByteArray() /* dst */, srcMac.toByteArray(),
+                request.senderIp /* target IP */, targetIp /* sender IP */);
+        return request;
+    }
+
+    /**
+     * A function helper to set up the steps to verify NUD (neighbor unreachable detection) probes
+     * for IPv4 hosts. This function helper intends to respond to the ARP probes for the default
+     * gateway during address resolution if the param shouldMakeNeighborReachableFirst is true,
+     * which makes the default gateway reachable first, and then sending a gratuitous ARP with a
+     * different mac address, it will override the ARP entry on the tap interface, as a result, it
+     * should do the address resolution again. The param shouldMakeNeighborReachableFirst depends on
+     * the specific test case.
+     *
+     * If a specific test case expectes to see an NUD failure after that, then it should not respond
+     * to any upcoming ARP probes. The packet order example as below,
+     *
+     * 37  0.0.0.0        255.255.255.255  DHCP  338   DHCP Discover - Transaction ID 0xf367c95c
+     * 38  192.168.1.100  192.168.1.2      DHCP  360   DHCP ACK      - Transaction ID 0xf367c95c
+     *
+     * 45  66:64:50:87:c3:22  ARP  48  Who has 192.168.1.100? Tell 192.168.1.2
+     * 46  Google_22:33:44    ARP  48  192.168.1.100 is at 00:1a:11:22:33:44
+     * 47  192.168.1.2  192.168.1.100  UDP  148  47467 1234 Len=100
+     *
+     * 76  Google_22:33:55  ARP  48   Gratuitous ARP for 192.168.1.100 (Reply) (duplicate use of 192.168.1.100 detected!)
+     * 77  192.168.1.2  192.168.1.100  UDP  148  38374  1234 Len=100
+     *
+     * 92  66:64:50:87:c3:22  ARP  48  Who has 192.168.1.100? Tell 192.168.1.2
+     * 93  66:64:50:87:c3:22  ARP  48  Who has 192.168.1.100? Tell 192.168.1.2
+     * ...
+     * 116 66:64:50:87:c3:22  ARP  48  Who has 192.168.1.100? Tell 192.168.1.2
+     */
+    private void prepareIpReachabilityMonitorIpv4AddressResolutionTest(
+            boolean shouldMakeNeighborReachableFirst) throws Exception {
+        // Reduce the delay first probe time from 5s to 2s, speed up the test duration and we can
+        // still use PACKET_TIMEOUT_MS to wait for the next upcoming ARP packet.
+        setNudDelayFirstProbeTime(TEST_DELAY_FIRST_PROBE_TIME_S, "ipv4");
         mNetworkAgentThread =
                 new HandlerThread(IpClientIntegrationTestCommon.class.getSimpleName());
         mNetworkAgentThread.start();
@@ -4215,17 +4395,35 @@
 
         runAsShell(MANAGE_TEST_NETWORKS, () -> createTestNetworkAgentAndRegister(lp));
 
-        // Send a UDP packet to IPv4 DNS server to trigger address resolution process for IPv4
-        // on-link DNS server or default router.
-        final Random random = new Random();
-        final byte[] data = new byte[100];
-        random.nextBytes(data);
-        sendUdpPacketToNetwork(mNetworkAgent.getNetwork(), SERVER_ADDR, 1234 /* port */, data);
+        // Send a UDP packet to IPv4 on-link DNS server to trigger address resolution process for
+        // the default gateway, respond to the broadcast ARP probe and make the default gateway as
+        // reachable.
+        sendPacketToPeer(SERVER_ADDR);
+        if (shouldMakeNeighborReachableFirst) {
+            final ArpPacket request = expectAndRespondToArpRequest(ROUTER_MAC, SERVER_ADDR);
+            assertNotNull(request);
+            verifyRestoringNeighborParametersToSteadyState();
+
+            // Wait the locktime expires then we are able to override ARP entry by sending a
+            // gratuitous ARP with a different MAC address, see locktime sysctl on
+            // https://man7.org/linux/man-pages/man7/arp.7.html.
+            Thread.sleep(TEST_ARP_LOCKTIME_MS);
+
+            // Send a gratuitous ARP to override the default gateway MAC address, this makes the
+            // default gateway become stale in the ARP entry, sending another UDP packet to the
+            // default gateway make it transit to DELAY state. If no reachability confirmation is
+            // received within DELAY_FIRST_PROBE_TIME seconds of entering the DELAY state, kernel
+            // goes to PROBE state and start probing, see RFC 4861 section 7.3.2.
+            final MacAddress newMac = MacAddress.fromString("00:1A:11:22:33:55");
+            sendGratuitousArp(newMac, SERVER_ADDR);
+            sendPacketToPeer(SERVER_ADDR);
+        }
     }
 
     private void doTestIpReachabilityMonitor_replyBroadcastArpRequestWithDiffMacAddresses(
             boolean disconnect) throws Exception {
-        prepareIpReachabilityMonitorIpv4AddressResolutionTest();
+        prepareIpReachabilityMonitorIpv4AddressResolutionTest(
+                false /* shouldMakeNeighborReachableFirst */);
 
         // Respond to the broadcast ARP request.
         final ArpPacket request = getNextArpPacket();
@@ -4233,7 +4431,7 @@
         sendArpReply(request.senderHwAddress.toByteArray() /* dst */, ROUTER_MAC_BYTES /* srcMac */,
                 request.senderIp /* target IP */, SERVER_ADDR /* sender IP */);
 
-        Thread.sleep(1500);
+        Thread.sleep(TEST_ARP_LOCKTIME_MS);
 
         // Reply with a different MAC address but the same server IP.
         final MacAddress gateway = MacAddress.fromString("00:11:22:33:44:55");
@@ -4271,7 +4469,9 @@
     @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = true)
     public void testIpReachabilityMonitor_ignoreIpv4DefaultRouterOrganicNudFailure()
             throws Exception {
-        prepareIpReachabilityMonitorIpv4AddressResolutionTest();
+        prepareIpReachabilityMonitorIpv4AddressResolutionTest(
+                true /* shouldMakeNeighborReachableFirst */
+        );
 
         ArpPacket packet;
         while ((packet = getNextArpPacket(TEST_TIMEOUT_MS)) != null) {
@@ -4281,14 +4481,14 @@
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = false)
     @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = false)
     public void testIpReachabilityMonitor_ignoreIpv4DefaultRouterOrganicNudFailure_flagoff()
             throws Exception {
-        prepareIpReachabilityMonitorIpv4AddressResolutionTest();
+        prepareIpReachabilityMonitorIpv4AddressResolutionTest(
+                true /* shouldMakeNeighborReachableFirst */);
 
         ArpPacket packet;
-        while ((packet = getNextArpPacket(TEST_TIMEOUT_MS)) != null) {
+        while ((packet = getNextArpPacket(PACKET_TIMEOUT_MS)) != null) {
             // wait address resolution to complete.
         }
         final ArgumentCaptor<ReachabilityLossInfoParcelable> lossInfoCaptor =
@@ -4307,8 +4507,44 @@
         socket.send(pkt);
     }
 
+    /**
+     * A function helper to set up the steps to verify NUD (neighbor unreachable detection) probes
+     * for IPv6 hosts. This function helper intends to respond to the NS probes for the default
+     * gateway during address resolution if the param shouldMakeNeighborReachableFirst is true,
+     * which makes the default gateway reachable first, and then sending a gratuitous NA with a
+     * different mac address, it will override the neighbor entry on the tap interface, as a result,
+     * it should do the address resolution eventually. The param shouldMakeNeighborReachableFirst
+     * depends on the specific test case which may or may not expect a NUD failure event.
+     *
+     * 36  2025-02-12 13:19:11.316069  fe80::1e3a:4b22:df15:896c  ff02::2  ICMPv6  76   Router Solicitation from 46:0c:b1:ab:47:f7
+     * 46  2025-02-12 13:19:11.409184  fe80::1  ff02::1                    ICMPv6  132  Router Advertisement
+     *
+     * // Respond to the NS for the default gateway and make the default gateay as reachable.
+     * 50  2025-02-12 13:19:11.443185  2001:db8:1:0:d8d6:93c1:9620:2a37  ff02::1:ff00:1      ICMPv6  92  Neighbor Solicitation for fe80::1 from 46:0c:b1:ab:47:f7
+     * 51  2025-02-12 13:19:11.448864  2001:db8:1:0:48c7:9fe:4ee:47ad    ff02::1:ff00:1      ICMPv6  92  Neighbor Solicitation for fe80::1 from 46:0c:b1:ab:47:f7
+     * 52  2025-02-12 13:19:11.522637  2001:db8:1:0:48c7:9fe:4ee:47ad    ff02::1:ff00:1      ICMPv6  92  Neighbor Solicitation for fe80::1 from 46:0c:b1:ab:47:f7
+     * 53  2025-02-12 13:19:11.523851  fe80::1  2001:db8:1:0:d8d6:93c1:9620:2a37             ICMPv6  92  Neighbor Advertisement fe80::1 (rtr, sol) is at 00:1a:11:22:33:44
+     * 54  2025-02-12 13:19:11.523881  2001:db8:1:0:48c7:9fe:4ee:47ad    2001:4860:4860::64  UDP  168  49350 → 1234 Len=100
+     *
+     * 55  2025-02-12 13:19:11.535166  fe80::1  ff02::1  ICMPv6  92  Neighbor Advertisement fe80::1 (rtr, ovr) is at 00:11:22:33:44:55
+     * 56  2025-02-12 13:19:11.535853  2001:db8:1:0:48c7:9fe:4ee:47ad  2001:4860:4860::64  UDP  168  36306 → 1234 Len=100
+     *
+     * // After test_delay_first_probe_time(2s) the tap interface starts probing again.
+     * 99   2025-02-12 13:19:13.544188  fe80::1e3a:4b22:df15:896c  fe80::1         ICMPv6  92  Neighbor Solicitation for fe80::1 from 46:0c:b1:ab:47:f7
+     * 107  2025-02-12 13:19:14.312212  fe80::1e3a:4b22:df15:896c  fe80::1         ICMPv6  92  Neighbor Solicitation for fe80::1 from 46:0c:b1:ab:47:f7
+     * ...
+     * 133  2025-02-12 13:19:20.456209  fe80::1e3a:4b22:df15:896c  fe80::1         ICMPv6  92  Neighbor Solicitation for fe80::1 from 46:0c:b1:ab:47:f7
+     * 134  2025-02-12 13:19:21.224214  fe80::1e3a:4b22:df15:896c  ff02::1:ff00:1  ICMPv6  92  Neighbor Solicitation for fe80::1 from 46:0c:b1:ab:47:f7
+     * 135  2025-02-12 13:19:21.992192  fe80::1e3a:4b22:df15:896c  ff02::1:ff00:1  ICMPv6  92  Neighbor Solicitation for fe80::1 from 46:0c:b1:ab:47:f7
+     * 136  2025-02-12 13:19:22.760202  fe80::1e3a:4b22:df15:896c  ff02::1:ff00:1  ICMPv6  92  Neighbor Solicitation for fe80::1 from 46:0c:b1:ab:47:f7
+     */
     private void prepareIpReachabilityMonitorAddressResolutionTest(final String dnsServer,
-            final Inet6Address targetIp) throws Exception {
+            final Inet6Address targetIp,
+            boolean shouldMakeNeighborReachableFirst) throws Exception {
+        // Reduce the delay first probe time from 5s to 2s, speed up the test duration and we can
+        // still use PACKET_TIMEOUT_MS to wait for the next upcoming ARP packet.
+        setNudDelayFirstProbeTime(TEST_DELAY_FIRST_PROBE_TIME_S, "ipv6");
+
         mNetworkAgentThread =
                 new HandlerThread(IpClientIntegrationTestCommon.class.getSimpleName());
         mNetworkAgentThread.start();
@@ -4332,18 +4568,30 @@
         startIpClientProvisioning(config);
         verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true);
 
-        final List<ByteBuffer> options = new ArrayList<ByteBuffer>();
-        options.add(buildPioOption(3600, 1800, "2001:db8:1::/64")); // PIO
-        options.add(buildRdnssOption(3600, dnsServer));             // RDNSS
-        // If target IP of address resolution is default router's IPv6 link-local address,
-        // then we should not take SLLA option in RA.
-        if (!targetIp.equals(ROUTER_LINK_LOCAL)) {
-            options.add(buildSllaOption());                         // SLLA
-        }
-        final ByteBuffer ra = buildRaPacket(options.toArray(new ByteBuffer[options.size()]));
+        final ByteBuffer ra = buildRaPacket(TEST_IPV6_PREFIX, dnsServer,
+                3600 /* validLifetime */, 1800 /* preferredLifetime */, 3600 /* dnsLifetime */,
+                !targetIp.equals(ROUTER_LINK_LOCAL) /* shouldIncludeSlla */);
         final Inet6Address dnsServerIp = ipv6Addr(dnsServer);
         final LinkProperties lp = performDualStackProvisioning(ra, dnsServerIp);
         runAsShell(MANAGE_TEST_NETWORKS, () -> createTestNetworkAgentAndRegister(lp));
+
+        if (shouldMakeNeighborReachableFirst) {
+            sendPacketToPeer(dnsServerIp);
+            expectAndRespondToMulticastNeighborSolicitation(targetIp);
+            assertNeverNotifyNeighborLost();
+            verifyRestoringNeighborParametersToSteadyState();
+
+            // Send a gratuitous NA for the target neighbor with a different MAC address, this
+            // should make the neighbor state transit from REACHABLE to STALE, sending another UDP
+            // packet to neighbor will force the neighbor state transit from STALE to DELAY, and
+            // after DELAY_FIRST_PROBE_TIME the tap interface starts probing for the target neighbor
+            // again.
+            final MacAddress srcMac = MacAddress.fromString("00:11:22:33:44:55");
+            final Inet6Address srcIp = targetIp.equals(ROUTER_LINK_LOCAL)
+                    ? ROUTER_LINK_LOCAL
+                    : ipv6Addr(IPV6_ON_LINK_DNS_SERVER);
+            sendGratuitousNeighborAdvertisement(srcMac, srcIp, targetIp);
+        }
     }
 
     /**
@@ -4353,26 +4601,27 @@
      * If dstIp is off-link, then targetIp should be the IPv6 default router.
      * The ND cache should not have an entry for targetIp.
      */
-    private void sendPacketToUnreachableNeighbor(Inet6Address dstIp) throws Exception {
+    private void sendPacketToPeer(final InetAddress dstIp) throws Exception {
         final Random random = new Random();
         final byte[] data = new byte[100];
         random.nextBytes(data);
         sendUdpPacketToNetwork(mNetworkAgent.getNetwork(), dstIp, 1234 /* port */, data);
     }
 
-    private void expectAndDropMulticastNses(Inet6Address targetIp, boolean expectNeighborLost)
+    private void expectAndDropMultipleNses(Inet6Address targetIp, boolean expectNeighborLost)
             throws Exception {
-        // Wait for the multicast NSes but never respond to them, that results in the on-link
-        // DNS gets lost and onReachabilityLost callback will be invoked.
+        // Wait for the multiple NSes but never respond to them, that results in the on-link
+        // DNS or default gateway gets lost and onReachabilityLost callback will be invoked.
         final List<NeighborSolicitation> nsList = new ArrayList<NeighborSolicitation>();
         NeighborSolicitation ns;
         while ((ns = getNextNeighborSolicitation()) != null) {
-            // multicast NS for address resolution, IPv6 dst address in that NS is solicited-node
-            // multicast address based on the target IP, the target IP is either on-link IPv6 DNS
-            // server address or IPv6 link-local address of default gateway.
+            // multiple NSes for address resolution, a few unicast NSes will be sent first and
+            // fall back to multicast if mcast_resolicit is enabled. IPv6 dst address in multicast
+            // NS is solicited-node multicast address based on the target IP, the target IP is
+            // either on-link IPv6 DNS server address or IPv6 link-local address of default gateway.
             final LinkAddress actual = new LinkAddress(ns.nsHdr.target, 64);
             final LinkAddress target = new LinkAddress(targetIp, 64);
-            if (actual.equals(target) && ns.ipv6Hdr.dstIp.isMulticastAddress()) {
+            if (actual.equals(target)) {
                 nsList.add(ns);
             }
         }
@@ -4387,107 +4636,102 @@
 
     private void runIpReachabilityMonitorAddressResolutionTest(final String dnsServer,
             final Inet6Address targetIp,
+            final boolean shouldMakeNeighborReachableFirst,
             final boolean expectNeighborLost) throws Exception {
-        prepareIpReachabilityMonitorAddressResolutionTest(dnsServer, targetIp);
-        sendPacketToUnreachableNeighbor(ipv6Addr(dnsServer));
-        expectAndDropMulticastNses(targetIp, expectNeighborLost);
+        prepareIpReachabilityMonitorAddressResolutionTest(dnsServer, targetIp,
+                shouldMakeNeighborReachableFirst);
+        sendPacketToPeer(ipv6Addr(dnsServer));
+        expectAndDropMultipleNses(targetIp, expectNeighborLost);
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION, enabled = true)
-    @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION, enabled = false)
     @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = false)
     public void testIpReachabilityMonitor_incompleteIpv6DnsServerInDualStack() throws Exception {
         final Inet6Address targetIp = ipv6Addr(IPV6_ON_LINK_DNS_SERVER);
-        runIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER, targetIp,
+        runIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER,
+                targetIp,
+                false /* shouldMakeNeighborReachableFirst */,
                 false /* expectNeighborLost */);
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION, enabled = false)
-    @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION, enabled = false)
     @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = false)
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = false)
     public void testIpReachabilityMonitor_incompleteIpv6DnsServerInDualStack_flagoff()
             throws Exception {
         final Inet6Address targetIp = ipv6Addr(IPV6_ON_LINK_DNS_SERVER);
-        runIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER, targetIp,
-                true /* expectNeighborLost */);
+        runIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER,
+                targetIp,
+                false /* shouldMakeNeighborReachableFirst */,
+                false /* expectNeighborLost */);
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION, enabled = false)
-    @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION, enabled = true)
     @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = false)
     public void testIpReachabilityMonitor_incompleteIpv6DefaultRouterInDualStack()
             throws Exception {
         runIpReachabilityMonitorAddressResolutionTest(IPV6_OFF_LINK_DNS_SERVER,
                 ROUTER_LINK_LOCAL /* targetIp */,
+                false /* shouldMakeNeighborReachableFirst */,
                 false /* expectNeighborLost */);
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION, enabled = false)
-    @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION, enabled = false)
     @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = false)
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = false)
     public void testIpReachabilityMonitor_incompleteIpv6DefaultRouterInDualStack_flagoff()
             throws Exception {
         runIpReachabilityMonitorAddressResolutionTest(IPV6_OFF_LINK_DNS_SERVER,
                 ROUTER_LINK_LOCAL /* targetIp */,
-                true /* expectNeighborLost */);
-    }
-
-    @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION, enabled = false)
-    @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION, enabled = false)
-    @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = true)
-    public void testIpReachabilityMonitor_ignoreOnLinkIpv6DnsOrganicNudFailure()
-            throws Exception {
-        final Inet6Address targetIp = ipv6Addr(IPV6_ON_LINK_DNS_SERVER);
-        runIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER, targetIp,
+                false /* shouldMakeNeighborReachableFirst */,
                 false /* expectNeighborLost */);
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION, enabled = false)
-    @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION, enabled = false)
-    @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = false)
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = false)
-    public void testIpReachabilityMonitor_ignoreOnLinkIpv6DnsOrganicNudFailure_flagoff()
+    @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = true)
+    public void testIpReachabilityMonitor_ignoreOnLinkIpv6DnsOrganicNudFailure()
             throws Exception {
         final Inet6Address targetIp = ipv6Addr(IPV6_ON_LINK_DNS_SERVER);
-        runIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER, targetIp,
-                true /* expectNeighborLost */);
+        runIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER,
+                targetIp,
+                true /* shouldMakeNeighborReachableFirst */,
+                false /* expectNeighborLost */);
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION, enabled = false)
-    @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION, enabled = false)
+    @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = false)
+    public void testIpReachabilityMonitor_ignoreOnLinkIpv6DnsOrganicNudFailure_flagoff()
+            throws Exception {
+        final Inet6Address targetIp = ipv6Addr(IPV6_ON_LINK_DNS_SERVER);
+        runIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER,
+                targetIp,
+                false /* shouldMakeNeighborReachableFirst */,
+                false /* expectNeighborLost */);
+    }
+
+    @Test
     @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = true)
     public void testIpReachabilityMonitor_ignoreIpv6DefaultRouterOrganicNudFailure()
             throws Exception {
         runIpReachabilityMonitorAddressResolutionTest(IPV6_OFF_LINK_DNS_SERVER,
                 ROUTER_LINK_LOCAL /* targetIp */,
+                true /* shouldMakeNeighborReachableFirst */,
                 false /* expectNeighborLost */);
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION, enabled = false)
-    @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION, enabled = false)
     @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = false)
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = false)
     public void testIpReachabilityMonitor_ignoreIpv6DefaultRouterOrganicNudFailure_flagoff()
             throws Exception {
         runIpReachabilityMonitorAddressResolutionTest(IPV6_OFF_LINK_DNS_SERVER,
                 ROUTER_LINK_LOCAL /* targetIp */,
+                true /* shouldMakeNeighborReachableFirst */,
                 true /* expectNeighborLost */);
     }
 
     private void runIpReachabilityMonitorEverReachableIpv6NeighborTest(final String dnsServer,
             final Inet6Address targetIp) throws Exception {
-        prepareIpReachabilityMonitorAddressResolutionTest(dnsServer, targetIp);
-        sendPacketToUnreachableNeighbor(ipv6Addr(dnsServer));
+        prepareIpReachabilityMonitorAddressResolutionTest(dnsServer, targetIp,
+                false /*shouldMakeNeighborReachableFirst */);
+        sendPacketToPeer(ipv6Addr(dnsServer));
 
         // Simulate the default router/DNS was reachable by responding to multicast NS(not for DAD).
         NeighborSolicitation ns;
@@ -4527,8 +4771,6 @@
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION, enabled = false)
-    @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION, enabled = true)
     @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = false)
     public void testIpReachabilityMonitor_ignoreIpv6DefaultRouter_everReachable() throws Exception {
         runIpReachabilityMonitorEverReachableIpv6NeighborTest(IPV6_OFF_LINK_DNS_SERVER,
@@ -4536,8 +4778,6 @@
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION, enabled = true)
-    @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION, enabled = false)
     @Flag(name = IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, enabled = false)
     public void testIpReachabilityMonitor_ignoreIpv6Dns_everReachable() throws Exception {
         runIpReachabilityMonitorEverReachableIpv6NeighborTest(IPV6_ON_LINK_DNS_SERVER,
@@ -4545,14 +4785,14 @@
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = true)
     public void testIpReachabilityMonitor_ignoreNeverReachableIpv6Dns() throws Exception {
         runIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER,
-                ipv6Addr(IPV6_ON_LINK_DNS_SERVER), false /* expectNeighborLost */);
+                ipv6Addr(IPV6_ON_LINK_DNS_SERVER),
+                false /* shouldMakeNeighborReachableFirst */,
+                false /* expectNeighborLost */);
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = true)
     public void testIpReachabilityMonitor_ignoreNeverReachableIpv6Dns_butEverReachable()
             throws Exception {
         runIpReachabilityMonitorEverReachableIpv6NeighborTest(IPV6_ON_LINK_DNS_SERVER,
@@ -4560,14 +4800,14 @@
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = true)
     public void testIpReachabilityMonitor_ignoreNeverReachableIpv6DefaultRouter() throws Exception {
         runIpReachabilityMonitorAddressResolutionTest(IPV6_OFF_LINK_DNS_SERVER,
-                ROUTER_LINK_LOCAL, false /* expectNeighborLost */);
+                ROUTER_LINK_LOCAL,
+                false /* shouldMakeNeighborReachableFirst */,
+                false /* expectNeighborLost */);
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = true)
     public void testIpReachabilityMonitor_ignoreNeverReachableIpv6DefaultRouter_butEverReachable()
             throws Exception {
         runIpReachabilityMonitorEverReachableIpv6NeighborTest(IPV6_ON_LINK_DNS_SERVER,
@@ -4848,6 +5088,34 @@
         }
     }
 
+    @Test
+    public void testKernelDeletesIPv6AddressesOnValidLifetimeExpires() throws Exception {
+        ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
+                .withoutIPv4()
+                .build();
+        startIpClientProvisioning(config);
+
+        // Intend to set the same preferred and valid lifetime in RA PIO to 3s. All global IPv6
+        // addresses will be deleted from the interface when the valid lifetime expires, this might
+        // be caused by the loss of RA due to the DTIM config. Then a onProvisioningFailure event
+        // will be triggered if that's the IPv6-only network.
+        final ByteBuffer ra = buildRaPacket(TEST_IPV6_PREFIX, IPV6_ON_LINK_DNS_SERVER,
+                3 /* validLifetime */, 3 /* preferredLifetime */, 600 /* dnsLifetime */,
+                true /* shouldIncludeSlla */);
+        doIpv6OnlyProvisioning(null /* inOrder */, ra);
+
+        final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
+        verify(mCb, timeout(PACKET_TIMEOUT_MS)).onProvisioningFailure(captor.capture());
+        final LinkProperties lp = captor.getValue();
+        assertNotNull(lp);
+        assertFalse(lp.hasGlobalIpv6Address());
+        assertEquals(1, lp.getLinkAddresses().size()); // only IPv6 Link-local address
+        // because the DNS server is on-link, if off-link, due to the loss of IPv6 address, off-link
+        // DNS dest will be removed from LP as well.
+        assertTrue(lp.hasIpv6DnsServer());
+        assertTrue(lp.hasIpv6DefaultRoute());
+    }
+
     @Test @SignatureRequiredTest(reason = "requires mNetd to delete IPv6 GUAs")
     public void testOnIpv6AddressRemoved() throws Exception {
         ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
@@ -4858,12 +5126,25 @@
         LinkProperties lp = doIpv6OnlyProvisioning();
         assertNotNull(lp);
         assertEquals(3, lp.getLinkAddresses().size()); // IPv6 privacy, stable privacy, link-local
-        for (LinkAddress la : lp.getLinkAddresses()) {
-            final Inet6Address address = (Inet6Address) la.getAddress();
-            if (address.isLinkLocalAddress()) continue;
-            // Remove IPv6 GUAs from interface.
-            mNetd.interfaceDelAddress(mIfaceName, address.getHostAddress(), la.getPrefixLength());
-        }
+
+        final LinkAddress privacyAddress =
+                IpClient.find(lp.getLinkAddresses(), this::isPrivacyAddress);
+        final LinkAddress stableAddress =
+                IpClient.find(lp.getLinkAddresses(), this::isStablePrivacyAddress);
+        assertNotNull(privacyAddress);
+        assertNotNull(stableAddress);
+
+        // Delete the temporary privacy address before deleting the stable privacy address.
+        // Otherwise, deleting the stable privacy address will also delete the associated
+        // temporary privacy address. If this happens first, then deleting the non-existent
+        // temporary privacy address will throw an EADDRNOTAVAIL error.
+        // TODO: send RTM_DELADDR instead of Netd API.
+        mNetd.interfaceDelAddress(mIfaceName,
+                ((Inet6Address) privacyAddress.getAddress()).getHostAddress(),
+                privacyAddress.getPrefixLength());
+        mNetd.interfaceDelAddress(mIfaceName,
+                ((Inet6Address) stableAddress.getAddress()).getHostAddress(),
+                stableAddress.getPrefixLength());
 
         final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class);
         verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningFailure(captor.capture());
@@ -4893,7 +5174,6 @@
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
     public void testMaxDtimMultiplier_IPv6LinkLocalOnlyMode() throws Exception {
-        final InOrder inOrder = inOrder(mCb);
         ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                 .withoutIPv4()
                 .withIpv6LinkLocalOnly()
@@ -6035,7 +6315,6 @@
 
     @Test
     @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = true)
-    @SignatureRequiredTest(reason = "need to delete cluster from real db in tearDown")
     public void testIgnoreNudFailuresIfTooManyInPastDay() throws Exception {
         // // NUD failure event count exceeds daily threshold nor weekly.
         final long when = System.currentTimeMillis() - ONE_DAY_IN_MS / 2; // 12h ago
@@ -6047,9 +6326,7 @@
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = false)
     @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = false)
-    @SignatureRequiredTest(reason = "need to delete cluster from real db in tearDown")
     public void testIgnoreNudFailuresIfTooManyInPastDay_flagOff() throws Exception {
         // NUD failure event count exceeds daily threshold nor weekly.
         final long when = System.currentTimeMillis() - ONE_DAY_IN_MS / 2; // 12h ago
@@ -6062,9 +6339,7 @@
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = false)
     @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = true)
-    @SignatureRequiredTest(reason = "need to delete cluster from real db in tearDown")
     public void testIgnoreNudFailuresIfTooManyInPastDay_notUpToThreshold()
             throws Exception {
         // NUD failure event count doesn't exceed either weekly threshold nor daily.
@@ -6079,7 +6354,6 @@
 
     @Test
     @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = true)
-    @SignatureRequiredTest(reason = "need to delete cluster from real db in tearDown")
     public void testIgnoreNudFailuresIfTooManyInPastWeek() throws Exception {
         // NUD failure event count exceeds the weekly threshold, but not daily threshold in the past
         // day.
@@ -6096,9 +6370,7 @@
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = false)
     @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = false)
-    @SignatureRequiredTest(reason = "need to delete cluster from real db in tearDown")
     public void testIgnoreNudFailuresIfTooManyInPastWeek_flagOff() throws Exception {
         // NUD failure event count exceeds the weekly threshold, but not daily threshold in the past
         // day.
@@ -6117,7 +6389,6 @@
 
     @Test
     @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = true)
-    @SignatureRequiredTest(reason = "need to delete cluster from real db in tearDown")
     public void testIgnoreNudFailuresIfTooManyInPastWeek_notUpToThreshold() throws Exception {
         // NUD failure event count doesn't exceed either weekly threshold nor daily.
         long when = System.currentTimeMillis() - ONE_WEEK_IN_MS / 2; // half a week ago
@@ -6133,26 +6404,41 @@
                 NudEventType.NUD_POST_ROAMING_FAILED_CRITICAL);
     }
 
+    private void assertRetrievedNetworkEventCount(String cluster, int expectedCountInPastWeek,
+            int expectedCountInPastDay, int expectedCountInPastSixHours) {
+        final long now = System.currentTimeMillis();
+        final long[] sinceTimes = new long[3];
+        sinceTimes[0] = now - ONE_WEEK_IN_MS;
+        sinceTimes[1] = now - ONE_DAY_IN_MS;
+        sinceTimes[2] = now - SIX_HOURS_IN_MS;
+        final int[] counts = getStoredNetworkEventCount(cluster, sinceTimes,
+                NETWORK_EVENT_NUD_FAILURE_TYPES, TEST_TIMEOUT_MS);
+        assertEquals(expectedCountInPastWeek, counts[0]);
+        assertEquals(expectedCountInPastDay, counts[1]);
+        assertEquals(expectedCountInPastSixHours, counts[2]);
+    }
+
     @Test
     @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = true)
-    @SignatureRequiredTest(reason = "need to delete cluster from real db in tearDown")
     public void testIgnoreNudFailuresIfTooManyInPastWeek_stopWritingEvent() throws Exception {
         long when = (long) (System.currentTimeMillis() - SIX_HOURS_IN_MS * 0.9);
         long expiry = when + ONE_WEEK_IN_MS;
         storeNudFailureEvents(when, expiry, 10, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC);
 
+        // Expect that a NUD failure happens, but onReachabilityFailure callback won't be called due
+        // to the experiment flag is enabled and this event won't be written to db because the
+        // number of events has been up to the threshold, then the retrieved event count should
+        // still be 10.
         runIpReachabilityMonitorAddressResolutionTest(IPV6_OFF_LINK_DNS_SERVER,
                 ROUTER_LINK_LOCAL /* targetIp */,
+                true /* shouldMakeNeighborReachableFirst */,
                 false /* expectNeighborLost */);
-        verify(mIpMemoryStore, never()).storeNetworkEvent(any(), anyLong(), anyLong(),
-                eq(IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC), any());
+        assertRetrievedNetworkEventCount(TEST_CLUSTER, 10 /* expectedCountInPastWeek */,
+                10 /* expectedCountInPastDay */, 10 /* expectedCountInPastSixHours */);
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION, enabled = false)
-    @Flag(name = IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION, enabled = false)
     @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = true)
-    @SignatureRequiredTest(reason = "need to delete cluster from real db in tearDown")
     public void testIgnoreNudFailuresStopWritingEvents() throws Exception {
         // Add enough failures that NUD failures are ignored.
         long when = (long) (System.currentTimeMillis() - SIX_HOURS_IN_MS * 1.1);
@@ -6165,44 +6451,47 @@
         storeNudFailureEvents(when, expiry, 9, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC);
 
         prepareIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER,
-                ROUTER_LINK_LOCAL);
+                ROUTER_LINK_LOCAL, true /* shouldMakeNeighborReachableFirst */);
 
         // The first new failure is ignored and written to the database.
-        // The total is 10 failures in the last 6 hours.
-        sendPacketToUnreachableNeighbor(ipv6Addr(IPV6_OFF_LINK_DNS_SERVER));
-        expectAndDropMulticastNses(ROUTER_LINK_LOCAL, false /* expectNeighborLost */);
-        verify(mIpMemoryStore).storeNetworkEvent(any(), anyLong(), anyLong(),
-                eq(IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC), any());
+        // The total is 10 failures in the last 6 hours, and 20 failures
+        // in the past week and day.
+        sendPacketToPeer(ipv6Addr(IPV6_OFF_LINK_DNS_SERVER));
+        expectAndDropMultipleNses(ROUTER_LINK_LOCAL, false /* expectNeighborLost */);
+        assertRetrievedNetworkEventCount(TEST_CLUSTER, 20 /* expectedCountInPastWeek */,
+                20 /* expectedCountInPastDay */, 10 /* expectedCountInPastSixHours */);
 
         // The second new failure is ignored, but not written.
-        reset(mIpMemoryStore);
-        sendPacketToUnreachableNeighbor(ipv6Addr(IPV6_ON_LINK_DNS_SERVER));
-        expectAndDropMulticastNses(ipv6Addr(IPV6_ON_LINK_DNS_SERVER),
+        sendPacketToPeer(ipv6Addr(IPV6_ON_LINK_DNS_SERVER));
+        expectAndDropMultipleNses(ipv6Addr(IPV6_ON_LINK_DNS_SERVER),
                 false /* expectNeighborLost */);
-        verifyNoMoreInteractions(mIpMemoryStore);
+        assertRetrievedNetworkEventCount(TEST_CLUSTER, 20 /* expectedCountInPastWeek */,
+                20 /* expectedCountInPastDay */, 10 /* expectedCountInPastSixHours */);
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = false)
     @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = false)
-    @SignatureRequiredTest(reason = "need to delete cluster from real db in tearDown")
     public void testIgnoreNudFailuresIfTooManyInPastWeek_stopWritingEvent_flagOff()
             throws Exception {
         long when = (long) (System.currentTimeMillis() - SIX_HOURS_IN_MS * 0.9);
         long expiry = when + ONE_WEEK_IN_MS;
-        storeNudFailureEvents(when, expiry, 10, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC);
+        storeNudFailureEvents(when, expiry, 8, IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC);
 
         runIpReachabilityMonitorAddressResolutionTest(IPV6_OFF_LINK_DNS_SERVER,
                 ROUTER_LINK_LOCAL /* targetIp */,
+                true /* shouldMakeNeighborReachableFirst */,
                 true /* expectNeighborLost */);
-        verify(mIpMemoryStore, never()).storeNetworkEvent(any(), anyLong(), anyLong(),
-                eq(IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC), any());
+
+        // Although the total NUD failure events count in the past 6 hours hasn't been up to the
+        // threshold, however, the experiment flag is disabled, therefore, the new NUD failure
+        // event will not be written to db, then the retrieved event count should still be 8 rather
+        // than 9.
+        assertRetrievedNetworkEventCount(TEST_CLUSTER, 8 /* expectedCountInPastWeek */,
+                8 /* expectedCountInPastDay */, 8 /* expectedCountInPastSixHours */);
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = false)
     @Flag(name = IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION, enabled = true)
-    @SignatureRequiredTest(reason = "need to delete cluster from real db in tearDown")
     public void testIgnoreNudFailuresIfTooManyInPastWeek_stopWritingEvent_notUpToThreshold()
             throws Exception {
         long when = (long) (System.currentTimeMillis() - SIX_HOURS_IN_MS * 0.9);
@@ -6211,10 +6500,15 @@
 
         runIpReachabilityMonitorAddressResolutionTest(IPV6_OFF_LINK_DNS_SERVER,
                 ROUTER_LINK_LOCAL /* targetIp */,
+                true /* shouldMakeNeighborReachableFirst */,
                 true /* expectNeighborLost */);
         assertNotifyNeighborLost(ROUTER_LINK_LOCAL /* targetIp */,
                 NudEventType.NUD_ORGANIC_FAILED_CRITICAL);
-        verify(mIpMemoryStore).storeNetworkEvent(any(), anyLong(), anyLong(),
-                eq(IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC), any());
+
+        // Given that total NUD failure event counts in the past 6 hours doesn't exceed the
+        // threshold yet, the new NUD failure event will be written to db, then the retrieved
+        // event count should be 10.
+        assertRetrievedNetworkEventCount(TEST_CLUSTER, 10 /* expectedCountInPastWeek */,
+                10 /* expectedCountInPastDay */, 10 /* expectedCountInPastSixHours */);
     }
 }
diff --git a/tests/integration/root/android/net/ip/IpClientRootTest.kt b/tests/integration/root/android/net/ip/IpClientRootTest.kt
index 3a56139..b0b51fa 100644
--- a/tests/integration/root/android/net/ip/IpClientRootTest.kt
+++ b/tests/integration/root/android/net/ip/IpClientRootTest.kt
@@ -19,6 +19,7 @@
 import android.Manifest.permission.INTERACT_ACROSS_USERS_FULL
 import android.Manifest.permission.NETWORK_SETTINGS
 import android.Manifest.permission.READ_DEVICE_CONFIG
+import android.Manifest.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG
 import android.Manifest.permission.WRITE_DEVICE_CONFIG
 import android.net.IIpMemoryStore
 import android.net.IIpMemoryStoreCallbacks
@@ -163,6 +164,7 @@
         // Delete the IpMemoryStore entry corresponding to TEST_L2KEY, make sure each test starts
         // from a clean state.
         mStore.delete(TEST_L2KEY, true) { _, _ -> latch.countDown() }
+        mStore.deleteCluster(TEST_CLUSTER, true) { _, _ -> latch.countDown() }
         assertTrue(latch.await(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS))
     }
 
@@ -191,7 +193,11 @@
     private val mOriginalPropertyValues = ArrayMap<String, String>()
 
     override fun setDeviceConfigProperty(name: String?, value: String?) {
-        automation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG)
+        automation.adoptShellPermissionIdentity(
+            READ_DEVICE_CONFIG,
+            WRITE_DEVICE_CONFIG,
+            WRITE_ALLOWLISTED_DEVICE_CONFIG
+        )
         try {
             // Do not use computeIfAbsent as it would overwrite null values,
             // property originally unset.
@@ -214,7 +220,11 @@
     @After
     fun tearDownDeviceConfigProperties() {
         if (testSkipped()) return
-        automation.adoptShellPermissionIdentity(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG)
+        automation.adoptShellPermissionIdentity(
+            READ_DEVICE_CONFIG,
+            WRITE_DEVICE_CONFIG,
+            WRITE_ALLOWLISTED_DEVICE_CONFIG
+        )
         try {
             for (key in mOriginalPropertyValues.keys) {
                 if (key == null) continue
diff --git a/tests/integration/signature/android/net/util/NetworkStackUtilsIntegrationTest.kt b/tests/integration/signature/android/net/util/NetworkStackUtilsIntegrationTest.kt
index 29e6237..ea6f35a 100644
--- a/tests/integration/signature/android/net/util/NetworkStackUtilsIntegrationTest.kt
+++ b/tests/integration/signature/android/net/util/NetworkStackUtilsIntegrationTest.kt
@@ -20,6 +20,8 @@
 import android.content.Context
 import android.net.InetAddresses.parseNumericAddress
 import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
 import android.net.MacAddress
 import android.net.TestNetworkInterface
 import android.net.TestNetworkManager
@@ -30,8 +32,12 @@
 import android.system.OsConstants
 import android.system.OsConstants.AF_INET
 import android.system.OsConstants.AF_PACKET
+import android.system.OsConstants.ETH_P_ALL
 import android.system.OsConstants.ETH_P_IPV6
+import android.system.OsConstants.IFA_F_DEPRECATED
+import android.system.OsConstants.IFA_F_TENTATIVE
 import android.system.OsConstants.IPPROTO_UDP
+import android.system.OsConstants.RT_SCOPE_LINK
 import android.system.OsConstants.SOCK_CLOEXEC
 import android.system.OsConstants.SOCK_DGRAM
 import android.system.OsConstants.SOCK_NONBLOCK
@@ -46,6 +52,7 @@
 import com.android.net.module.util.Ipv6Utils
 import com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN
 import com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.ETHER_SRC_ADDR_OFFSET
 import com.android.net.module.util.NetworkStackConstants.IPV4_ADDR_ANY
 import com.android.net.module.util.NetworkStackConstants.IPV4_CHECKSUM_OFFSET
 import com.android.net.module.util.NetworkStackConstants.IPV4_FLAGS_OFFSET
@@ -62,6 +69,10 @@
 import java.io.FileDescriptor
 import java.net.Inet4Address
 import java.net.Inet6Address
+import java.net.InetAddress
+import java.net.InetSocketAddress
+import java.net.MulticastSocket
+import java.net.NetworkInterface
 import java.nio.ByteBuffer
 import java.util.Arrays
 import kotlin.reflect.KClass
@@ -92,7 +103,8 @@
     private val SOLICITED_NODE_MULTICAST_PREFIX = "FF02:0:0:0:0:1:FF00::/104"
 
     private val readerHandler = HandlerThread(
-            NetworkStackUtilsIntegrationTest::class.java.simpleName)
+            NetworkStackUtilsIntegrationTest::class.java.simpleName
+    )
     private lateinit var iface: TestNetworkInterface
     private lateinit var reader: PollPacketReader
 
@@ -122,8 +134,12 @@
         val socket = Os.socket(AF_INET, SOCK_DGRAM or SOCK_NONBLOCK, IPPROTO_UDP)
         SocketUtils.bindSocketToInterface(socket, iface.interfaceName)
 
-        NetworkStackUtils.addArpEntry(TEST_TARGET_IPV4_ADDR, TEST_TARGET_MAC, iface.interfaceName,
-                socket)
+        NetworkStackUtils.addArpEntry(
+            TEST_TARGET_IPV4_ADDR,
+            TEST_TARGET_MAC,
+            iface.interfaceName,
+            socket
+        )
 
         // Fake DHCP packet: would not be usable as a DHCP offer (most IPv4 addresses are all-zero,
         // no gateway or DNS servers, etc).
@@ -141,8 +157,15 @@
         // Not using .array as per errorprone "ByteBufferBackingArray" recommendation
         val originalPacket = buffer.readAsArray()
 
-        Os.sendto(socket, originalPacket, 0 /* bytesOffset */, originalPacket.size /* bytesCount */,
-                0 /* flags */, TEST_TARGET_IPV4_ADDR, DhcpPacket.DHCP_CLIENT.toInt() /* port */)
+        Os.sendto(
+            socket,
+            originalPacket,
+            0 /* bytesOffset */,
+            originalPacket.size /* bytesCount */,
+            0 /* flags */,
+            TEST_TARGET_IPV4_ADDR,
+            DhcpPacket.DHCP_CLIENT.toInt() /* port */
+        )
 
         // Verify the packet was sent to the mac address specified in the ARP entry
         // Also accept ARP requests, but expect that none is sent before the UDP packet
@@ -154,7 +177,9 @@
         assertEquals(TEST_TARGET_MAC, sentTargetAddr, "Destination ethernet address does not match")
 
         val sentDhcpPacket = sentPacket.copyOfRange(
-                ETHER_HEADER_LEN + IPV4_HEADER_MIN_LEN + UDP_HEADER_LEN, sentPacket.size)
+            ETHER_HEADER_LEN + IPV4_HEADER_MIN_LEN + UDP_HEADER_LEN,
+            sentPacket.size
+        )
 
         assertArrayEquals("Sent packet != original packet", originalPacket, sentDhcpPacket)
     }
@@ -165,12 +190,20 @@
                 ?: fail("Could not obtain interface params for ${iface.interfaceName}")
         val socketAddr = SocketUtils.makePacketSocketAddress(ETH_P_IPV6, ifParams.index)
         Os.bind(socket, socketAddr)
-        Os.setsockoptTimeval(socket, SOL_SOCKET, SO_RCVTIMEO,
-                StructTimeval.fromMillis(TEST_TIMEOUT_MS))
+        Os.setsockoptTimeval(
+            socket,
+            SOL_SOCKET,
+            SO_RCVTIMEO,
+            StructTimeval.fromMillis(TEST_TIMEOUT_MS)
+        )
 
         // Verify that before setting any filter, the socket receives pings
-        val echo = Ipv6Utils.buildEchoRequestPacket(TEST_SRC_MAC, TEST_TARGET_MAC, TEST_INET6ADDR_1,
-                TEST_INET6ADDR_2)
+        val echo = Ipv6Utils.buildEchoRequestPacket(
+            TEST_SRC_MAC,
+            TEST_TARGET_MAC,
+            TEST_INET6ADDR_1,
+            TEST_INET6ADDR_2
+        )
         reader.sendResponse(echo)
         echo.rewind()
         assertNextPacketEquals(socket, echo.readAsArray(), "ICMPv6 echo")
@@ -183,8 +216,12 @@
         // Send another echo, then an RA. After setting the filter expect only the RA.
         echo.rewind()
         reader.sendResponse(echo)
-        val pio = PrefixInformationOption.build(IpPrefix("2001:db8:1::/64"),
-                0.toByte() /* flags */, 3600 /* validLifetime */, 1800 /* preferredLifetime */)
+        val pio = PrefixInformationOption.build(
+            IpPrefix("2001:db8:1::/64"),
+            0.toByte() /* flags */,
+            3600 /* validLifetime */,
+            1800 /* preferredLifetime */
+        )
         val ra = Ipv6Utils.buildRaPacket(TEST_SRC_MAC, TEST_TARGET_MAC,
                 TEST_INET6ADDR_1 /* routerAddr */, IPV6_ADDR_ALL_NODES_MULTICAST,
                 0.toByte() /* flags */, 1800 /* lifetime */, 0 /* reachableTime */,
@@ -205,13 +242,340 @@
         doTestAttachRaFilter(true)
     }
 
+    @Test
+    fun testAttachEgressIgmpReportFilter() {
+        val socket = Os.socket(AF_PACKET, SOCK_RAW or SOCK_CLOEXEC, 0)
+        val ifParams = InterfaceParams.getByName(iface.interfaceName)
+            ?: fail("Could not obtain interface params for ${iface.interfaceName}")
+        val socketAddr = SocketUtils.makePacketSocketAddress(ETH_P_ALL, ifParams.index)
+        NetworkStackUtils.attachEgressIgmpReportFilter(socket)
+        Os.bind(socket, socketAddr)
+        Os.setsockoptTimeval(
+            socket,
+            SOL_SOCKET,
+            SO_RCVTIMEO,
+            StructTimeval.fromMillis(TEST_TIMEOUT_MS)
+        )
+
+        val sendSocket = Os.socket(AF_PACKET, SOCK_RAW or SOCK_CLOEXEC, 0)
+        Os.bind(sendSocket, socketAddr)
+
+        testExpectedPacketsReceived(sendSocket, socket)
+
+        // shorten the socket timeout to prevent waiting too long in the test
+        Os.setsockoptTimeval(socket, SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(100))
+
+        testExpectedPacketsNotReceived(sendSocket, socket)
+    }
+
+    @Test
+    fun testAttachEgressIgmpReportFilterForMulticastGroupChange() {
+        val socket = Os.socket(AF_PACKET, SOCK_RAW or SOCK_CLOEXEC, 0)
+        val ifParams = InterfaceParams.getByName(iface.interfaceName)
+            ?: fail("Could not obtain interface params for ${iface.interfaceName}")
+        val socketAddr = SocketUtils.makePacketSocketAddress(ETH_P_ALL, ifParams.index)
+        NetworkStackUtils.attachEgressIgmpReportFilter(socket)
+        Os.bind(socket, socketAddr)
+        Os.setsockoptTimeval(
+            socket,
+            SOL_SOCKET,
+            SO_RCVTIMEO,
+            StructTimeval.fromMillis(TEST_TIMEOUT_MS)
+        )
+
+        val multicastSock = MulticastSocket()
+        val mcastAddr = InetSocketAddress(InetAddress.getByName("239.0.0.1") as Inet4Address, 5000)
+        val networkInterface = NetworkInterface.getByName(iface.interfaceName)
+
+        multicastSock.joinGroup(mcastAddr, networkInterface)
+        // Using scapy to generate IGMPv3 membership report:
+        // ether = Ether(src='02:03:04:05:06:07', dst='01:00:5e:00:00:16')
+        // ip = IP(src='0.0.0.0', dst='224.0.0.22', id=0, flags='DF', options=[IPOption_Router_Alert()])
+        // igmp = IGMPv3(type=0x22)/IGMPv3mr(records=[IGMPv3gr(rtype=4, maddr='239.0.0.1')])
+        // pkt = ether/ip/igmp
+        val joinReport = """
+            01005e000016020304050607080046c0002800004000010203fa00000000e0000016940400002200ea
+            fc0000000104000000ef000001
+        """.replace("\\s+".toRegex(), "").trim()
+        val srcMac = ifParams.macAddr.toString().replace(":", "")
+        val expectedJoinPkt = HexDump.hexStringToByteArray(
+            joinReport.replace("020304050607", srcMac)
+        )
+        assertNextPacketEquals(socket, expectedJoinPkt, "IGMPv3 join report")
+
+        multicastSock.leaveGroup(mcastAddr, networkInterface)
+        // Using scapy to generate IGMPv3 membership report:
+        // ether = Ether(src='02:03:04:05:06:07', dst='01:00:5e:00:00:16')
+        // ip = IP(src='0.0.0.0', dst='224.0.0.22', id=0, flags='DF', options=[IPOption_Router_Alert()])
+        // igmp = IGMPv3(type=0x22)/IGMPv3mr(records=[IGMPv3gr(rtype=3, maddr='239.0.0.1')])
+        // pkt = ether/ip/igmp
+        val leaveReport = """
+            01005e000016020304050607080046c0002800004000010203fa00000000e0000016940400002200eb
+            fc0000000103000000ef000001
+        """.replace("\\s+".toRegex(), "").trim()
+        val expectedLeavePkt = HexDump.hexStringToByteArray(
+            leaveReport.replace("020304050607", srcMac)
+        )
+        assertNextPacketEquals(socket, expectedLeavePkt, "IGMPv3 leave report")
+    }
+
+    @Test
+    fun testAttachEgressMulticastReportFilter() {
+        val socket = Os.socket(AF_PACKET, SOCK_RAW or SOCK_CLOEXEC, 0)
+        val ifParams = InterfaceParams.getByName(iface.interfaceName)
+            ?: fail("Could not obtain interface params for ${iface.interfaceName}")
+        val socketAddr = SocketUtils.makePacketSocketAddress(ETH_P_ALL, ifParams.index)
+        NetworkStackUtils.attachEgressMulticastReportFilter(socket)
+        Os.bind(socket, socketAddr)
+        Os.setsockoptTimeval(
+            socket,
+            SOL_SOCKET,
+            SO_RCVTIMEO,
+            StructTimeval.fromMillis(TEST_TIMEOUT_MS)
+        )
+
+        val sendSocket = Os.socket(AF_PACKET, SOCK_RAW or SOCK_CLOEXEC, 0)
+        Os.bind(sendSocket, socketAddr)
+
+        testExpectedPacketsReceived(sendSocket, socket)
+
+        // Using scapy to generate MLDv1 membership report:
+        // ether = Ether(src='02:03:04:05:06:07', dst='33:33:33:11:11:11')
+        // ipv6 = IPv6(src='fe80::fc01:83ff:fea6:378b', dst='ff12::1:1111:1111', hlim=1)
+        // option = IPv6ExtHdrHopByHop(options=[RouterAlert(otype=5)])
+        // mld = ICMPv6MLReport(type=131, mladdr='ff12::1:1111:1111')
+        // pkt = ether/ipv6/option/mld
+        val mldv1ReportHexStr = """
+            33333311111102030405060786dd6000000000200001fe80000000000000fc0183fffea6378bff12000000
+            00000000000001111111113a000502000001008300858c00000000ff120000000000000000000111111111
+        """.replace("\\s+".toRegex(), "").trim()
+        val mldv1Report = HexDump.hexStringToByteArray(mldv1ReportHexStr)
+        Os.write(sendSocket, mldv1Report, 0, mldv1Report.size)
+        assertUntilPacketEquals(socket, mldv1Report, "MLDv1 report")
+
+        // Using scapy to generate MLDv1 membership done:
+        // ether = Ether(src='02:03:04:05:06:07', dst='33:33:33:00:00:02')
+        // ipv6 = IPv6(src='fe80::fc01:83ff:fea6:378b', dst='ff02::2', hlim=1)
+        // option = IPv6ExtHdrHopByHop(options=[RouterAlert(otype=5)])
+        // mld = ICMPv6MLReport(type=132, mladdr='ff12::1:1111:1111')
+        // pkt = ether/ipv6/option/mld
+        val mldv1DoneHexStr = """
+            33333300000202030405060786dd6000000000200001fe80000000000000fc0183fffea6378bff02000000
+            00000000000000000000023a000502000001008400a6bd00000000ff120000000000000000000111111111
+        """.replace("\\s+".toRegex(), "").trim()
+        val mldv1Done = HexDump.hexStringToByteArray(mldv1DoneHexStr)
+        Os.write(sendSocket, mldv1Done, 0, mldv1Done.size)
+        assertUntilPacketEquals(socket, mldv1Done, "MLDv1 done")
+
+        // Using scapy to generate MLDv2 membership report:
+        // ether = Ether(src='02:03:04:05:06:07', dst='33:33:33:00:00:16')
+        // ipv6 = IPv6(src='fe80::fc01:83ff:fea6:378b', dst='ff02::16', hlim=1)
+        // option = IPv6ExtHdrHopByHop(options=[RouterAlert(otype=5)])
+        // mld = ICMPv6MLReport2(records=[ICMPv6MLDMultAddrRec(dst='ff12::1:1111:1111')])
+        // pkt = ether/ipv6/option/mld
+        val mldv2ReportHexStr = """
+            33333300001602030405060786dd6000000000240001fe80000000000000fc0183fffea6378bff02000000
+            00000000000000000000163a000502000001008f0097a40000000104000000ff1200000000000000000001
+        11111111
+        """.replace("\\s+".toRegex(), "").trim()
+        val mldv2Report = HexDump.hexStringToByteArray(mldv2ReportHexStr)
+        Os.write(sendSocket, mldv2Report, 0, mldv2Report.size)
+        assertUntilPacketEquals(socket, mldv2Report, "MLDv2 report")
+
+        // shorten the socket timeout to prevent waiting too long in the test
+        Os.setsockoptTimeval(socket, SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(100))
+
+        testExpectedPacketsNotReceived(sendSocket, socket)
+
+        // Using scapy to generate MLDv1 general query packet:
+        //   ether = Ether(src='02:03:04:05:06:07', dst='33:33:33:00:00:01')
+        //   ipv6 = IPv6(src='fe80::fc01:83ff:fea6:378b', dst='ff02::1', hlim=1)
+        //   option = IPv6ExtHdrHopByHop(options=[RouterAlert(otype=5)])
+        //   mld = ICMPv6MLQuery()
+        //   pkt = ether/ipv6/option/mld
+        val mldv1GqHexStr = """
+            33333300000102030405060786dd6000000000200001fe80000000000000fc0183fffea6378bff02000000
+            00000000000000000000013a000502000001008200a2e42710000000000000000000000000000000000000
+        """.replace("\\s+".toRegex(), "").trim()
+        val mldv1Gq = HexDump.hexStringToByteArray(mldv1GqHexStr)
+        Os.write(sendSocket, mldv1Gq, 0, mldv1Gq.size)
+        assertUntilSocketReadErrno(
+            "MLDv1 General Query Packet should not been received",
+            socket,
+            OsConstants.EAGAIN
+        )
+
+        // Using scapy to generate MLDv2 general query packet:
+        //   ether = Ether(src='02:03:04:05:06:07', dst='33:33:33:00:00:01')
+        //   ipv6 = IPv6(src='fe80::fc01:83ff:fea6:378b', dst='ff02::1', hlim=1)
+        //   option = IPv6ExtHdrHopByHop(options=[RouterAlert(otype=5)])
+        //   mld = ICMPv6MLQuery2()
+        //   pkt = ether/ipv6/option/mld
+        val mldv2GqHexStr = """
+            33333300000102030405060786dd6000000000240001fe80000000000000fc0183fffea6378bff02000000
+            00000000000000000000013a000502000001008200a2e02710000000000000000000000000000000000000
+            00000000
+        """.replace("\\s+".toRegex(), "").trim()
+        val mldv2Gq = HexDump.hexStringToByteArray(mldv2GqHexStr)
+        Os.write(sendSocket, mldv2Gq, 0, mldv1Gq.size)
+        assertUntilSocketReadErrno(
+            "MLDv2 General Query Packet should not been received",
+            socket,
+            OsConstants.EAGAIN
+        )
+    }
+
+    private fun testExpectedPacketsReceived(
+        sendSocket: FileDescriptor,
+        recvSocket: FileDescriptor
+    ) {
+        // Using scapy to generate IGMPv2 membership report:
+        // ether = Ether(src='02:03:04:05:06:07', dst='01:00:5e:00:00:01')
+        // ip = IP(src='10.0.0.1', dst='239.0.0.1', id=0, flags='DF', tos=0xc0, options=[IPOption_Router_Alert()])
+        // igmp = IGMP(type=0x16, mrcode=0, gaddr='239.0.0.1')
+        // pkt = ether/ip/igmp
+        val igmpv2ReportHexStr = """
+            01005e000001020304050607080046c00020000040000102eb150a000001ef000001940400001600fafd
+            ef000001
+        """.replace("\\s+".toRegex(), "").trim()
+        val igmpv2Report = HexDump.hexStringToByteArray(igmpv2ReportHexStr)
+        Os.write(sendSocket, igmpv2Report, 0, igmpv2Report.size)
+        assertUntilPacketEquals(recvSocket, igmpv2Report, "IGMPv2 report")
+
+        // Using scapy to generate IGMPv2 membership leave report:
+        // ether = Ether(src='02:03:04:05:06:07', dst='01:00:5e:00:00:01')
+        // ip = IP(src='10.0.0.1', dst='239.0.0.1', id=0, flags='DF', tos=0xc0, options=[IPOption_Router_Alert()])
+        // igmp = IGMP(type=0x17, mrcode=0, gaddr='239.0.0.1')
+        // pkt = ether/ip/igmp
+        val igmpv2LeaveHexStr = """
+            01005e000001020304050607080046c00020000040000102eb150a000001ef000001940400001700f9fd
+            ef000001
+        """.replace("\\s+".toRegex(), "").trim()
+        val igmpv2Leave = HexDump.hexStringToByteArray(igmpv2LeaveHexStr)
+        Os.write(sendSocket, igmpv2Leave, 0, igmpv2Leave.size)
+        assertUntilPacketEquals(recvSocket, igmpv2Leave, "IGMPv2 leave")
+
+        // Using scapy to generate IGMPv3 membership report:
+        // ether = Ether(src='02:03:04:05:06:07', dst='01:00:5e:00:00:16')
+        // ip = IP(src='10.0.0.1', dst='224.0.0.22', id=0, flags='DF', options=[IPOption_Router_Alert()])
+        // igmp = IGMPv3(type=0x22)/IGMPv3mr(records=[IGMPv3gr(rtype=2, maddr='239.0.0.1')])
+        // pkt = ether/ip/igmp
+        val igmpv3ReportHexStr = """
+            01005e000016020304050607080046c00028000040000102f9f80a000001e0000016940400002200ecfc
+            0000000102000000ef000001
+        """.replace("\\s+".toRegex(), "").trim()
+        val igmpv3Report = HexDump.hexStringToByteArray(igmpv3ReportHexStr)
+        Os.write(sendSocket, igmpv3Report, 0, igmpv3Report.size)
+        assertUntilPacketEquals(recvSocket, igmpv3Report, "IGMPv3 report")
+    }
+
+    private fun testExpectedPacketsNotReceived(
+        sendSocket: FileDescriptor,
+        recvSocket: FileDescriptor
+    ) {
+        val dhcpNak = DhcpPacket.buildNakPacket(
+            DhcpPacket.ENCAP_L2,
+            42,
+            TEST_TARGET_IPV4_ADDR, /*relayIp=*/
+            IPV4_ADDR_ANY,
+            TEST_TARGET_MAC.toByteArray(),
+            /*broadcast=*/
+            false,
+            "NAK"
+        ).readAsArray()
+        Os.write(sendSocket, dhcpNak, 0, dhcpNak.size)
+        assertUntilSocketReadErrno(
+            "DHCP Packet should not been received",
+            recvSocket,
+            OsConstants.EAGAIN
+        )
+
+        // Using scapy to generate IGMPv2 general query packet:
+        //   ether = Ether(src='02:03:04:05:06:07', dst='01:00:5e:00:00:01')
+        //   ip = IP(src='10.0.0.1', dst='239.0.0.1', id=0, flags='DF', tos=0xc0, options=[IPOption_Router_Alert()])
+        //   igmp = IGMP(type=0x11)
+        //   pkt = ether/ip/igmp
+        val igmpv2GqHexStr = """
+            01005e000001020304050607080046c00020000040000102eb150a000001ef000001940400001114eeeb
+            00000000
+        """.replace("\\s+".toRegex(), "").trim()
+        val igmpv2Gq = HexDump.hexStringToByteArray(igmpv2GqHexStr)
+        Os.write(sendSocket, igmpv2Gq, 0, igmpv2Gq.size)
+        assertUntilSocketReadErrno(
+            "IGMPv2 General Query Packet should not been received",
+            recvSocket,
+            OsConstants.EAGAIN
+        )
+
+        // Using scapy to generate IGMPv1 general query packet:
+        //   ether = Ether(src='02:03:04:05:06:07', dst='01:00:5e:00:00:01')
+        //   ip = IP(src='10.0.0.1', dst='239.0.0.1', id=0, flags='DF', tos=0xc0, options=[IPOption_Router_Alert()])
+        //   igmp = IGMP(type=0x11, mrcode=0)
+        //   pkt = ether/ip/igmp
+        val igmpv1GqHexStr = """
+            01005e000001020304050607080046c00020000040000102eb150a000001ef000001940400001100eeff
+            00000000
+        """.replace("\\s+".toRegex(), "").trim()
+        val igmpv1Gq = HexDump.hexStringToByteArray(igmpv1GqHexStr)
+        Os.write(sendSocket, igmpv1Gq, 0, igmpv1Gq.size)
+        assertUntilSocketReadErrno(
+            "IGMPv1 General Query Packet should not been received",
+            recvSocket,
+            OsConstants.EAGAIN
+        )
+    }
+
+    private fun assertUntilPacketEquals(
+        socket: FileDescriptor,
+        expected: ByteArray,
+        descr: String
+    ) {
+        val buffer = ByteArray(TEST_MTU)
+        var readBytes: Int
+        var actualPkt: ByteArray? = null
+        while (Os.read(socket, buffer, 0 /* byteOffset */, buffer.size)
+            .also { readBytes = it } > 0
+        ) {
+            actualPkt = buffer.copyOfRange(0, readBytes)
+            if (!isTestInterfaceEgressPacket(actualPkt)) break
+        }
+
+        assertNotNull(actualPkt, "no received packets")
+        assertArrayEquals(
+            "Received packet(${HexDump.toHexString(actualPkt)}) " +
+            "!= expected(${HexDump.toHexString(expected)}) $descr",
+            expected,
+            actualPkt
+        )
+    }
+
+    private fun assertUntilSocketReadErrno(msg: String, socket: FileDescriptor, errno: Int) {
+        val buffer = ByteArray(TEST_MTU)
+        var readBytes: Int
+        var actualPkt: ByteArray? = null
+        try {
+            while (Os.read(socket, buffer, 0 /* byteOffset */, buffer.size)
+                    .also { readBytes = it } > 0
+            ) {
+                actualPkt = buffer.copyOfRange(0, readBytes)
+                if (!isTestInterfaceEgressPacket(actualPkt)) break
+            }
+            fail(msg + ": " + HexDump.toHexString(actualPkt))
+        } catch (expected: ErrnoException) {
+            assertEquals(errno.toLong(), expected.errno.toLong())
+        }
+    }
+
     private fun assertNextPacketEquals(socket: FileDescriptor, expected: ByteArray, descr: String) {
         val buffer = ByteArray(TEST_MTU)
         val readPacket = Os.read(socket, buffer, 0 /* byteOffset */, buffer.size)
         assertTrue(readPacket > 0, "$descr not received")
         assertEquals(expected.size, readPacket, "Received packet size does not match for $descr")
-        assertArrayEquals("Received packet != expected $descr",
-                expected, buffer.copyOfRange(0, readPacket))
+        assertArrayEquals(
+            "Received packet != expected $descr",
+            expected,
+            buffer.copyOfRange(0, readPacket)
+        )
     }
 
     private fun assertSolicitedNodeMulticastAddress(
@@ -223,28 +587,40 @@
         assertTrue(prefix.contains(expected))
         assertTrue(expected.isMulticastAddress())
         // check the last 3 bytes of address
-        assertArrayEquals(Arrays.copyOfRange(expected.getAddress(), 13, 15),
-                Arrays.copyOfRange(unicast.getAddress(), 13, 15))
+        assertArrayEquals(
+            Arrays.copyOfRange(expected.getAddress(), 13, 15),
+            Arrays.copyOfRange(unicast.getAddress(), 13, 15)
+        )
     }
 
     @Test
     fun testConvertIpv6AddressToSolicitedNodeMulticast() {
         val addr1 = NetworkStackUtils.ipv6AddressToSolicitedNodeMulticast(TEST_INET6ADDR_1)
         assertSolicitedNodeMulticastAddress(addr1, TEST_INET6ADDR_1)
+        assertTrue(NetworkStackUtils.isIPv6AddressSolicitedNodeMulticast(addr1!!))
 
         val addr2 = NetworkStackUtils.ipv6AddressToSolicitedNodeMulticast(TEST_INET6ADDR_2)
         assertSolicitedNodeMulticastAddress(addr2, TEST_INET6ADDR_2)
+        assertTrue(NetworkStackUtils.isIPv6AddressSolicitedNodeMulticast(addr2!!))
 
         val addr3 = NetworkStackUtils.ipv6AddressToSolicitedNodeMulticast(TEST_INET6ADDR_3)
         assertSolicitedNodeMulticastAddress(addr3, TEST_INET6ADDR_3)
+        assertTrue(NetworkStackUtils.isIPv6AddressSolicitedNodeMulticast(addr3!!))
     }
 
     @Test
     fun testConvertMacAddressToEui64() {
         // MAC address with universal/local bit set (the first byte: 0xBA)
         var expected = byteArrayOf(
-                0xB8.toByte(), 0x98.toByte(), 0x76.toByte(), 0xFF.toByte(),
-                0xFE.toByte(), 0x54.toByte(), 0x32.toByte(), 0x10.toByte())
+            0xB8.toByte(),
+            0x98.toByte(),
+            0x76.toByte(),
+            0xFF.toByte(),
+            0xFE.toByte(),
+            0x54.toByte(),
+            0x32.toByte(),
+            0x10.toByte()
+        )
         val srcEui64 = NetworkStackUtils.macAddressToEui64(TEST_SRC_MAC)
         assertArrayEquals(expected, srcEui64)
 
@@ -288,8 +664,10 @@
     private fun assertNextPacketOnSocket(fd: FileDescriptor, expectedPacket: ByteBuffer) {
         val received = ByteBuffer.allocate(TEST_MTU)
         val len = Os.read(fd, received)
-        assertEquals(toHexString(expectedPacket, expectedPacket.limit()),
-            toHexString(received, len))
+        assertEquals(
+            toHexString(expectedPacket, expectedPacket.limit()),
+            toHexString(received, len)
+        )
     }
 
     private fun setMfBit(packet: ByteBuffer, set: Boolean) {
@@ -307,6 +685,15 @@
         packet.putShort(checksumOffset, IpUtils.ipChecksum(packet, ETHER_HEADER_LEN))
     }
 
+    private fun isTestInterfaceEgressPacket(packet: ByteArray): Boolean {
+        val srcMac = packet.copyOfRange(
+            ETHER_SRC_ADDR_OFFSET,
+            ETHER_SRC_ADDR_OFFSET + ETHER_ADDR_LEN
+        )
+        val ifParams = InterfaceParams.getByName(iface.interfaceName)
+        return srcMac.contentEquals(ifParams.macAddr.toByteArray())
+    }
+
     private fun doTestDhcpResponseWithMfBitDropped(generic: Boolean) {
         val ifindex = InterfaceParams.getByName(iface.interfaceName).index
         val packetSock = Os.socket(AF_PACKET, SOCK_RAW or SOCK_NONBLOCK, /*protocol=*/0)
@@ -318,15 +705,25 @@
             }
             val addr = SocketUtils.makePacketSocketAddress(OsConstants.ETH_P_IP, ifindex)
             Os.bind(packetSock, addr)
-            val packet = DhcpPacket.buildNakPacket(DhcpPacket.ENCAP_L2, 42,
-                TEST_TARGET_IPV4_ADDR, /*relayIp=*/ IPV4_ADDR_ANY, TEST_TARGET_MAC.toByteArray(),
-                /*broadcast=*/ false, "NAK")
+            val packet = DhcpPacket.buildNakPacket(
+                DhcpPacket.ENCAP_L2,
+                42,
+                TEST_TARGET_IPV4_ADDR, /*relayIp=*/
+                IPV4_ADDR_ANY,
+                TEST_TARGET_MAC.toByteArray(),
+                /*broadcast=*/
+                false,
+                "NAK"
+            )
             setMfBit(packet, true)
             reader.sendResponse(packet)
 
             // Packet with MF bit set is not received.
-            assertSocketReadErrno("Packet with MF bit should have been dropped",
-                packetSock, OsConstants.EAGAIN)
+            assertSocketReadErrno(
+                "Packet with MF bit should have been dropped",
+                packetSock,
+                OsConstants.EAGAIN
+            )
 
             // Identical packet, except with MF bit cleared, should be received.
             setMfBit(packet, false)
@@ -346,6 +743,49 @@
     fun testGenericDhcpResponseWithMfBitDropped() {
         doTestDhcpResponseWithMfBitDropped(true)
     }
+
+    @Test
+    fun testConvertIpv4AddressToEthernetMulticast() {
+        var mcastAddrs = listOf(
+            // ipv4 multicast address, multicast ethernet address
+            Pair(
+                InetAddress.getByName("224.0.0.1") as Inet4Address,
+                MacAddress.fromString("01:00:5e:00:00:01")
+            ),
+            Pair(
+                InetAddress.getByName("239.128.1.1") as Inet4Address,
+                MacAddress.fromString("01:00:5e:00:01:01")
+            ),
+            Pair(
+                InetAddress.getByName("239.255.255.255") as Inet4Address,
+                MacAddress.fromString("01:00:5e:7f:ff:ff")
+            )
+        )
+
+        for ((addr, expectAddr) in mcastAddrs) {
+            val ether = NetworkStackUtils.ipv4MulticastToEthernetMulticast(addr)
+            assertEquals(expectAddr, ether)
+        }
+    }
+
+    @Test
+    fun testSelectPreferredIPv6LinkLocalAddress() {
+        val addr1 = LinkAddress("fe80::1/64", IFA_F_TENTATIVE, RT_SCOPE_LINK)
+        val addr2 = LinkAddress("fe80::2/64", 0 /* flags */, RT_SCOPE_LINK)
+        val addr3 = LinkAddress("fe80::3/64", IFA_F_DEPRECATED, RT_SCOPE_LINK)
+
+        val lp1 = LinkProperties()
+        lp1.setLinkAddresses(listOf(addr1, addr2, addr3))
+        assertEquals(addr2.address, NetworkStackUtils.selectPreferredIPv6LinkLocalAddress(lp1))
+
+        val lp2 = LinkProperties()
+        lp2.setLinkAddresses(listOf(addr1, addr3))
+        assertEquals(addr3.address, NetworkStackUtils.selectPreferredIPv6LinkLocalAddress(lp2))
+
+       val lp3 = LinkProperties()
+        lp3.setLinkAddresses(listOf(addr1))
+        assertNull(NetworkStackUtils.selectPreferredIPv6LinkLocalAddress(lp3))
+    }
 }
 
 private fun ByteBuffer.readAsArray(): ByteArray {
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 7e6de1a..47fd29b 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -34,6 +34,7 @@
         "net-tests-utils",
         "net-utils-framework-common",
         "testables",
+        "truth",
     ],
     libs: [
         "android.test.runner.stubs",
@@ -115,6 +116,7 @@
     name: "libnetworkstackutilsjni_deps",
     jni_libs: [
         "libnativehelper_compat_libc++",
-        "libnetworkstacktestsjni",
+        "libapfjniv6",
+        "libapfjninext",
     ],
 }
diff --git a/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp
index 1ea4268..0dc31a3 100644
--- a/tests/unit/jni/Android.bp
+++ b/tests/unit/jni/Android.bp
@@ -19,8 +19,8 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-cc_library_shared {
-    name: "libnetworkstacktestsjni",
+cc_defaults {
+    name: "libapfjni_defaults",
     srcs: [
         "**/*.cpp",
     ],
@@ -38,10 +38,32 @@
     ],
     static_libs: [
         "libapf",
-        "libapf_v7",
         "libapfdisassembler",
         "libpcap",
+        "libapfbuf",
     ],
     sdk_version: "30",
     stl: "c++_static",
 }
+
+cc_library_shared {
+    name: "libapfjniv6",
+    defaults: ["libapfjni_defaults"],
+    cflags: [
+        "-DAPF_INTERPRETER_V6",
+    ],
+    static_libs: [
+        "libapf_v6",
+    ],
+}
+
+cc_library_shared {
+    name: "libapfjninext",
+    defaults: ["libapfjni_defaults"],
+    cflags: [
+        "-DAPF_INTERPRETER_NEXT",
+    ],
+    static_libs: [
+        "libapf_next",
+    ],
+}
diff --git a/tests/unit/jni/apf_jni.cpp b/tests/unit/jni/apf_jni.cpp
index 873b217..98078c9 100644
--- a/tests/unit/jni/apf_jni.cpp
+++ b/tests/unit/jni/apf_jni.cpp
@@ -23,14 +23,22 @@
 #include <string>
 #include <vector>
 
-#include "apf_interpreter.h"
+#include "v4/apf_interpreter.h"
 #include "disassembler.h"
 #include "nativehelper/scoped_primitive_array.h"
-#include "v7/apf_interpreter.h"
-#include "v7/test_buf_allocator.h"
+
+#include "next/test_buf_allocator.h"
+
+#ifdef APF_INTERPRETER_NEXT
+#include "next/apf_interpreter.h"
+#endif
+
+#ifdef APF_INTERPRETER_V6
+#include "v6/apf_interpreter.h"
+#endif
 
 #define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
-#define LOG_TAG "NetworkStackUtils-JNI"
+#define LOG_TAG "ApfJniUtils"
 
 static int run_apf_interpreter(int apf_version, uint32_t* program,
                                uint32_t program_len, uint32_t ram_len,
@@ -56,12 +64,14 @@
     uint32_t program_len = env->GetArrayLength(jprogram);
     uint32_t data_len = jdata ? env->GetArrayLength(jdata) : 0;
     // we need to guarantee room for APFv6's 5 u32 counters (20 bytes)
+    // and APFv6.1's 6 u32 counters (24 bytes)
     // and we need to make sure ram_len is a multiple of 4 bytes,
     // so that the counters (which are indexed from the back are aligned.
     uint32_t ram_len = program_len + data_len;
     if (apf_version > 4) {
         ram_len += 3; ram_len &= ~3;
-        if (data_len < 20) ram_len += 20;
+        uint32_t need = 24; // TODO: (apf_version > 6000) ? 24 : 20;
+        if (data_len < need) ram_len += need;
     }
     std::vector<uint32_t> buf((ram_len + 3) / 4, 0);
     jbyte* jbuf = reinterpret_cast<jbyte*>(buf.data());
@@ -268,7 +278,11 @@
                             reinterpret_cast<jbyte*>(buf.data()));
     std::vector<std::string> disassemble_output;
     for (uint32_t pc = 0; pc < program_len;) {
-         disassemble_output.emplace_back(apf_disassemble(buf.data(), program_len, &pc));
+      // TODO: Implement proper selection of APFv4 or APFv6 code for
+      // disassembly.
+      const disas_ret ret =
+          apf_disassemble(buf.data(), program_len, &pc, true /* is_v6*/);
+      disassemble_output.emplace_back(ret.content);
     }
     jclass stringClass = env->FindClass("java/lang/String");
     jobjectArray disassembleOutput =
@@ -284,21 +298,43 @@
     return disassembleOutput;
 }
 
-jbyteArray com_android_server_ApfTest_getTransmittedPacket(JNIEnv* env,
-                                                           jclass) {
-    jbyteArray jdata = env->NewByteArray((jint) apf_test_tx_packet_len);
-    if (jdata == NULL) { return NULL; }
-    if (apf_test_tx_packet_len == 0) { return jdata; }
+static jobjectArray com_android_server_ApfTest_getAllTransmittedPackets(JNIEnv* env,
+                                                                        jclass) {
+    jclass arrayListClass = env->FindClass("java/util/ArrayList");
+    jmethodID arrayListConstructor = env->GetMethodID(arrayListClass, "<init>", "()V");
+    jobject arrayList = env->NewObject(arrayListClass, arrayListConstructor);
 
-    env->SetByteArrayRegion(jdata, 0, (jint) apf_test_tx_packet_len,
-                            reinterpret_cast<jbyte*>(apf_test_buffer));
+    jmethodID addMethod = env->GetMethodID(arrayListClass, "add", "(Ljava/lang/Object;)Z");
+    packet_buffer *ptr = head;
+    while (ptr) {
+        jbyteArray jdata = env->NewByteArray((jint) ptr->len);
+        if (jdata == NULL) {
+            return static_cast<jobjectArray>(arrayList);
+        }
 
-    return jdata;
+        env->SetByteArrayRegion(jdata, 0, (jint) ptr->len,
+                                reinterpret_cast<jbyte*>(ptr->data));
+        env->CallBooleanMethod(arrayList, addMethod, jdata);
+        env->DeleteLocalRef(jdata);
+
+        ptr = ptr->next;
+    }
+
+    env->DeleteLocalRef(arrayListClass);
+    return static_cast<jobjectArray>(arrayList);
 }
 
 void com_android_server_ApfTest_resetTransmittedPacketMemory(JNIEnv, jclass) {
-    apf_test_tx_packet_len = 0;
-    memset(apf_test_buffer, 0xff, sizeof(apf_test_buffer));
+    packet_buffer* current = head;
+    packet_buffer* tmp = NULL;
+    while (current) {
+        tmp = current->next;
+        free(current);
+        current = tmp;
+    }
+
+    head = NULL;
+    tail = NULL;
 }
 
 extern "C" jint JNI_OnLoad(JavaVM* vm, void*) {
@@ -319,8 +355,8 @@
                     (void*)com_android_server_ApfTest_dropsAllPackets },
             { "disassembleApf", "([B)[Ljava/lang/String;",
               (void*)com_android_server_ApfTest_disassembleApf },
-            { "getTransmittedPacket", "()[B",
-              (void*)com_android_server_ApfTest_getTransmittedPacket },
+            { "getAllTransmittedPackets", "()Ljava/util/List;",
+                    (void*)com_android_server_ApfTest_getAllTransmittedPackets },
             { "resetTransmittedPacketMemory", "()V",
               (void*)com_android_server_ApfTest_resetTransmittedPacketMemory },
     };
diff --git a/tests/unit/res/raw/apf.pcap b/tests/unit/res/raw/apf.pcap
deleted file mode 100644
index 963165f..0000000
--- a/tests/unit/res/raw/apf.pcap
+++ /dev/null
Binary files differ
diff --git a/tests/unit/src/android/net/apf/ApfFilterTest.kt b/tests/unit/src/android/net/apf/ApfFilterTest.kt
index 15ff224..5cfeaad 100644
--- a/tests/unit/src/android/net/apf/ApfFilterTest.kt
+++ b/tests/unit/src/android/net/apf/ApfFilterTest.kt
@@ -16,11 +16,14 @@
 package android.net.apf
 
 import android.content.Context
+import android.net.InetAddresses
 import android.net.LinkAddress
 import android.net.LinkProperties
 import android.net.MacAddress
 import android.net.NattKeepalivePacketDataParcelable
 import android.net.TcpKeepalivePacketDataParcelable
+import android.net.apf.ApfConstants.ETH_MULTICAST_MDNS_V4_MAC_ADDRESS
+import android.net.apf.ApfConstants.ETH_MULTICAST_MDNS_V6_MAC_ADDRESS
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_NON_IPV4
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_OTHER_HOST
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_REPLY_SPA_NO_HOST
@@ -28,42 +31,53 @@
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_UNKNOWN
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_ARP_V6_ONLY
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_ETHERTYPE_NOT_ALLOWED
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_ETHER_OUR_SRC_MAC
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_GARP_REPLY
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IGMP_INVALID
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IGMP_REPORT
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IGMP_V2_GENERAL_QUERY_REPLIED
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IGMP_V3_GENERAL_QUERY_REPLIED
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_BROADCAST_ADDR
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_BROADCAST_NET
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_ICMP_INVALID
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_KEEPALIVE_ACK
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_L2_BROADCAST
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_MULTICAST
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_NATT_KEEPALIVE
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_NON_DHCP4
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_PING_REQUEST_REPLIED
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_TCP_PORT7_UNICAST
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_ICMP6_ECHO_REQUEST_INVALID
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_ICMP6_ECHO_REQUEST_REPLIED
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MLD_INVALID
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MLD_REPORT
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MLD_V1_GENERAL_QUERY_REPLIED
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MLD_V2_GENERAL_QUERY_REPLIED
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_MULTICAST_NA
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NON_ICMP_MULTICAST
 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_ETHER_OUR_SRC_MAC
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_MDNS
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_MDNS_REPLIED
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_NON_UNICAST_TDLS
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_RA
 import android.net.apf.ApfCounterTracker.Counter.PASSED_ARP_BROADCAST_REPLY
 import android.net.apf.ApfCounterTracker.Counter.PASSED_ARP_REQUEST
 import android.net.apf.ApfCounterTracker.Counter.PASSED_ARP_UNICAST_REPLY
 import android.net.apf.ApfCounterTracker.Counter.PASSED_DHCP
+import android.net.apf.ApfCounterTracker.Counter.PASSED_ETHER_OUR_SRC_MAC
 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_IPV4_UNICAST
+import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_HOPOPTS
 import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_ICMP
 import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NON_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.ApfCounterTracker.Counter.PASSED_MLD
+import android.net.apf.ApfCounterTracker.Counter.PASSED_MDNS
+import android.net.apf.ApfCounterTracker.Counter.PASSED_NON_IP_UNICAST
 import android.net.apf.ApfFilter.Dependencies
 import android.net.apf.ApfTestHelpers.Companion.TIMEOUT_MS
-import android.net.apf.ApfTestHelpers.Companion.consumeInstalledProgram
-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.net.nsd.NsdManager
 import android.net.nsd.OffloadEngine
 import android.net.nsd.OffloadServiceInfo
@@ -75,6 +89,7 @@
 import android.system.OsConstants.AF_UNIX
 import android.system.OsConstants.IFA_F_TENTATIVE
 import android.system.OsConstants.SOCK_STREAM
+import android.util.Log
 import androidx.test.filters.SmallTest
 import com.android.internal.annotations.GuardedBy
 import com.android.net.module.util.HexDump
@@ -85,28 +100,38 @@
 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_ADDR_ALL_NODES_MULTICAST
+import com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_NODE_LOCAL_ALL_NODES_MULTICAST
 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.networkstack.util.NetworkStackUtils.isAtLeast25Q2
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.quitResources
+import com.android.testutils.tryTest
+import com.android.testutils.visibleOnHandlerThread
 import com.android.testutils.waitForIdle
+import com.google.common.truth.Truth.assertThat
 import java.io.FileDescriptor
+import java.net.Inet4Address
 import java.net.Inet6Address
 import java.net.InetAddress
+import kotlin.random.Random
 import kotlin.test.assertContentEquals
 import kotlin.test.assertEquals
 import libcore.io.IoUtils
 import org.junit.After
+import org.junit.Assume.assumeTrue
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
 import org.mockito.ArgumentCaptor
 import org.mockito.ArgumentMatchers.any
 import org.mockito.ArgumentMatchers.anyInt
@@ -114,14 +139,18 @@
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mock
 import org.mockito.Mockito
+import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.doAnswer
 import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 import org.mockito.invocation.InvocationOnMock
 
+open class FromU<Type>(val value: Type)
+
 /**
  * Test for APF filter.
  */
@@ -131,12 +160,27 @@
 class ApfFilterTest {
     companion object {
         private const val THREAD_QUIT_MAX_RETRY_COUNT = 3
+        private const val NO_CALLBACK_TIMEOUT_MS: Long = 500
         private const val TAG = "ApfFilterTest"
+
+        @Parameterized.Parameters
+        @JvmStatic
+        fun data(): Iterable<Any?> {
+            return mutableListOf<Int?>(
+                ApfJniUtils.APF_INTERPRETER_VERSION_V6,
+                ApfJniUtils.APF_INTERPRETER_VERSION_NEXT
+            )
+        }
     }
 
     @get:Rule
     val ignoreRule = DevSdkIgnoreRule()
 
+    // Indicates which apfInterpreter to load.
+    @Parameterized.Parameter(0)
+    @JvmField
+    var apfInterpreterVersion: Int = ApfJniUtils.APF_INTERPRETER_VERSION_NEXT
+
     @Mock
     private lateinit var context: Context
 
@@ -144,11 +188,11 @@
 
     @Mock private lateinit var dependencies: Dependencies
 
-    @Mock private lateinit var ipClientCallback: IpClientCallbacksWrapper
+    @Mock private lateinit var apfController: ApfFilter.IApfController
     @Mock private lateinit var nsdManager: NsdManager
 
     @GuardedBy("mApfFilterCreated")
-    private val mApfFilterCreated = ArrayList<AndroidPacketFilter>()
+    private val mApfFilterCreated = ArrayList<ApfFilter>()
     private val loInterfaceParams = InterfaceParams.getByName("lo")
     private val ifParams =
         InterfaceParams(
@@ -175,6 +219,7 @@
         intArrayOf(0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x01, 0, 0, 0x1b, 0x44, 0x55, 0x66, 0x77)
             .map{ it.toByte() }.toByteArray()
     )
+    private val hostLinkLocalIpv6Address = InetAddresses.parseNumericAddress("fe80::3")
     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)
@@ -197,16 +242,211 @@
         intArrayOf(0x33, 0x33, 0xff, 0x55, 0x66, 0x77).map { it.toByte() }.toByteArray(),
         // 33:33:ff:bb:cc:dd
         intArrayOf(0x33, 0x33, 0xff, 0xbb, 0xcc, 0xdd).map { it.toByte() }.toByteArray(),
+        ETH_MULTICAST_MDNS_V4_MAC_ADDRESS,
+        ETH_MULTICAST_MDNS_V6_MAC_ADDRESS
     )
 
+    // Using scapy to generate payload:
+    // answers = [
+    //    DNSRR(rrname="_googlecast._tcp.local", type="PTR", ttl=120, rdata="gambit-3cb56c6253638b3641e3d289013cc0ae._googlecast._tcp.local."),
+    //    DNSRR(rrname="gambit-3cb56c6253638b3641e3d289013cc0ae._googlecast._tcp.local", type="SRV", ttl=120, rdata="0 0 8009 3cb56c62-5363-8b36-41e3-d289013cc0ae.local."),
+    //    DNSRR(rrname="gambit-3cb56c6253638b3641e3d289013cc0ae._googlecast._tcp.local", type="TXT", ttl=120, rdata=' "id=3cb56c6253638b3641e3d289013cc0ae cd=8ECC37F6755390D005DFC02F8EC0D4FA rm=4ABD579644ACFCCF ve=05 md=gambit ic=/setup/icon.png fn=gambit a=264709 st=0 bs=FA8FFD2242A7 nf=1 rs= ',),
+    //    DNSRR(rrname="Android_f47ac10b58cc4b88bc3f5e7a81e59872.local", type="A", ttl=120, rdata="100.89.85.228"),
+    //    DNSRR(rrname="Android_f47ac10b58cc4b88bc3f5e7a81e59872.local", type="AAAA", ttl=120, rdata="fe80:0000:0000:0000:0000:0000:0000:0003"),
+    //    DNSRR(rrname="Android_f47ac10b58cc4b88bc3f5e7a81e59872.local", type="AAAA", ttl=120, rdata="200a:0000:0000:0000:0000:0000:0000:0003"),
+    //    DNSRR(rrname="Android_f47ac10b58cc4b88bc3f5e7a81e59872.local", type="AAAA", ttl=120, rdata="200b:0000:0000:0000:0000:0000:0000:0003"),
+    // ]
+    // dns = dns_compress(DNS(qr=1, aa=1, rd=0, qd=None, an=answers))
+    private val castOffloadPayload = """
+            0000840000000007000000000b5f676f6f676c6563617374045f746370056c6
+            f63616c00000c000100000078002a2767616d6269742d336362353663363235
+            3336333862333634316533643238393031336363306165c00c01c0000021000
+            100000078003430203020383030392033636235366336322d353336332d3862
+            33362d343165332d6432383930313363633061652e6c6f63616c2e01c000001
+            000010000007800b3b2202269643d3363623536633632353336333862333634
+            3165336432383930313363633061652063643d3845434333374636373535333
+            93044303035444643303246384543304434464120726d3d3441424435373936
+            34344143464343462076653d3035206d643d67616d6269742069633d2f73657
+            475702f69636f6e2e706e6720666e3d67616d62697420613d32363437303920
+            73743d302062733d464138464644323234324137206e663d312072733d20284
+            16e64726f69645f663437616331306235386363346238386263336635653761
+            3831653539383732c01d00010001000000780004645955e4c157001c0001000
+            000780010fe800000000000000000000000000003c157001c00010000007800
+            10200a0000000000000000000000000003c157001c0001000000780010200b0
+            000000000000000000000000003
+        """.replace("\\s+".toRegex(), "").trim()
+
+    // Using scapy to generate payload:
+    // answers = [
+    //    DNSRR(rrname="_androidtvremote2._tcp.local", type="PTR", rdata="gambit._androidtvremote2._tcp.local", ttl=120),
+    //    DNSRR(rrname="gambit._androidtvremote2._tcp.local", type="SRV", rdata="0 0 6466 Android_2570595cc11d4af4a4b7146b946eeb9e.local", ttl=120),
+    //    DNSRR(rrname="gambit._androidtvremote2._tcp.local", type="TXT", rdata='''"bt=3C:4E:56:76:1E:E9"''', ttl=120),
+    //    DNSRR(rrname="Android_f47ac10b58cc4b88bc3f5e7a81e59872.local", type="A", ttl=120, rdata="100.89.85.228"),
+    //    DNSRR(rrname="Android_f47ac10b58cc4b88bc3f5e7a81e59872.local", type="AAAA", ttl=120, rdata="fe80:0000:0000:0000:0000:0000:0000:0003"),
+    //    DNSRR(rrname="Android_f47ac10b58cc4b88bc3f5e7a81e59872.local", type="AAAA", ttl=120, rdata="200a:0000:0000:0000:0000:0000:0000:0003"),
+    //    DNSRR(rrname="Android_f47ac10b58cc4b88bc3f5e7a81e59872.local", type="AAAA", ttl=120, rdata="200b:0000:0000:0000:0000:0000:0000:0003"),
+    // ]
+    // dns = dns_compress(DNS(qr=1, aa=1, rd=0, qd=None, an=answers))
+    val tvRemoteOffloadPayload = """
+            000084000000000700000000115f616e64726f6964747672656d6f746532045
+            f746370056c6f63616c00000c00010000007800090667616d626974c00cc034
+            00210001000000780037302030203634363620416e64726f69645f323537303
+            53935636331316434616634613462373134366239343665656239652e6c6f63
+            616cc03400100001000000780017162262743d33433a34453a35363a37363a3
+            1453a45392228416e64726f69645f6634376163313062353863633462383862
+            633366356537613831653539383732c02300010001000000780004645955e4c
+            0a3001c0001000000780010fe800000000000000000000000000003c0a3001c
+            0001000000780010200a0000000000000000000000000003c0a3001c0001000
+            000780010200b0000000000000000000000000003
+        """.replace("\\s+".toRegex(), "").trim()
+
+    // answers = [
+    //    DNSRR(rrname="_airplay._tcp.local", type="PTR", rdata="gambit._airplay._tcp.local", ttl=120),
+    //    DNSRR(rrname="gambit._airplay._tcp.local", type="SRV", rdata="0 0 6466 Android_2570595cc11d4af4a4b7146b946eeb9e.local", ttl=120),
+    //    DNSRR(rrname="gambit._airplay._tcp.local", type="TXT", rdata='"deviceid=58:55:CA:1A:E2:88 features=0x39f7 model=AppleTV2,1 srcvers=130.14"', ttl=120), DNSRR(rrname="Android_f47ac10b58cc4b88bc3f5e7a81e59872.local", type="A", ttl=120, rdata="100.89.85.228"),
+    //    DNSRR(rrname="Android_f47ac10b58cc4b88bc3f5e7a81e59872.local", type="AAAA", ttl=120, rdata="fe80:0000:0000:0000:0000:0000:0000:0003"),
+    //    DNSRR(rrname="Android_f47ac10b58cc4b88bc3f5e7a81e59872.local", type="AAAA", ttl=120, rdata="200a:0000:0000:0000:0000:0000:0000:0003"),
+    //    DNSRR(rrname="Android_f47ac10b58cc4b88bc3f5e7a81e59872.local", type="AAAA", ttl=120, rdata="200b:0000:0000:0000:0000:0000:0000:0003"),
+    // ]
+    // dns = dns_compress(DNS(qr=1, aa=1, rd=0, qd=None, an=answers))
+    val airplayOffloadPayload = """
+            000084000000000700000000085f616972706c6179045f746370056c6f63616
+            c00000c00010000007800090667616d626974c00cc02b002100010000007800
+            37302030203634363620416e64726f69645f323537303539356363313164346
+            16634613462373134366239343665656239652e6c6f63616cc02b0010000100
+            000078004d4c2264657669636569643d35383a35353a43413a31413a45323a3
+            8382066656174757265733d307833396637206d6f64656c3d4170706c655456
+            322c3120737263766572733d3133302e31342228416e64726f69645f6634376
+            163313062353863633462383862633366356537613831653539383732c01a00
+            010001000000780004645955e4c0d0001c0001000000780010fe80000000000
+            0000000000000000003c0d0001c0001000000780010200a0000000000000000
+            000000000003c0d0001c0001000000780010200b00000000000000000000000
+            00003
+        """.replace("\\s+".toRegex(), "").trim()
+
+    // answers = [
+    //    DNSRR(rrname="_raop._tcp.local", type="PTR", rdata="5855CA1AE288@gambit._raop._tcp.local", ttl=120),
+    //    DNSRR(rrname="5855CA1AE288@gambit._raop._tcp.local", type="SRV", rdata="0 0 6466 Android_2570595cc11d4af4a4b7146b946eeb9e.local", ttl=120),
+    //    DNSRR(rrname="5855CA1AE288@gambit._raop._tcp.local", type="TXT", rdata='"txtvers=1 ch=2 cn=0,1,2,3 da=true et=0,3,5 md=0,1,2 pw=false sv=false sr=44100 ss=16 tp=UDP vn=65537 vs=130.14 am=AppleTV2,1 sf=0x4"', ttl=120),
+    //    DNSRR(rrname="Android_f47ac10b58cc4b88bc3f5e7a81e59872.local", type="A", ttl=120, rdata="100.89.85.228"),
+    //    DNSRR(rrname="Android_f47ac10b58cc4b88bc3f5e7a81e59872.local", type="AAAA", ttl=120, rdata="fe80:0000:0000:0000:0000:0000:0000:0003"),
+    //    DNSRR(rrname="Android_f47ac10b58cc4b88bc3f5e7a81e59872.local", type="AAAA", ttl=120, rdata="200a:0000:0000:0000:0000:0000:0000:0003"),
+    //    DNSRR(rrname="Android_f47ac10b58cc4b88bc3f5e7a81e59872.local", type="AAAA", ttl=120, rdata="200b:0000:0000:0000:0000:0000:0000:0003"),
+    // ]
+    // dns = dns_compress(DNS(qr=1, aa=1, rd=0, qd=None, an=answers))
+    val raopOffloadPayload = """
+            000084000000000700000000055f72616f70045f746370056c6f63616c00000
+            c0001000000780016133538353543413141453238384067616d626974c00cc0
+            2800210001000000780037302030203634363620416e64726f69645f3235373
+            0353935636331316434616634613462373134366239343665656239652e6c6f
+            63616cc028001000010000007800868522747874766572733d312063683d322
+            0636e3d302c312c322c332064613d747275652065743d302c332c35206d643d
+            302c312c322070773d66616c73652073763d66616c73652073723d343431303
+            02073733d31362074703d55445020766e3d36353533372076733d3133302e31
+            3420616d3d4170706c655456322c312073663d3078342228416e64726f69645
+            f66343761633130623538636334623838626333663565376138316535393837
+            32c01700010001000000780004645955e4c113001c0001000000780010fe800
+            000000000000000000000000003c113001c0001000000780010200a00000000
+            00000000000000000003c113001c0001000000780010200b000000000000000
+            0000000000003
+        """.replace("\\s+".toRegex(), "").trim()
+
+    private val passthroughCastOffloadInfo by lazy {
+        FromU(
+            OffloadServiceInfo(
+                OffloadServiceInfo.Key(
+                    "gambit-3cb56c6253638b3641e3d289013cc0ae",
+                    "_googlecast._tcp"
+                ),
+                listOf(),
+                "Android_f47ac10b58cc4b88bc3f5e7a81e59872.local",
+                null,
+                0,
+                OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+            )
+        )
+    }
+
+    private val castOffloadInfo by lazy {
+        FromU(
+            OffloadServiceInfo(
+                OffloadServiceInfo.Key(
+                    "gambit-3cb56c6253638b3641e3d289013cc0ae",
+                    "_googlecast._tcp"
+                ),
+                listOf(),
+                "Android_f47ac10b58cc4b88bc3f5e7a81e59872.local",
+                HexDump.hexStringToByteArray(castOffloadPayload),
+                1,
+                OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+            )
+        )
+    }
+    private val tvRemoteOffloadInfo by lazy {
+        FromU(
+            OffloadServiceInfo(
+                OffloadServiceInfo.Key("gambit", "_androidtvremote2._tcp"),
+                listOf(),
+                "Android_f47ac10b58cc4b88bc3f5e7a81e59872.local",
+                HexDump.hexStringToByteArray(tvRemoteOffloadPayload),
+                2,
+                OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+            )
+        )
+    }
+    private val manySubtypeOffloadInfo by lazy {
+        FromU(
+            OffloadServiceInfo(
+                OffloadServiceInfo.Key("gambit", "_testsubtype._tcp"),
+                listOf("subtype1", "subtype2", "subtype3", "subtype4", "subtype5"),
+                "Android_f47ac10b58cc4b88bc3f5e7a81e59872.local",
+                HexDump.hexStringToByteArray(castOffloadPayload),
+                3,
+                OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+            )
+        )
+    }
+
+    private val airplayOffloadInfo by lazy {
+        FromU(
+            OffloadServiceInfo(
+                OffloadServiceInfo.Key("gambit", "_airplay._tcp"),
+                listOf(),
+                "Android_f47ac10b58cc4b88bc3f5e7a81e59872.local",
+                HexDump.hexStringToByteArray(airplayOffloadPayload),
+                4,
+                OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+            )
+        )
+    }
+
+    private val raopOffloadInfo by lazy {
+        FromU(
+            OffloadServiceInfo(
+                OffloadServiceInfo.Key("5855CA1AE288@gambit", "_raop._tcp"),
+                listOf(),
+                "Android_f47ac10b58cc4b88bc3f5e7a81e59872.local",
+                HexDump.hexStringToByteArray(raopOffloadPayload),
+                4,
+                OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+            )
+
+        )
+    }
+    private val counterTotalSize = ApfCounterTracker.Counter.totalSize()
+
     private val handlerThread by lazy {
         HandlerThread("$TAG handler thread").apply { start() }
     }
     private val handler by lazy { Handler(handlerThread.looper) }
-    private var writerSocket = FileDescriptor()
+    private lateinit var raReadSocket: FileDescriptor
+    private var raWriterSocket = FileDescriptor()
+    private var mcastWriteSocket = FileDescriptor()
+    private lateinit var apfTestHelpers: ApfTestHelpers
 
     @Before
     fun setUp() {
+        apfTestHelpers = ApfTestHelpers(apfInterpreterVersion)
         MockitoAnnotations.initMocks(this)
         // mock anycast6 address from /proc/net/anycast6
         doReturn(hostAnycast6Addresses).`when`(dependencies).getAnycast6Addresses(any())
@@ -222,9 +462,15 @@
             }
         }.`when`(dependencies).onApfFilterCreated(any())
         doReturn(SystemClock.elapsedRealtime()).`when`(dependencies).elapsedRealtime()
-        val readSocket = FileDescriptor()
-        Os.socketpair(AF_UNIX, SOCK_STREAM, 0, writerSocket, readSocket)
-        doReturn(readSocket).`when`(dependencies).createPacketReaderSocket(anyInt())
+        raReadSocket = FileDescriptor()
+        Os.socketpair(AF_UNIX, SOCK_STREAM, 0, raWriterSocket, raReadSocket)
+        doReturn(raReadSocket).`when`(dependencies).createPacketReaderSocket(anyInt())
+        val mcastReadSocket = FileDescriptor()
+        Os.socketpair(AF_UNIX, SOCK_STREAM, 0, mcastWriteSocket, mcastReadSocket)
+        doReturn(mcastReadSocket)
+                .`when`(dependencies).createEgressIgmpReportsReaderSocket(anyInt())
+        doReturn(mcastReadSocket)
+                .`when`(dependencies).createEgressMulticastReportsReaderSocket(anyInt())
         doReturn(nsdManager).`when`(context).getSystemService(NsdManager::class.java)
     }
 
@@ -235,7 +481,7 @@
                 mApfFilterCreated.clear()
                 return@quitResources ret
             }
-        }, { apf: AndroidPacketFilter ->
+        }, { apf: ApfFilter ->
             handler.post { apf.shutdown() }
         })
 
@@ -250,16 +496,19 @@
 
     @After
     fun tearDown() {
-        IoUtils.closeQuietly(writerSocket)
+        IoUtils.closeQuietly(raWriterSocket)
+        IoUtils.closeQuietly(mcastWriteSocket)
         shutdownApfFilters()
         handler.waitForIdle(TIMEOUT_MS)
         Mockito.framework().clearInlineMocks()
-        ApfJniUtils.resetTransmittedPacketMemory()
+        apfTestHelpers.resetTransmittedPacketMemory()
         handlerThread.quitSafely()
         handlerThread.join()
     }
 
-    private fun getDefaultConfig(apfVersion: Int = APF_VERSION_6): ApfFilter.ApfConfiguration {
+    private fun getDefaultConfig(
+        apfVersion: Int = apfInterpreterVersion
+    ): ApfFilter.ApfConfiguration {
         val config = ApfFilter.ApfConfiguration()
         config.apfVersionSupported = apfVersion
         // 4K is the highly recommended value in APFv6 for vendor
@@ -267,13 +516,13 @@
         config.multicastFilter = false
         config.ieee802_3Filter = false
         config.ethTypeBlackList = IntArray(0)
-        config.shouldHandleArpOffload = true
-        config.shouldHandleNdOffload = true
+        config.handleArpOffload = true
+        config.handleNdOffload = true
         return config
     }
 
     private fun getApfFilter(
-            apfCfg: ApfFilter.ApfConfiguration = getDefaultConfig(APF_VERSION_6)
+            apfCfg: ApfFilter.ApfConfiguration = getDefaultConfig(apfInterpreterVersion)
     ): ApfFilter {
         lateinit var apfFilter: ApfFilter
         handler.post {
@@ -282,7 +531,7 @@
                     context,
                     apfCfg,
                     ifParams,
-                    ipClientCallback,
+                    apfController,
                     metrics,
                     dependencies
             )
@@ -291,8 +540,28 @@
         return apfFilter
     }
 
+    private fun getIgmpApfFilter(): ApfFilter {
+        val mcastAddrs = listOf(
+            InetAddress.getByName("224.0.0.1") as Inet4Address,
+            InetAddress.getByName("239.0.0.1") as Inet4Address,
+            InetAddress.getByName("239.0.0.2") as Inet4Address,
+            InetAddress.getByName("239.0.0.3") as Inet4Address
+        )
+        val apfConfig = getDefaultConfig()
+        apfConfig.handleIgmpOffload = true
+
+        // mock IPv4 multicast address from /proc/net/igmp
+        doReturn(mcastAddrs).`when`(dependencies).getIPv4MulticastAddresses(any())
+        val apfFilter = getApfFilter(apfConfig)
+        val linkAddress = LinkAddress(InetAddress.getByAddress(hostIpv4Address), 24)
+        val lp = LinkProperties()
+        lp.addLinkAddress(linkAddress)
+        apfFilter.setLinkProperties(lp)
+        return apfFilter
+    }
+
     private fun doTestEtherTypeAllowListFilter(apfFilter: ApfFilter) {
-        val program = consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
 
         // Using scapy to generate IPv4 mDNS packet:
         //   eth = Ether(src="E8:9F:80:66:60:BB", dst="01:00:5E:00:00:FB")
@@ -304,7 +573,7 @@
             01005e0000fbe89f806660bb080045000035000100004011d812c0a80101e00000f
             b14e914e900214d970000010000010000000000000161056c6f63616c00000c0001
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(mdnsPkt),
@@ -320,7 +589,7 @@
             333300000001e89f806660bb86dd6000000000103afffe800000000000000000000000
             000001ff0200000000000000000000000000018600600700080e100000000000000e10
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(raPkt),
@@ -330,7 +599,7 @@
         // Using scapy to generate ethernet packet with type 0x88A2:
         //  p = Ether(type=0x88A2)/Raw(load="01")
         val ethPkt = "ffffffffffff047bcb463fb588a23031"
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(ethPkt),
@@ -384,6 +653,16 @@
         return naPacket
     }
 
+    private fun updateIPv4MulticastAddrs(apfFilter: ApfFilter, mcastAddrs: List<Inet4Address>) {
+        doReturn(mcastAddrs).`when`(dependencies).getIPv4MulticastAddresses(any())
+        apfFilter.updateMulticastAddrs()
+    }
+
+    private fun updateIPv6MulticastAddrs(apfFilter: ApfFilter, mcastAddrs: List<Inet6Address>) {
+        doReturn(mcastAddrs).`when`(dependencies).getIPv6MulticastAddresses(any())
+        apfFilter.updateMulticastAddrs()
+    }
+
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     fun testV4EtherTypeAllowListFilter() {
@@ -394,7 +673,7 @@
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     fun testV6EtherTypeAllowListFilter() {
-        val apfFilter = getApfFilter(getDefaultConfig(APF_VERSION_6))
+        val apfFilter = getApfFilter(getDefaultConfig(apfInterpreterVersion))
         doTestEtherTypeAllowListFilter(apfFilter)
     }
 
@@ -402,7 +681,7 @@
     fun testIPv4PacketFilterOnV6OnlyNetwork() {
         val apfFilter = getApfFilter()
         apfFilter.updateClatInterfaceState(true)
-        val program = consumeInstalledProgram(ipClientCallback, installCnt = 3)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
 
         // Using scapy to generate IPv4 mDNS packet:
         //   eth = Ether(src="E8:9F:80:66:60:BB", dst="01:00:5E:00:00:FB")
@@ -414,7 +693,7 @@
             01005e0000fbe89f806660bb080045000035000100004011d812c0a80101e00000f
             b14e914e900214d970000010000010000000000000161056c6f63616c00000c0001
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(mdnsPkt),
@@ -428,7 +707,7 @@
         val nonUdpPkt = """
             ffffffffffff00112233445508004500001400010000400cb934c0a80101ffffffff
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(nonUdpPkt),
@@ -442,7 +721,7 @@
         val fragmentUdpPkt = """
             ffffffffffff0011223344550800450000140001200a40119925c0a80101ffffffff
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(fragmentUdpPkt),
@@ -457,7 +736,7 @@
         val nonDhcpServerPkt = """
             ffffffffffff00112233445508004500001c000100004011b927c0a80101ffffffff0035004600083dba
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(nonDhcpServerPkt),
@@ -492,7 +771,7 @@
             0000000000000000000000000000000000000000000000000000638253633501023604c0
             a801010104ffffff000304c0a80101330400015180060408080808ff
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(dhcp4Pkt),
@@ -511,7 +790,7 @@
             0000000000000000000000000000000000000000000000000000638253633501023604c0
             a801010104ffffff000304c0a80101330400015180060408080808ff
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(dhcp4PktDf),
@@ -530,7 +809,7 @@
             01005e0000fbe89f806660bb08004500001d000100034011f75dc0a8010ac0a8
             01146f63616c00000c0001
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(fragmentedUdpPkt),
@@ -542,7 +821,7 @@
     fun testLoopbackFilter() {
         val apfConfig = getDefaultConfig()
         val apfFilter = getApfFilter(apfConfig)
-        val program = consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
         // Using scapy to generate echo-ed broadcast packet:
         //   ether = Ether(src=${ifParams.macAddr}, dst='ff:ff:ff:ff:ff:ff')
         //   ip = IP(src='192.168.1.1', dst='255.255.255.255', proto=21)
@@ -550,11 +829,1147 @@
         val nonDhcpBcastPkt = """
             ffffffffffff020304050607080045000014000100004015b92bc0a80101ffffffff
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
                 apfFilter.mApfVersionSupported,
                 program,
                 HexDump.hexStringToByteArray(nonDhcpBcastPkt),
-                PASSED_ETHER_OUR_SRC_MAC
+                if (isAtLeast25Q2()) DROPPED_ETHER_OUR_SRC_MAC else PASSED_ETHER_OUR_SRC_MAC
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testInvalidIgmpPacketDropped() {
+        val apfFilter = getIgmpApfFilter()
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
+        // Using scapy to generate invalid length IGMPv1 general query packet:
+        //   ether = Ether(src='00:11:22:33:44:55', dst='01:00:5e:00:00:01')
+        //   ip = IP(src='10.0.0.2', dst='224.0.0.1', len=24, proto=2)
+        //   payload = Raw(b'\x11\x00\xee\xff\x01\x02\x03\x04\x05\x06')
+        //   pkt = ether/ip/payload
+        val payloadLen10Pkt = """
+            01005e00000100112233445508004500001800010000400290e00a000002e00000011100eeff010203040506
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(payloadLen10Pkt),
+            DROPPED_IGMP_INVALID
+        )
+
+        // Using scapy to generate invalid length IGMPv1 general query packet:
+        //   ether = Ether(src='00:11:22:33:44:55', dst='01:00:5e:00:00:01')
+        //   ip = IP(src='10.0.0.2', dst='224.0.0.1', len=20, proto=2)
+        //   payload = Raw(b'\x11\x00\xee\xff\x01\x02')
+        //   pkt = ether/ip/payload
+        val payloadLen7Pkt = """
+            01005e00000100112233445508004500001400010000400290e40a000002e00000011100eeff010203
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(payloadLen7Pkt),
+            DROPPED_IGMP_INVALID
+        )
+
+        // Using scapy to generate invalid length IGMP general query which the destination IP is
+        // not 224.0.0.1:
+        //   ether = Ether(src='00:11:22:33:44:55', dst='01:00:5e:00:00:05')
+        //   ip = IP(src='10.0.0.2', dst='224.0.0.5')
+        //   igmp = IGMP(type=0x11, mrcode=0)
+        //   pkt = ether/ip/igmp
+        val pktWithWrongDst = """
+            01005e00000300112233445508004500001c000100000102cfda0a000002e00000031100eeff00000000
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(pktWithWrongDst),
+            DROPPED_IGMP_INVALID
+        )
+
+        // Using scapy to generate invalid IGMP general query with wrong type:
+        //   ether = Ether(src='00:11:22:33:44:55', dst='01:00:5e:00:00:01')
+        //   ip = IP(src='10.0.0.2', dst='224.0.0.1')
+        //   igmp = IGMP(type=0x51, mrcode=0)
+        //   pkt = ether/ip/igmp
+        val pktWithWrongType = """
+            01005e00000100112233445508004500001c000100000102cfdc0a000002e00000015100aeff00000000
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(pktWithWrongType),
+            DROPPED_IGMP_INVALID
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIgmpV1ReportDropped() {
+        val apfFilter = getIgmpApfFilter()
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
+        // Using scapy to generate IGMPv1 report packet:
+        //   ether = Ether(src='00:11:22:33:44:55', dst='01:00:5e:7f:00:01')
+        //   ip = IP(src='10.0.0.2', dst='239.0.0.1')
+        //   igmp = IGMP(type=0x12, mrcode=0, gaddr='239.0.0.1')
+        //   pkt = ether/ip/igmp
+        val pkt = """
+            01005e7f000100112233445508004500001c000100000102c0dc0a000002ef0000011200fefdef000001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(pkt),
+            DROPPED_IGMP_REPORT
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIgmpV1GeneralQueryPassed() {
+        val apfFilter = getIgmpApfFilter()
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
+        // Using scapy to generate IGMPv1 general query packet:
+        //   ether = Ether(src='00:11:22:33:44:55', dst='01:00:5e:00:00:01')
+        //   ip = IP(src='10.0.0.2', dst='224.0.0.1')
+        //   igmp = IGMP(type=0x11, mrcode=0)
+        //   pkt = ether/ip/igmp
+        val pkt = """
+            01005e00000100112233445508004500001c000100000102cfdc0a000002e00000011100eeff00000000
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(pkt),
+            PASSED_IPV4
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIgmpV2ReportDropped() {
+        val apfFilter = getIgmpApfFilter()
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
+        // Using scapy to generate IGMPv2 report packet:
+        //   ether = Ether(src='00:11:22:33:44:55', dst='01:00:5e:7f:00:01')
+        //   ip = IP(src='10.0.0.2', dst='239.0.0.1')
+        //   igmp = IGMP(type=0x16, gaddr='239.0.0.1')
+        //   pkt = ether/ip/igmp
+        val v2ReportPkt = """
+            01005e7f000100112233445508004500001c000100000102c0dc0a000002ef0000011614fae9ef000001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(v2ReportPkt),
+            DROPPED_IGMP_REPORT
+        )
+
+        // Using scapy to generate IGMPv2 leave packet:
+        //   ether = Ether(src='00:11:22:33:44:55', dst='01:00:5e:7f:00:01')
+        //   ip = IP(src='10.0.0.2', dst='239.0.0.1')
+        //   igmp = IGMP(type=0x17, gaddr='239.0.0.1')
+        //   pkt = ether/ip/igmp
+        val v2LeaveReportPkt = """
+            01005e7f000100112233445508004500001c000100000102c0dc0a000002ef0000011714f9e9ef000001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(v2LeaveReportPkt),
+            DROPPED_IGMP_REPORT
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIgmpV2GeneralQueryReplied() {
+        val apfFilter = getIgmpApfFilter()
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
+        // Using scapy to generate IGMPv2 general query packet without router alert option:
+        //   ether = Ether(src='00:11:22:33:44:55', dst='01:00:5e:00:00:01')
+        //   ip = IP(src='10.0.0.2', dst='224.0.0.1')
+        //   igmp = IGMP(type=0x11)
+        //   pkt = ether/ip/igmp
+        val pkt = """
+            01005e00000100112233445508004500001c000100000102cfdc0a000002e00000011114eeeb00000000
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(pkt),
+            DROPPED_IGMP_V2_GENERAL_QUERY_REPLIED
+        )
+
+        val igmpv2ReportPkts = setOf(
+            // ###[ Ethernet ]###
+            //   dst       = 01:00:5e:00:00:01
+            //   src       = 02:03:04:05:06:07
+            //   type      = IPv4
+            // ###[ IP ]###
+            //      version   = 4
+            //      ihl       = 6
+            //      tos       = 0xc0
+            //      len       = 32
+            //      id        = 0
+            //      flags     = DF
+            //      frag      = 0
+            //      ttl       = 1
+            //      proto     = igmp
+            //      chksum    = 0xeb15
+            //      src       = 10.0.0.1
+            //      dst       = 239.0.0.1
+            //      \options   \
+            //       |###[ IP Option Router Alert ]###
+            //       |  copy_flag = 1
+            //       |  optclass  = control
+            //       |  option    = router_alert
+            //       |  length    = 4
+            //       |  alert     = router_shall_examine_packet
+            // ###[ IGMP ]###
+            //         type      = Version 2 - Membership Report
+            //         mrcode    = 0
+            //         chksum    = 0xfafd
+            //         gaddr     = 239.0.0.1
+            """
+            01005e000001020304050607080046c00020000040000102eb150a000001ef000001940400001600fafd
+            ef000001
+            """.replace("\\s+".toRegex(), "").trim().uppercase(),
+
+            // ###[ Ethernet ]###
+            //   dst       = 01:00:5e:00:00:02
+            //   src       = 02:03:04:05:06:07
+            //   type      = IPv4
+            // ###[ IP ]###
+            //      version   = 4
+            //      ihl       = 6
+            //      tos       = 0xc0
+            //      len       = 32
+            //      id        = 0
+            //      flags     = DF
+            //      frag      = 0
+            //      ttl       = 1
+            //      proto     = igmp
+            //      chksum    = 0xeb14
+            //      src       = 10.0.0.1
+            //      dst       = 239.0.0.2
+            //      \options   \
+            //       |###[ IP Option Router Alert ]###
+            //       |  copy_flag = 1
+            //       |  optclass  = control
+            //       |  option    = router_alert
+            //       |  length    = 4
+            //       |  alert     = router_shall_examine_packet
+            // ###[ IGMP ]###
+            //         type      = Version 2 - Membership Report
+            //         mrcode    = 0
+            //         chksum    = 0xfafc
+            //         gaddr     = 239.0.0.2
+            """
+            01005e000002020304050607080046c00020000040000102eb140a000001ef000002940400001600fafc
+            ef000002
+            """.replace("\\s+".toRegex(), "").trim().uppercase(),
+            // ###[ Ethernet ]###
+            //   dst       = 01:00:5e:00:00:03
+            //   src       = 02:03:04:05:06:07
+            //   type      = IPv4
+            // ###[ IP ]###
+            //      version   = 4
+            //      ihl       = 6
+            //      tos       = 0xc0
+            //      len       = 32
+            //      id        = 0
+            //      flags     = DF
+            //      frag      = 0
+            //      ttl       = 1
+            //      proto     = igmp
+            //      chksum    = 0xeb13
+            //      src       = 10.0.0.1
+            //      dst       = 239.0.0.3
+            //      \options   \
+            //       |###[ IP Option Router Alert ]###
+            //       |  copy_flag = 1
+            //       |  optclass  = control
+            //       |  option    = router_alert
+            //       |  length    = 4
+            //       |  alert     = router_shall_examine_packet
+            // ###[ IGMP ]###
+            //         type      = Version 2 - Membership Report
+            //         mrcode    = 0
+            //         chksum    = 0xfafb
+            //         gaddr     = 239.0.0.3
+            """
+            01005e000003020304050607080046c00020000040000102eb130a000001ef000003940400001600fafb
+            ef000003
+            """.replace("\\s+".toRegex(), "").trim().uppercase()
+        )
+
+        val transmitPackets = apfTestHelpers.getAllTransmittedPackets()
+            .map { HexDump.toHexString(it).uppercase() }.toSet()
+        assertEquals(igmpv2ReportPkts, transmitPackets)
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIgmpV2GeneralQueryWithRouterAlertOptionReplied() {
+        val apfFilter = getIgmpApfFilter()
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
+        // Using scapy to generate IGMPv2 general query packet with router alert option:
+        //   ether = Ether(src='00:11:22:33:44:55', dst='01:00:5e:00:00:01')
+        //   ip = IP(src='10.0.0.2', dst='224.0.0.1', options=[IPOption_Router_Alert()])
+        //   igmp = IGMP(type=0x11)
+        //   pkt = ether/ip/igmp
+        val pkt = """
+            01005e0000010011223344550800460000200001000001023ad40a000002e0000001940400001114eeeb
+            00000000
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(pkt),
+            DROPPED_IGMP_V2_GENERAL_QUERY_REPLIED
+        )
+
+        val igmpv2ReportPkts = setOf(
+            // ###[ Ethernet ]###
+            //   dst       = 01:00:5e:00:00:01
+            //   src       = 02:03:04:05:06:07
+            //   type      = IPv4
+            // ###[ IP ]###
+            //      version   = 4
+            //      ihl       = 6
+            //      tos       = 0xc0
+            //      len       = 32
+            //      id        = 0
+            //      flags     = DF
+            //      frag      = 0
+            //      ttl       = 1
+            //      proto     = igmp
+            //      chksum    = 0xeb15
+            //      src       = 10.0.0.1
+            //      dst       = 239.0.0.1
+            //      \options   \
+            //       |###[ IP Option Router Alert ]###
+            //       |  copy_flag = 1
+            //       |  optclass  = control
+            //       |  option    = router_alert
+            //       |  length    = 4
+            //       |  alert     = router_shall_examine_packet
+            // ###[ IGMP ]###
+            //         type      = Version 2 - Membership Report
+            //         mrcode    = 0
+            //         chksum    = 0xfafd
+            //         gaddr     = 239.0.0.1
+            """
+            01005e000001020304050607080046c00020000040000102eb150a000001ef000001940400001600fafd
+            ef000001
+            """.replace("\\s+".toRegex(), "").trim().uppercase(),
+
+            // ###[ Ethernet ]###
+            //   dst       = 01:00:5e:00:00:02
+            //   src       = 02:03:04:05:06:07
+            //   type      = IPv4
+            // ###[ IP ]###
+            //      version   = 4
+            //      ihl       = 6
+            //      tos       = 0xc0
+            //      len       = 32
+            //      id        = 0
+            //      flags     = DF
+            //      frag      = 0
+            //      ttl       = 1
+            //      proto     = igmp
+            //      chksum    = 0xeb14
+            //      src       = 10.0.0.1
+            //      dst       = 239.0.0.2
+            //      \options   \
+            //       |###[ IP Option Router Alert ]###
+            //       |  copy_flag = 1
+            //       |  optclass  = control
+            //       |  option    = router_alert
+            //       |  length    = 4
+            //       |  alert     = router_shall_examine_packet
+            // ###[ IGMP ]###
+            //         type      = Version 2 - Membership Report
+            //         mrcode    = 0
+            //         chksum    = 0xfafc
+            //         gaddr     = 239.0.0.2
+            """
+            01005e000002020304050607080046c00020000040000102eb140a000001ef000002940400001600fafc
+            ef000002
+            """.replace("\\s+".toRegex(), "").trim().uppercase(),
+
+            // ###[ Ethernet ]###
+            //   dst       = 01:00:5e:00:00:03
+            //   src       = 02:03:04:05:06:07
+            //   type      = IPv4
+            // ###[ IP ]###
+            //      version   = 4
+            //      ihl       = 6
+            //      tos       = 0xc0
+            //      len       = 32
+            //      id        = 0
+            //      flags     = DF
+            //      frag      = 0
+            //      ttl       = 1
+            //      proto     = igmp
+            //      chksum    = 0xeb13
+            //      src       = 10.0.0.1
+            //      dst       = 239.0.0.3
+            //      \options   \
+            //       |###[ IP Option Router Alert ]###
+            //       |  copy_flag = 1
+            //       |  optclass  = control
+            //       |  option    = router_alert
+            //       |  length    = 4
+            //       |  alert     = router_shall_examine_packet
+            // ###[ IGMP ]###
+            //         type      = Version 2 - Membership Report
+            //         mrcode    = 0
+            //         chksum    = 0xfafb
+            //         gaddr     = 239.0.0.3
+            """
+            01005e000003020304050607080046c00020000040000102eb130a000001ef000003940400001600fafb
+            ef000003
+            """.replace("\\s+".toRegex(), "").trim().uppercase()
+        )
+
+        val transmitPackets = apfTestHelpers.getAllTransmittedPackets()
+            .map { HexDump.toHexString(it).uppercase() }.toSet()
+        assertEquals(igmpv2ReportPkts, transmitPackets)
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIgmpV2GroupSpecificQueryPassed() {
+        val apfFilter = getIgmpApfFilter()
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
+        // Using scapy to generate IGMPv2 group specific query packet without router alert option:
+        //   ether = Ether(src='00:11:22:33:44:55', dst='01:00:5e:7f:00:01')
+        //   ip = IP(src='10.0.0.2', dst='239.0.0.1')
+        //   igmp = IGMP(type=0x11, gaddr='239.0.0.1')
+        //   pkt = ether/ip/igmp
+        val pkt = """
+            01005e7f000100112233445508004500001c000100000102c0dc0a000002ef0000011114ffe9ef000001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(pkt),
+            PASSED_IPV4
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIgmpV3ReportDropped() {
+        val apfFilter = getIgmpApfFilter()
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
+        // Using scapy to generate IGMPv3 report packet without router alert option:
+        //   ether = Ether(src='00:11:22:33:44:55', dst='01:00:5e:00:00:16')
+        //   ip = IP(src='10.0.0.2', dst='224.0.0.22')
+        //   igmp = IGMPv3(type=0x22)/IGMPv3mr(records=[IGMPv3gr(rtype=2, maddr='239.0.0.1')])
+        //   pkt = ether/ip/igmp
+        val pkt = """
+            01005e000001001122334455080045c00024000100000102cf140a000002e00000012200ecfc000000
+            0102000000ef000001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(pkt),
+            DROPPED_IGMP_REPORT
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIgmpV3GeneralQueryReplied() {
+        val apfFilter = getIgmpApfFilter()
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
+        // Using scapy to generate IGMPv3 general query packet without router alert option:
+        //   ether = Ether(src='00:11:22:33:44:55', dst='01:00:5e:00:00:01')
+        //   ip = IP(src='10.0.0.2', dst='224.0.0.1')
+        //   igmp = IGMPv3(type=0x11)/IGMPv3mq()
+        //   pkt = ether/ip/igmp
+        val pkt = """
+            01005e000001001122334455080045c00020000100000102cf180a000002e00000011114eeeb00000000
+            00000000
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(pkt),
+            DROPPED_IGMP_V3_GENERAL_QUERY_REPLIED
+        )
+
+        val transmittedIgmpv3Reports = apfTestHelpers.consumeTransmittedPackets(1)
+
+        // ###[ Ethernet ]###
+        //   dst       = 01:00:5e:00:00:16
+        //   src       = 02:03:04:05:06:07
+        //   type      = IPv4
+        // ###[ IP ]###
+        //      version   = 4
+        //      ihl       = 6
+        //      tos       = 0xc0
+        //      len       = 56
+        //      id        = 0
+        //      flags     = DF
+        //      frag      = 0
+        //      ttl       = 1
+        //      proto     = igmp
+        //      chksum    = 0xf9e8
+        //      src       = 10.0.0.1
+        //      dst       = 224.0.0.22
+        //      \options   \
+        //       |###[ IP Option Router Alert ]###
+        //       |  copy_flag = 1
+        //       |  optclass  = control
+        //       |  option    = router_alert
+        //       |  length    = 4
+        //       |  alert     = router_shall_examine_packet
+        // ###[ IGMPv3 ]###
+        //         type      = Version 3 Membership Report
+        //         mrcode    = 0
+        //         chksum    = 0xaf4
+        // ###[ IGMPv3mr ]###
+        //            res2      = 0x0
+        //            numgrp    = 3
+        //            \records   \
+        //             |###[ IGMPv3gr ]###
+        //             |  rtype     = Mode Is Exclude
+        //             |  auxdlen   = 0
+        //             |  numsrc    = 0
+        //             |  maddr     = 239.0.0.1
+        //             |  srcaddrs  = []
+        //             |###[ IGMPv3gr ]###
+        //             |  rtype     = Mode Is Exclude
+        //             |  auxdlen   = 0
+        //             |  numsrc    = 0
+        //             |  maddr     = 239.0.0.2
+        //             |  srcaddrs  = []
+        //             |###[ IGMPv3gr ]###
+        //             |  rtype     = Mode Is Exclude
+        //             |  auxdlen   = 0
+        //             |  numsrc    = 0
+        //             |  maddr     = 239.0.0.3
+        //             |  srcaddrs  = []
+        val igmpv3ReportPkt = """
+            01005e000016020304050607080046c00038000040000102f9e80a000001e00000169404000022000af40
+            000000302000000ef00000102000000ef00000202000000ef000003
+        """.replace("\\s+".toRegex(), "").trim()
+
+        assertContentEquals(
+            HexDump.hexStringToByteArray(igmpv3ReportPkt),
+            transmittedIgmpv3Reports[0]
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIgmpV3GeneralQueryWithRouterAlertOptionReplied() {
+        val apfFilter = getIgmpApfFilter()
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
+        // Using scapy to generate IGMPv3 general query packet with router alert option:
+        //   ether = Ether(src='00:11:22:33:44:55', dst='01:00:5e:00:00:01')
+        //   ip = IP(src='10.0.0.2', dst='224.0.0.1', options=[IPOption_Router_Alert()])
+        //   igmp = IGMPv3(type=0x11)/IGMPv3mq()
+        //   pkt = ether/ip/igmp
+        val pkt = """
+            01005e000001001122334455080046c000240001000001023a100a000002e0000001940400001114eeeb0
+            000000000000000
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(pkt),
+            DROPPED_IGMP_V3_GENERAL_QUERY_REPLIED
+        )
+
+        val transmittedIgmpv3Reports = apfTestHelpers.consumeTransmittedPackets(1)
+
+        // ###[ Ethernet ]###
+        //   dst       = 01:00:5e:00:00:16
+        //   src       = 02:03:04:05:06:07
+        //   type      = IPv4
+        // ###[ IP ]###
+        //      version   = 4
+        //      ihl       = 6
+        //      tos       = 0xc0
+        //      len       = 56
+        //      id        = 0
+        //      flags     = DF
+        //      frag      = 0
+        //      ttl       = 1
+        //      proto     = igmp
+        //      chksum    = 0xf9e8
+        //      src       = 10.0.0.1
+        //      dst       = 224.0.0.22
+        //      \options   \
+        //       |###[ IP Option Router Alert ]###
+        //       |  copy_flag = 1
+        //       |  optclass  = control
+        //       |  option    = router_alert
+        //       |  length    = 4
+        //       |  alert     = router_shall_examine_packet
+        // ###[ IGMPv3 ]###
+        //         type      = Version 3 Membership Report
+        //         mrcode    = 0
+        //         chksum    = 0xaf4
+        // ###[ IGMPv3mr ]###
+        //            res2      = 0x0
+        //            numgrp    = 3
+        //            \records   \
+        //             |###[ IGMPv3gr ]###
+        //             |  rtype     = Mode Is Exclude
+        //             |  auxdlen   = 0
+        //             |  numsrc    = 0
+        //             |  maddr     = 239.0.0.1
+        //             |  srcaddrs  = []
+        //             |###[ IGMPv3gr ]###
+        //             |  rtype     = Mode Is Exclude
+        //             |  auxdlen   = 0
+        //             |  numsrc    = 0
+        //             |  maddr     = 239.0.0.2
+        //             |  srcaddrs  = []
+        //             |###[ IGMPv3gr ]###
+        //             |  rtype     = Mode Is Exclude
+        //             |  auxdlen   = 0
+        //             |  numsrc    = 0
+        //             |  maddr     = 239.0.0.3
+        //             |  srcaddrs  = []
+        val igmpv3ReportPkt = """
+            01005e000016020304050607080046c00038000040000102f9e80a000001e00000169404000022000af40
+            000000302000000ef00000102000000ef00000202000000ef000003
+        """.replace("\\s+".toRegex(), "").trim()
+
+        assertContentEquals(
+            HexDump.hexStringToByteArray(igmpv3ReportPkt),
+            transmittedIgmpv3Reports[0]
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIgmpV3GroupSpecificQueryPassed() {
+        val apfFilter = getIgmpApfFilter()
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
+        // Using scapy to generate IGMPv3 group specific query packet
+        //   ether = Ether(src='00:11:22:33:44:55', dst='01:00:5e:7f:00:01')
+        //   ip = IP(src='10.0.0.2', dst='239.0.0.1')
+        //   igmp = IGMPv3(type=0x11)/IGMPv3mq(gaddr='239.0.0.1')
+        //   pkt = ether/ip/igmp
+        val pkt = """
+            01005e7f0001001122334455080045c00020000100000102c0180a000002ef0000011114ffe9ef000001
+            00000000
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(pkt),
+            PASSED_IPV4
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIgmpV3GroupAndSourceSpecificQueryPassed() {
+        val apfFilter = getIgmpApfFilter()
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
+        // Using scapy to generate IGMPv3 group and source specific query packet
+        //   ether = Ether(src='00:11:22:33:44:55', dst='01:00:5e:7f:00:01')
+        //   ip = IP(src='10.0.0.2', dst='239.0.0.1')
+        //   igmp = IGMPv3(type=0x11)/IGMPv3mq(gaddr='239.0.0.1', numsrc=1, srcaddrs=['10.0.0.1'])
+        //   pkt = ether/ip/igmp
+        val pkt = """
+            01005e7f0001001122334455080045c00024000100000102c0140a000002ef0000011114f5e7ef0000010
+            00000010a000001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(pkt),
+            PASSED_IPV4
+        )
+    }
+
+    private fun getMldApfFilter(): ApfFilter {
+        val mcastAddrs = listOf(
+            InetAddress.getByName("ff12::1:1111:1111") as Inet6Address,
+            InetAddress.getByName("ff12::1:2222:2222") as Inet6Address,
+            InetAddress.getByName("ff12::1:3333:3333") as Inet6Address,
+        )
+        val apfConfig = getDefaultConfig()
+        apfConfig.handleMldOffload = true
+
+        // mock IPv6 multicast address from /proc/net/igmp6
+        doReturn(mcastAddrs).`when`(dependencies).getIPv6MulticastAddresses(any())
+        val apfFilter = getApfFilter(apfConfig)
+        val ipv6LinkAddress = LinkAddress(hostLinkLocalIpv6Address, 64)
+        val lp = LinkProperties()
+        lp.addLinkAddress(ipv6LinkAddress)
+        apfFilter.setLinkProperties(lp)
+        return apfFilter
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIPv6PacketWithNonMldHopByHopPassed() {
+        val apfFilter = getMldApfFilter()
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
+        // Using scapy to generate MLDv1 general query with different HOPOPTS
+        //  ether = Ether(src='00:11:22:33:44:55', dst='33:33:11:11:11:11')
+        //  ipv6 = IPv6(src='fe80::fc01:83ff:fea6:3712', dst='ff02::1:1111:1111', hlim=1)
+        //  hopOpts = IPv6ExtHdrHopByHop(options=[RouterAlert(otype=3)])
+        //  mld = ICMPv6MLQuery()
+        //  pkt = ether/ipv6/hopOpts/mld
+        var invalidHopOptPkt = """
+            33331111111100112233445586dd6000000000200001fe80000000000000fc0183fffea63712ff020000
+            0000000000000001111111113a000302000001008200813b271000000000000000000000000000000000
+            0000
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(invalidHopOptPkt),
+            PASSED_IPV6_NON_ICMP
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testInvalidMldPacketDropped() {
+        val apfFilter = getMldApfFilter()
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
+        // Using scapy to generate MLDv1 general query with invalid source addr
+        //  ether = Ether(src='00:11:22:33:44:55', dst='33:33:11:11:11:11')
+        //  ipv6 = IPv6(src='ff02::1:4444:4444', dst='ff02::1:1111:1111', hlim=1)
+        //  hopOpts = IPv6ExtHdrHopByHop(options=[RouterAlert(otype=5)])
+        //  mld = ICMPv6MLQuery()
+        //  pkt = ether/ipv6/hopOpts/mld
+        var invalidSrcIpPkt = """
+            33331111111100112233445586dd6000000000200001ff020000000000000000000144444444ff02000
+            00000000000000001111111113a000502000001008200adea2710000000000000000000000000000000
+            000000
+        """.replace("\\s+".toRegex(), "").trim().uppercase()
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(invalidSrcIpPkt),
+            DROPPED_IPV6_MLD_INVALID
+        )
+
+        // Using scapy to generate MLDv1 general query with invalid hoplimit
+        //  ether = Ether(src='00:11:22:33:44:55', dst='33:33:11:11:11:11')
+        //  ipv6 = IPv6(src='fe80::fc01:83ff:fea6:3712', dst='ff02::1:1111:1111', hlim=5)
+        //  hopOpts = IPv6ExtHdrHopByHop(options=[RouterAlert(otype=5)])
+        //  mld = ICMPv6MLQuery()
+        //  pkt = ether/ipv6/hopOpts/mld
+        var invalidHopLimitPkt = """
+            33331111111100112233445586dd6000000000200005fe80000000000000fc0183fffea63712ff02000
+            00000000000000001111111113a000502000001008200813b2710000000000000000000000000000000
+            000000
+        """.replace("\\s+".toRegex(), "").trim()
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(invalidHopLimitPkt),
+            DROPPED_IPV6_MLD_INVALID
+        )
+
+        // Using scapy to generate MLDv1 general query packet with invalid destination address
+        //  ether = Ether(src='00:11:22:33:44:55', dst='33:33:00:00:00:01')
+        //  ipv6 = IPv6(src='fe80::fc01:83ff:fea6:3712', dst='ff03::1', hlim=1)
+        //  hopOpts = IPv6ExtHdrHopByHop(options=[RouterAlert(otype=5)])
+        //  mld = ICMPv6MLQuery()
+        //  pkt = ether/ipv6/hopOpts/mld
+        var pkt = """
+            33330000000100112233445586dd6000000000200001fe80000000000000fc0183fffea63712ff03000
+            00000000000000000000000013a000502000001008200a35c2710000000000000000000000000000000
+            000000
+        """.replace("\\s+".toRegex(), "").trim()
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(pkt),
+            DROPPED_IPV6_MLD_INVALID
+        )
+
+        // Using scapy to generate MLD message with invalid payload length 27
+        //  ether = Ether(src='00:11:22:33:44:55', dst='33:33:00:00:00:01')
+        //  ipv6 = IPv6(src='fe80::fc01:83ff:fea6:3712', dst='ff03::1', hlim=1)
+        //  hopOpts = IPv6ExtHdrHopByHop(options=[RouterAlert(otype=5)])
+        //  mld = ICMPv6MLQuery()
+        //  pkt = ether/ipv6/hopOpts/mld (and drop last byte)
+        var invalidPayloadLength27Pkt = """
+            33330000000100112233445586dd6000000000240001fe80000000000000fc0183fffea63712ff0200000
+            000000000000000000000013a000502000001008200a35927100000000000000000000000000000000000
+            00000000
+        """.replace("\\s+".toRegex(), "").trim()
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(invalidPayloadLength27Pkt),
+            DROPPED_IPV6_MLD_INVALID
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testMldV1ReportDropped() {
+        val apfFilter = getMldApfFilter()
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
+        // Using scapy to generate MLDv1 report
+        //  ether = Ether(src='00:11:22:33:44:55', dst='33:33:11:11:11:11')
+        //  ipv6 = IPv6(src='fe80::fc01:83ff:fea6:3712', dst='ff12::1:1111:1111', hlim=1)
+        //  hopOpts = IPv6ExtHdrHopByHop(options=[RouterAlert(otype=5)])
+        //  mld = ICMPv6MLReport(mladdr='ff12::1:1111:1111')
+        //  pkt = ether/ipv6/hopOpts/mld
+        var pkt = """
+            33331111111100112233445586dd6000000000200001fe80000000000000fc0183fffea63712ff12000
+            00000000000000001111111113a000502000001008300860500000000ff120000000000000000000111
+            111111
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(pkt),
+            DROPPED_IPV6_MLD_REPORT
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testMldV1DoneDropped() {
+        val apfFilter = getMldApfFilter()
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
+        // Using scapy to generate MLDv1 done
+        //  ether = Ether(src='00:11:22:33:44:55', dst='33:33:00:00:00:02')
+        //  ipv6 = IPv6(src='fe80::fc01:83ff:fea6:3712', dst='ff02::2', hlim=1)
+        //  hopOpts = IPv6ExtHdrHopByHop(options=[RouterAlert(otype=5)])
+        //  mld = ICMPv6MLDone(mladdr='ff12::1:1111:1111')
+        //  pkt = ether/ipv6/hopOpts/mld
+        var pkt = """
+            33330000000200112233445586dd6000000000200001fe80000000000000fc0183fffea63712ff020000
+            0000000000000000000000023a000502000001008400a73600000000ff12000000000000000000011111
+            1111
+        """.replace("\\s+".toRegex(), "").trim()
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(pkt),
+            DROPPED_IPV6_MLD_REPORT
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testMldV2ReportDropped() {
+        val apfFilter = getMldApfFilter()
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
+        // Using scapy to generate MLDv2 report
+        //  ether = Ether(src='00:11:22:33:44:55', dst='33:33:00:00:00:16')
+        //  ipv6 = IPv6(src='fe80::fc01:83ff:fea6:3712', dst='ff02::16', hlim=1)
+        //  hopOpts = IPv6ExtHdrHopByHop(options=[RouterAlert(otype=5)])
+        //  mld = ICMPv6MLReport2(records=[ICMPv6MLDMultAddrRec(dst='ff02::1:1111:1111')])
+        //  pkt = ether/ipv6/hopOpts/mld
+        var pkt = """
+            33330000001600112233445586dd6000000000240001fe80000000000000fc0183fffea63712ff020000
+            0000000000000000000000163a000502000001008f00982d0000000104000000ff020000000000000000
+            000111111111
+        """.replace("\\s+".toRegex(), "").trim()
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(pkt),
+            DROPPED_IPV6_MLD_REPORT
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testMldV1GeneralQueryReplied() {
+        val apfFilter = getMldApfFilter()
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
+        // Using scapy to generate MLDv1 general query
+        //  ether = Ether(src='00:11:22:33:44:55', dst='33:33:00:00:00:01')
+        //  ipv6 = IPv6(src='fe80::fc01:83ff:fea6:3712', dst='ff02::1', hlim=1)
+        //  hopOpts = IPv6ExtHdrHopByHop(options=[RouterAlert(otype=5)])
+        //  mld = ICMPv6MLQuery()
+        //  pkt = ether/ipv6/hopOpts/mld
+        var pkt = """
+            33330000000100112233445586dd6000000000200001fe80000000000000fc0183fffea63712ff02000
+            00000000000000000000000013a000502000001008200a35d2710000000000000000000000000000000
+            000000
+        """.replace("\\s+".toRegex(), "").trim()
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(pkt),
+            DROPPED_IPV6_MLD_V1_GENERAL_QUERY_REPLIED
+        )
+
+        val mldV1ReportPkts = setOf(
+            //  ###[ Ethernet ]###
+            //    dst       = 33:33:11:11:11:11
+            //    src       = 02:03:04:05:06:07
+            //    type      = IPv6
+            //  ###[ IPv6 ]###
+            //       version   = 6
+            //       tc        = 0
+            //       fl        = 0
+            //       plen      = None
+            //       nh        = Hop-by-Hop Option Header
+            //       hlim      = 1
+            //       src       = fe80::3
+            //       dst       = ff12::1:1111:1111
+            //  ###[ IPv6 Extension Header - Hop-by-Hop Options Header ]###
+            //          nh        = ICMPv6
+            //          len       = None
+            //          autopad   = On
+            //          \options   \
+            //           |###[ Router Alert ]###
+            //           |  otype     = Router Alert [00: skip, 0: Don't change en-route]
+            //           |  optlen    = 2
+            //           |  value     = None
+            //  ###[ MLD - Multicast Listener Report ]###
+            //             type      = MLD Report
+            //             code      = 0
+            //             cksum     = None
+            //             mrd       = 0
+            //             reserved  = 0
+            //             mladdr    = ff12::1:1111:1111
+            """
+            33331111111102030405060786dd6000000000200001fe800000000000000000000000000003ff120000
+            0000000000000001111111113a0005020000010083003bbd00000000ff12000000000000000000011111
+            1111
+            """.replace("\\s+".toRegex(), "").trim().uppercase(),
+            //  ###[ Ethernet ]###
+            //    dst       = 33:33:22:22:22:22
+            //    src       = 02:03:04:05:06:07
+            //    type      = IPv6
+            //  ###[ IPv6 ]###
+            //       version   = 6
+            //       tc        = 0
+            //       fl        = 0
+            //       plen      = None
+            //       nh        = Hop-by-Hop Option Header
+            //       hlim      = 1
+            //       src       = fe80::3
+            //       dst       = ff12::1:2222:2222
+            //  ###[ IPv6 Extension Header - Hop-by-Hop Options Header ]###
+            //          nh        = ICMPv6
+            //          len       = None
+            //          autopad   = On
+            //          \options   \
+            //           |###[ Router Alert ]###
+            //           |  otype     = Router Alert [00: skip, 0: Don't change en-route]
+            //           |  optlen    = 2
+            //           |  value     = None
+            //  ###[ MLD - Multicast Listener Report ]###
+            //             type      = MLD Report
+            //             code      = 0
+            //             cksum     = None
+            //             mrd       = 0
+            //             reserved  = 0
+            //             mladdr    = ff12::1:2222:2222
+            """
+            33332222222202030405060786dd6000000000200001fe800000000000000000000000000003ff120000
+            0000000000000001222222223a000502000001008300f77800000000ff12000000000000000000012222
+            2222
+            """.replace("\\s+".toRegex(), "").trim().uppercase(),
+            //  ###[ Ethernet ]###
+            //    dst       = 33:33:33:33:33:33
+            //    src       = 02:03:04:05:06:07
+            //    type      = IPv6
+            //  ###[ IPv6 ]###
+            //       version   = 6
+            //       tc        = 0
+            //       fl        = 0
+            //       plen      = None
+            //       nh        = Hop-by-Hop Option Header
+            //       hlim      = 1
+            //       src       = fe80::3
+            //       dst       = ff12::1:3333:3333
+            //  ###[ IPv6 Extension Header - Hop-by-Hop Options Header ]###
+            //          nh        = ICMPv6
+            //          len       = None
+            //          autopad   = On
+            //          \options   \
+            //           |###[ Router Alert ]###
+            //           |  otype     = Router Alert [00: skip, 0: Don't change en-route]
+            //           |  optlen    = 2
+            //           |  value     = None
+            //  ###[ MLD - Multicast Listener Report ]###
+            //             type      = MLD Report
+            //             code      = 0
+            //             cksum     = None
+            //             mrd       = 0
+            //             reserved  = 0
+            //             mladdr    = ff12::1:3333:3333
+            """
+            33333333333302030405060786dd6000000000200001fe800000000000000000000000000003ff120000
+            0000000000000001333333333a000502000001008300b33400000000ff12000000000000000000013333
+            3333
+            """.replace("\\s+".toRegex(), "").trim().uppercase()
+        )
+
+        val transmitPackets = apfTestHelpers.getAllTransmittedPackets()
+            .map { HexDump.toHexString(it).uppercase() }.toSet()
+        assertEquals(mldV1ReportPkts, transmitPackets)
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testMldV2GeneralQueryReplied() {
+        val apfFilter = getMldApfFilter()
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
+        // Using scapy to generate MLDv2 general query
+        //  ether = Ether(src='00:11:22:33:44:55', dst='33:33:00:00:00:01')
+        //  ipv6 = IPv6(src='fe80::fc01:83ff:fea6:3712', dst='ff02::1', hlim=1)
+        //  hopOpts = IPv6ExtHdrHopByHop(options=[RouterAlert(otype=5)])
+        //  mld = ICMPv6MLQuery2()
+        //  pkt = ether/ipv6/hopOpts/mld
+        var pkt = """
+            33330000000100112233445586dd6000000000240001fe80000000000000fc0183fffea63712ff02000
+            00000000000000000000000013a000502000001008200a3592710000000000000000000000000000000
+            00000000000000
+        """.replace("\\s+".toRegex(), "").trim()
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(pkt),
+            DROPPED_IPV6_MLD_V2_GENERAL_QUERY_REPLIED
+        )
+
+        val transmittedMldV2Reports = apfTestHelpers.consumeTransmittedPackets(1)
+        //  ###[ Ethernet ]###
+        //    dst       = 33:33:00:00:00:16
+        //    src       = 02:03:04:05:06:07
+        //    type      = IPv6
+        //  ###[ IPv6 ]###
+        //       version   = 6
+        //       tc        = 0
+        //       fl        = 0
+        //       plen      = None
+        //       nh        = Hop-by-Hop Option Header
+        //       hlim      = 1
+        //       src       = fe80::3
+        //       dst       = ff02::16
+        //  ###[ IPv6 Extension Header - Hop-by-Hop Options Header ]###
+        //          nh        = ICMPv6
+        //          len       = None
+        //          autopad   = On
+        //          \options   \
+        //           |###[ Router Alert ]###
+        //           |  otype     = Router Alert [00: skip, 0: Don't change en-route]
+        //           |  optlen    = 2
+        //           |  value     = None
+        //  ###[ MLDv2 - Multicast Listener Report ]###
+        //             type      = MLD Report Version 2
+        //             res       = 0
+        //             cksum     = None
+        //             reserved  = 0
+        //             records_number= None
+        //             \records   \
+        //              |###[ ICMPv6 MLDv2 - Multicast Address Record ]###
+        //              |  rtype     = 2
+        //              |  auxdata_len= None
+        //              |  sources_number= None
+        //              |  dst       = ff12::1:1111:1111
+        //              |  sources   = [  ]
+        //              |  auxdata   = b''
+        //              |###[ ICMPv6 MLDv2 - Multicast Address Record ]###
+        //              |  rtype     = 2
+        //              |  auxdata_len= None
+        //              |  sources_number= None
+        //              |  dst       = ff12::1:2222:2222
+        //              |  sources   = [  ]
+        //              |  auxdata   = b''
+        //              |###[ ICMPv6 MLDv2 - Multicast Address Record ]###
+        //              |  rtype     = 2
+        //              |  auxdata_len= None
+        //              |  sources_number= None
+        //              |  dst       = ff12::1:3333:3333
+        //              |  sources   = [  ]
+        //              |  auxdata   = b''
+        val mldV2ReportPkt = """
+            33330000001602030405060786dd60000000004c0001fe800000000000000000000000000003ff020000
+            0000000000000000000000163a000502000001008f00a2d80000000302000000ff120000000000000000
+            00011111111102000000ff12000000000000000000012222222202000000ff1200000000000000000001
+            33333333
+        """.replace("\\s+".toRegex(), "").trim()
+        assertContentEquals(
+            HexDump.hexStringToByteArray(mldV2ReportPkt),
+            transmittedMldV2Reports[0]
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testMldV1GroupSpecificQueryPassed() {
+        val apfFilter = getMldApfFilter()
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
+        // Using scapy to generate MLDv1 group specific query
+        //  ether = Ether(src='00:11:22:33:44:55', dst='33:33:00:00:00:01')
+        //  ipv6 = IPv6(src='fe80::fc01:83ff:fea6:3712', dst='ff02::1:1111:1111', hlim=1)
+        //  hopOpts = IPv6ExtHdrHopByHop(options=[RouterAlert(otype=5)])
+        //  mld = ICMPv6MLQuery(mladdr='ff02::1:1111:1111')
+        //  pkt = ether/ipv6/hopOpts/mld
+        var pkt = """
+            33330000000100112233445586dd6000000000200001fe80000000000000fc0183fffea63712ff020000
+            0000000000000001111111113a000502000001008200601527100000ff02000000000000000000011111
+            1111
+        """.replace("\\s+".toRegex(), "").trim()
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(pkt),
+            PASSED_IPV6_ICMP
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testMldV2GroupSpecificQueryPassed() {
+        val apfFilter = getMldApfFilter()
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
+        // Using scapy to generate MLDv2 group specific query
+        //  ether = Ether(src='00:11:22:33:44:55', dst='33:33:00:00:00:01')
+        //  ipv6 = IPv6(src='fe80::fc01:83ff:fea6:3712', dst='ff02::1:1111:1111', hlim=1)
+        //  hopOpts = IPv6ExtHdrHopByHop(options=[RouterAlert(otype=5)])
+        //  mld = ICMPv6MLQuery2(mladdr='ff02::1:1111:1111')
+        //  pkt = ether/ipv6/hopOpts/mld
+        var pkt = """
+            33330000000100112233445586dd6000000000240001fe80000000000000fc0183fffea63712ff020000
+            0000000000000001111111113a000502000001008200601127100000ff02000000000000000000011111
+            111100000000
+        """.replace("\\s+".toRegex(), "").trim()
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(pkt),
+            PASSED_IPV6_ICMP
         )
     }
 
@@ -563,12 +1978,12 @@
         val apfConfig = getDefaultConfig()
         apfConfig.multicastFilter = true
         val apfFilter = getApfFilter(apfConfig)
-        consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
         val linkAddress = LinkAddress(InetAddress.getByAddress(hostIpv4Address), 24)
         val lp = LinkProperties()
         lp.addLinkAddress(linkAddress)
         apfFilter.setLinkProperties(lp)
-        val program = consumeInstalledProgram(ipClientCallback, installCnt = 1)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
 
         // Using scapy to generate DHCP4 offer packet:
         //   ether = Ether(src='00:11:22:33:44:55', dst='ff:ff:ff:ff:ff:ff')
@@ -598,7 +2013,7 @@
             0000000000000000000000000000000000000000000000000000638253633501023604c0
             a801010104ffffff000304c0a80101330400015180060408080808ff
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(dhcp4Pkt),
@@ -612,7 +2027,7 @@
         val nonDhcpMcastPkt = """
             ffffffffffff001122334455080045000014000100004015d929c0a80101e0000001
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(nonDhcpMcastPkt),
@@ -626,7 +2041,7 @@
         val nonDhcpBcastPkt = """
             ffffffffffff001122334455080045000014000100004015b92bc0a80101ffffffff
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(nonDhcpBcastPkt),
@@ -640,7 +2055,7 @@
         val nonDhcpNetBcastPkt = """
             ffffffffffff001122334455080045000014000100004015ae2cc0a801010a0000ff
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(nonDhcpNetBcastPkt),
@@ -654,7 +2069,7 @@
         val nonDhcpUcastPkt = """
             020304050607001122334455080045000014000100004015f780c0a80101c0a80102
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(nonDhcpUcastPkt),
@@ -668,7 +2083,7 @@
         val nonDhcpUcastL2BcastPkt = """
             ffffffffffff001122334455080045000014000100004015f780c0a80101c0a80102
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(nonDhcpUcastL2BcastPkt),
@@ -679,9 +2094,9 @@
     @Test
     fun testArpFilterDropPktsOnV6OnlyNetwork() {
         val apfFilter = getApfFilter()
-        consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
         apfFilter.updateClatInterfaceState(true)
-        val program = consumeInstalledProgram(ipClientCallback, installCnt = 1)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
 
         // Drop ARP request packet when clat is enabled
         // Using scapy to generate ARP request packet:
@@ -691,8 +2106,8 @@
         val arpPkt = """
             010203040506000102030405080600010800060400015c857e3c74e1c0a8012200000000000000000000
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(arpPkt),
             DROPPED_ARP_V6_ONLY
@@ -722,9 +2137,9 @@
         apfConfig.multicastFilter = true
         apfConfig.ieee802_3Filter = true
         val apfFilter = getApfFilter(apfConfig)
-        consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
         apfFilter.addTcpKeepalivePacketFilter(1, parcel)
-        var program = consumeInstalledProgram(ipClientCallback, installCnt = 1)
+        var program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
 
         // Drop IPv4 keepalive ack
         // Using scapy to generate IPv4 TCP keepalive ack packet with seq + 1:
@@ -736,8 +2151,8 @@
             01020304050600010203040508004500002800010000400666c50a0000060a000005d4313039499602d2
             7e916116501020004b4f0000
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(keepaliveAckPkt),
             DROPPED_IPV4_KEEPALIVE_ACK
@@ -753,8 +2168,8 @@
             01020304050600010203040508004500002800010000400666c50a0000060a000005d431303949960336
             7e916115501020004aec0000
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(nonKeepaliveAckPkt1),
             PASSED_IPV4_UNICAST
@@ -771,8 +2186,8 @@
             01020304050600010203040508004500003200010000400666bb0a0000060a000005d4313039499602d27
             e91611650102000372c000000010203040506070809
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(nonKeepaliveAckPkt2),
             PASSED_IPV4_UNICAST
@@ -788,8 +2203,8 @@
             01020304050600010203040508004500002800010000400666c40a0000070a0000055ba0ff987e91610c4
             2f697155010200066e60000
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(otherSrcKeepaliveAck),
             PASSED_IPV4_UNICAST
@@ -797,16 +2212,16 @@
 
         // test IPv4 packets when TCP keepalive filter is removed
         apfFilter.removeKeepalivePacketFilter(1)
-        program = consumeInstalledProgram(ipClientCallback, installCnt = 1)
-        verifyProgramRun(
-            APF_VERSION_6,
+        program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(keepaliveAckPkt),
             PASSED_IPV4_UNICAST
         )
 
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(otherSrcKeepaliveAck),
             PASSED_IPV4_UNICAST
@@ -832,9 +2247,9 @@
         apfConfig.multicastFilter = true
         apfConfig.ieee802_3Filter = true
         val apfFilter = getApfFilter(apfConfig)
-        consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
         apfFilter.addNattKeepalivePacketFilter(1, parcel)
-        val program = consumeInstalledProgram(ipClientCallback, installCnt = 1)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
 
         // Drop IPv4 keepalive response packet
         // Using scapy to generate IPv4 NAT-T keepalive ack packet with payload 0xff:
@@ -846,8 +2261,8 @@
         val validNattPkt = """
             01020304050600010203040508004500001d00010000401166c50a0000060a000005119404000009d73cff
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(validNattPkt),
             DROPPED_IPV4_NATT_KEEPALIVE
@@ -863,8 +2278,8 @@
         val invalidNattPkt = """
             01020304050600010203040508004500001d00010000401166c50a0000060a000005119404000009d83cfe
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(invalidNattPkt),
             PASSED_IPV4_UNICAST
@@ -881,8 +2296,8 @@
             01020304050600010203040508004500002600010000401166bc0a0000060a000005119404000012c2120
             0010203040506070809
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(nonNattPkt),
             PASSED_IPV4_UNICAST
@@ -899,8 +2314,8 @@
             01020304050600010203040508004500002600010000401166bb0a0000070a000005119404000012c2110
             0010203040506070809
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(otherSrcNonNattPkt),
             PASSED_IPV4_UNICAST
@@ -910,7 +2325,7 @@
     @Test
     fun testIPv4TcpPort7Filter() {
         val apfFilter = getApfFilter()
-        val program = consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
 
         // Drop IPv4 TCP port 7 packet
         // Using scapy to generate IPv4 TCP port 7 packet:
@@ -922,8 +2337,8 @@
             01020304050600010203040508004500002800010000400666c50a0000060a00000500140007000000000
             0000000500220007bbd0000
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(tcpPort7Pkt),
             DROPPED_IPV4_TCP_PORT7_UNICAST
@@ -939,8 +2354,8 @@
             01020304050600010203040508004500002800012000400646c50a0000060a00000500140050000000000
             0000000500220007b740000
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(initialFragmentTcpPkt),
             PASSED_IPV4
@@ -956,8 +2371,8 @@
             01020304050600010203040508004500002800012064400646610a0000060a00000500140050000000000
             0000000500220007b740000
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(fragmentTcpPkt),
             PASSED_IPV4
@@ -969,14 +2384,14 @@
         val apfConfig = getDefaultConfig()
         apfConfig.multicastFilter = true
         val apfFilter = getApfFilter(apfConfig)
-        consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
         val lp = LinkProperties()
         for (addr in hostIpv6Addresses) {
             lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
         }
         apfFilter.setLinkProperties(lp)
         apfFilter.setDozeMode(true)
-        val program = consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
         // Using scapy to generate non ICMPv6 sent to ff00::/8 (multicast prefix) 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="ff00::1", nh=59)
@@ -985,8 +2400,8 @@
             ffffffffffff00112233445586dd6000000000003b4020010000000000000200001a11223344ff00000
             0000000000000000000000000
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(nonIcmpv6McastPkt),
             DROPPED_IPV6_NON_ICMP_MULTICAST
@@ -1001,8 +2416,8 @@
             02030405060700010203040586dd6000000000083aff20010000000000000200001a11223344ff00000
             000000000000000000000000180001a3a00000000
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(icmpv6EchoPkt),
             DROPPED_IPV6_NON_ICMP_MULTICAST
@@ -1012,13 +2427,13 @@
     @Test
     fun testIPv6PacketFilter() {
         val apfFilter = getApfFilter()
-        consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
         val lp = LinkProperties()
         for (addr in hostIpv6Addresses) {
             lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
         }
         apfFilter.setLinkProperties(lp)
-        val program = consumeInstalledProgram(ipClientCallback, installCnt = 1)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
         // Using scapy to generate non ICMPv6 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", nh=59)
@@ -1027,8 +2442,8 @@
             ffffffffffff00112233445586dd6000000000003b4020010000000000000200001a112233442001000
             0000000000200001a33441122
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(nonIcmpv6Pkt),
             PASSED_IPV6_NON_ICMP
@@ -1043,8 +2458,8 @@
             01020304050600010203040586dd6000000000183aff20010000000000000200001a11223344ff02000
             000000000000000000000000188007227a000000000000000000000000000000000000000
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(icmpv6McastNaPkt),
             DROPPED_IPV6_MULTICAST_NA
@@ -1058,18 +2473,63 @@
             01020304050600010203040586dd600000000000004020010000000000000200001a112233442001000
             0000000000200001a33441122
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(ipv6WithHopByHopOptionPkt),
-            PASSED_MLD
+            PASSED_IPV6_HOPOPTS
+        )
+    }
+
+    @Test
+    fun testRaFilterIgnoreReservedFieldInRdnssOption() {
+        val apfFilter = getApfFilter()
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
+        val lp = LinkProperties()
+        for (addr in hostIpv6Addresses) {
+            lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
+        }
+        apfFilter.setLinkProperties(lp)
+        var program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+        val ra1 = """
+            33330000000100c0babecafe86dd6e00000000783afffe800000000000002a0079e12e003f01ff0
+            200000000000000000000000000018600571140000e100000000000000000010100c0babecafe05
+            010000000023ee2602fff80064ff9b0000000000000000190500000012750020014860486000000
+            00000000000006420014860486000000000000000006464030440c000002a3000001c2000000000
+            2a0079e12e003f010000000000000000
+        """.replace("\\s+".toRegex(), "").trim()
+        val ra1Bytes = HexDump.hexStringToByteArray(ra1)
+        Os.write(raWriterSocket, ra1Bytes, 0, ra1Bytes.size)
+
+        program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            ra1Bytes,
+            DROPPED_RA
+        )
+
+        val ra2 = """
+            33330000000100c0babecafe86dd6e00000000783afffe800000000000002a0079e12e003f01ff0
+            200000000000000000000000000018600dd3040000e100000000000000000010100c0babecafe05
+            010000000023ee2602fff80064ff9b0000000000000000190579e00012750020014860486000000
+            00000000000006420014860486000000000000000006464030440c000002a3000001c2000000000
+            2a0079e12e003f010000000000000000
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            HexDump.hexStringToByteArray(ra2),
+            DROPPED_RA
         )
     }
 
     @Test
     fun testArpFilterDropPktsNoIPv4() {
         val apfFilter = getApfFilter()
-        val program = consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
 
         // Drop ARP request packet with invalid hw type
         // Using scapy to generate ARP request packet with invalid hw type :
@@ -1079,8 +2539,8 @@
         val invalidHwTypePkt = """
             01020304050600010203040508060003080000040001c0a8012200000000
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(invalidHwTypePkt),
             DROPPED_ARP_NON_IPV4
@@ -1094,8 +2554,8 @@
         val invalidProtoTypePkt = """
             010203040506000102030405080600010014060000015c857e3c74e1000000000000
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(invalidProtoTypePkt),
             DROPPED_ARP_NON_IPV4
@@ -1111,8 +2571,8 @@
             0000000000000000c0a8012200000000000000000000000000000000000000000000
             0000
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(invalidHwLenPkt),
             DROPPED_ARP_NON_IPV4
@@ -1128,8 +2588,8 @@
             00000000000000000000000000000000000000000000000000000000000000000000
             000000000000
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(invalidProtoLenPkt),
             DROPPED_ARP_NON_IPV4
@@ -1143,8 +2603,8 @@
         val invalidOpPkt = """
             010203040506000102030405080600010800060400055c857e3c74e1c0a8012200000000000000000000
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(invalidOpPkt),
             DROPPED_ARP_UNKNOWN
@@ -1158,8 +2618,8 @@
         val noHostArpReplyPkt = """
             010203040506000102030405080600010800060400025c857e3c74e10000000000000000000000000000
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(noHostArpReplyPkt),
             DROPPED_ARP_REPLY_SPA_NO_HOST
@@ -1173,8 +2633,8 @@
         val garpReplyPkt = """
             ffffffffffff000102030405080600010800060400025c857e3c74e1c0a8012200000000000000000000
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(garpReplyPkt),
             DROPPED_GARP_REPLY
@@ -1184,7 +2644,7 @@
     @Test
     fun testArpFilterPassPktsNoIPv4() {
         val apfFilter = getApfFilter()
-        val program = consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
         // Pass non-broadcast ARP reply packet
         // Using scapy to generate unicast ARP reply packet:
         // eth = Ether(src="00:01:02:03:04:05", dst="01:02:03:04:05:06")
@@ -1193,8 +2653,8 @@
         val nonBcastArpReplyPkt = """
             010203040506000102030405080600010800060400025c857e3c74e10102030400000000000000000000
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(nonBcastArpReplyPkt),
             PASSED_ARP_UNICAST_REPLY
@@ -1208,8 +2668,8 @@
         val arpRequestPkt = """
             ffffffffffff000102030405080600010800060400015c857e3c74e1c0a8012200000000000001020304
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(arpRequestPkt),
             PASSED_ARP_REQUEST
@@ -1219,12 +2679,12 @@
     @Test
     fun testArpFilterDropPktsWithIPv4() {
         val apfFilter = getApfFilter()
-        consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
         val linkAddress = LinkAddress(InetAddress.getByAddress(hostIpv4Address), 24)
         val lp = LinkProperties()
         lp.addLinkAddress(linkAddress)
         apfFilter.setLinkProperties(lp)
-        val program = consumeInstalledProgram(ipClientCallback, installCnt = 1)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
         // Drop ARP reply packet is not for the device
         // Using scapy to generate ARP reply packet not for the device:
         // eth = Ether(src="00:01:02:03:04:05", dst="FF:FF:FF:FF:FF:FF")
@@ -1233,8 +2693,8 @@
         val otherHostArpReplyPkt = """
             ffffffffffff000102030405080600010800060400025c857e3c74e1c0a8012200000000000001020304
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(otherHostArpReplyPkt),
             DROPPED_ARP_OTHER_HOST
@@ -1248,8 +2708,8 @@
         val otherHostArpRequestPkt = """
             ffffffffffff000102030405080600010800060400015c857e3c74e1c0a8012200000000000001020304
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(otherHostArpRequestPkt),
             DROPPED_ARP_OTHER_HOST
@@ -1259,12 +2719,12 @@
     @Test
     fun testArpFilterPassPktsWithIPv4() {
         val apfFilter = getApfFilter()
-        consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
         val linkAddress = LinkAddress(InetAddress.getByAddress(hostIpv4Address), 24)
         val lp = LinkProperties()
         lp.addLinkAddress(linkAddress)
         apfFilter.setLinkProperties(lp)
-        val program = consumeInstalledProgram(ipClientCallback, installCnt = 1)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
 
         // Using scapy to generate ARP broadcast reply packet:
         // eth = Ether(src="00:01:02:03:04:05", dst="FF:FF:FF:FF:FF:FF")
@@ -1273,8 +2733,8 @@
         val bcastArpReplyPkt = """
             ffffffffffff000102030405080600010800060400025c857e3c74e1c0a801220000000000000a000001
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
-            APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
             program,
             HexDump.hexStringToByteArray(bcastArpReplyPkt),
             PASSED_ARP_BROADCAST_REPLY
@@ -1286,12 +2746,12 @@
     @Test
     fun testArpTransmit() {
         val apfFilter = getApfFilter()
-        consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
         val linkAddress = LinkAddress(InetAddress.getByAddress(hostIpv4Address), 24)
         val lp = LinkProperties()
         lp.addLinkAddress(linkAddress)
         apfFilter.setLinkProperties(lp)
-        val program = consumeInstalledProgram(ipClientCallback, installCnt = 1)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
         val receivedArpPacketBuf = ArpPacket.buildArpPacket(
             arpBroadcastMacAddress,
             senderMacAddress,
@@ -1302,14 +2762,14 @@
         )
         val receivedArpPacket = ByteArray(ARP_ETHER_IPV4_LEN)
         receivedArpPacketBuf.get(receivedArpPacket)
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             receivedArpPacket,
             DROPPED_ARP_REQUEST_REPLIED
         )
 
-        val transmittedPacket = ApfJniUtils.getTransmittedPacket()
+        val transmittedPackets = apfTestHelpers.consumeTransmittedPackets(1)
         val expectedArpReplyBuf = ArpPacket.buildArpPacket(
             senderMacAddress,
             apfFilter.mHardwareAddress,
@@ -1322,21 +2782,21 @@
         expectedArpReplyBuf.get(expectedArpReplyPacket)
         assertContentEquals(
             expectedArpReplyPacket + ByteArray(18) { 0 },
-            transmittedPacket
+            transmittedPackets[0]
         )
     }
 
     @Test
     fun testArpOffloadDisabled() {
         val apfConfig = getDefaultConfig()
-        apfConfig.shouldHandleArpOffload = false
+        apfConfig.handleArpOffload = false
         val apfFilter = getApfFilter(apfConfig)
-        consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
         val linkAddress = LinkAddress(InetAddress.getByAddress(hostIpv4Address), 24)
         val lp = LinkProperties()
         lp.addLinkAddress(linkAddress)
         apfFilter.setLinkProperties(lp)
-        val program = consumeInstalledProgram(ipClientCallback, installCnt = 1)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
         val receivedArpPacketBuf = ArpPacket.buildArpPacket(
             arpBroadcastMacAddress,
             senderMacAddress,
@@ -1347,7 +2807,7 @@
         )
         val receivedArpPacket = ByteArray(ARP_ETHER_IPV4_LEN)
         receivedArpPacketBuf.get(receivedArpPacket)
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             receivedArpPacket,
@@ -1361,7 +2821,7 @@
         doReturn(listOf<ByteArray>()).`when`(dependencies).getAnycast6Addresses(any())
         val apfFilter = getApfFilter()
         // validate NS packet check when there is no IPv6 address
-        val program = consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
         // 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)
@@ -1373,11 +2833,11 @@
             00000020010000000000000200001A33441122
         """.replace("\\s+".toRegex(), "").trim()
         // when there is no IPv6 addresses -> pass NS packet
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(nsPkt),
-            PASSED_IPV6_NS_NO_ADDRESS
+            PASSED_IPV6_ICMP
         )
     }
 
@@ -1385,7 +2845,7 @@
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     fun testNsFilter() {
         val apfFilter = getApfFilter()
-        consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
         val lp = LinkProperties()
         for (addr in hostIpv6Addresses) {
             lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
@@ -1403,9 +2863,9 @@
         }
 
         apfFilter.setLinkProperties(lp)
-        consumeInstalledProgram(ipClientCallback, installCnt = 1)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
         apfFilter.updateClatInterfaceState(true)
-        val program = consumeInstalledProgram(ipClientCallback, installCnt = 1)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
 
         // validate Ethernet dst address check
         // Using scapy to generate IPv6 NS packet:
@@ -1420,7 +2880,7 @@
             000020010000000000000200001A334411220201000102030405
         """.replace("\\s+".toRegex(), "").trim()
         // invalid unicast ether dst -> pass
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(nonHostDstMacNsPkt),
@@ -1439,7 +2899,7 @@
             0000000020010000000000000200001A334411220201000102030405
         """.replace("\\s+".toRegex(), "").trim()
         // mcast dst mac is not one of solicited mcast mac derived from one of device's ip -> pass
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(nonMcastDstMacNsPkt),
@@ -1459,7 +2919,7 @@
         """.replace("\\s+".toRegex(), "").trim()
         // mcast dst mac is one of solicited mcast mac derived from one of device's ip
         // -> drop and replied
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(hostMcastDstMacNsPkt),
@@ -1478,7 +2938,7 @@
             00000000000200001A334411220101000102030405
         """.replace("\\s+".toRegex(), "").trim()
         // mcast dst mac is broadcast address -> drop and replied
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(broadcastNsPkt),
@@ -1499,7 +2959,7 @@
             00000020010000000000000200001A334411220101000102030405
         """.replace("\\s+".toRegex(), "").trim()
         // dst ip is one of device's ip -> drop and replied
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(validHostDstIpNsPkt),
@@ -1519,7 +2979,7 @@
             0101000102030405
         """.replace("\\s+".toRegex(), "").trim()
         // dst ip is device's anycast address -> drop and replied
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(validHostAnycastDstIpNsPkt),
@@ -1538,7 +2998,7 @@
             E30000000020010000000000000200001A334411220101000102030405
         """.replace("\\s+".toRegex(), "").trim()
         // unicast dst ip is not one of device's ip -> pass
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(nonHostUcastDstIpNsPkt),
@@ -1557,7 +3017,7 @@
             1C0000000020010000000000000200001A334411220101000102030405
         """.replace("\\s+".toRegex(), "").trim()
         // mcast dst ip is not one of solicited mcast ip derived from one of device's ip -> pass
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(nonHostMcastDstIpNsPkt),
@@ -1576,7 +3036,7 @@
                     "000020010000000000000200001A334411220101000102030405"
         // mcast dst ip is one of solicited mcast ip derived from one of device's ip
         //   -> drop and replied
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(hostMcastDstIpNsPkt),
@@ -1597,7 +3057,7 @@
             000200001A334411220101010203040506
         """.replace("\\s+".toRegex(), "").trim()
         // payload len < 24 -> drop
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(shortNsPkt),
@@ -1616,7 +3076,7 @@
             00000000000200001A444455550101010203040506
         """.replace("\\s+".toRegex(), "").trim()
         // target ip is not one of device's ip -> drop
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(otherHostNsPkt),
@@ -1635,7 +3095,7 @@
             00000020010000000000000200001A334411220101010203040506
         """.replace("\\s+".toRegex(), "").trim()
         // hoplimit is not 255 -> drop
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(invalidHoplimitNsPkt),
@@ -1654,7 +3114,7 @@
             00000020010000000000000200001A334411220101010203040506
         """.replace("\\s+".toRegex(), "").trim()
         // icmp6 code is not 0 -> drop
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(invalidIcmpCodeNsPkt),
@@ -1673,11 +3133,11 @@
             16CE0000000020010000000000000200001A123456780101010203040506
         """.replace("\\s+".toRegex(), "").trim()
         // target ip is one of tentative address -> pass
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(tentativeTargetIpNsPkt),
-            PASSED_IPV6_NS_TENTATIVE
+            PASSED_IPV6_ICMP
         )
 
         // Using scapy to generate IPv6 NS packet:
@@ -1692,7 +3152,7 @@
             00000020010000000000000200001C225566660101010203040506
         """.replace("\\s+".toRegex(), "").trim()
         // target ip is none of {non-tentative, anycast} -> drop
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(invalidTargetIpNsPkt),
@@ -1711,11 +3171,11 @@
             00001A334411220201020304050607
         """.replace("\\s+".toRegex(), "").trim()
         // DAD NS request -> pass
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(dadNsPkt),
-            PASSED_IPV6_NS_DAD
+            PASSED_IPV6_ICMP
         )
 
         // Using scapy to generate IPv6 NS packet:
@@ -1729,11 +3189,11 @@
             000000000200001A33441122
         """.replace("\\s+".toRegex(), "").trim()
         // payload len < 32 -> pass
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(noOptionNsPkt),
-            PASSED_IPV6_NS_NO_SLLA_OPTION
+            PASSED_IPV6_ICMP
         )
 
         // Using scapy to generate IPv6 NS packet:
@@ -1748,7 +3208,7 @@
             000020010000000000000200001A334411220101010203040506
         """.replace("\\s+".toRegex(), "").trim()
         // non-DAD src IPv6 is FF::/8 -> drop
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(nonDadMcastSrcIpPkt),
@@ -1767,7 +3227,7 @@
             140000000020010000000000000200001A334411220101010203040506
         """.replace("\\s+".toRegex(), "").trim()
         // non-DAD src IPv6 is 00::/8 -> drop
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(nonDadLoopbackSrcIpPkt),
@@ -1788,11 +3248,11 @@
             05060101010203040506
         """.replace("\\s+".toRegex(), "").trim()
         // non-DAD with multiple options, SLLA in 2nd option -> pass
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(sllaNotFirstOptionNsPkt),
-            PASSED_IPV6_NS_NO_SLLA_OPTION
+            PASSED_IPV6_ICMP
         )
 
         // Using scapy to generate IPv6 NS packet:
@@ -1807,11 +3267,11 @@
             20010000000000000200001A334411220201010203040506
         """.replace("\\s+".toRegex(), "").trim()
         // non-DAD with one option but not SLLA -> pass
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(noSllaOptionNsPkt),
-            PASSED_IPV6_NS_NO_SLLA_OPTION
+            PASSED_IPV6_ICMP
         )
 
         // Using scapy to generate IPv6 NS packet:
@@ -1827,7 +3287,7 @@
             0506
         """.replace("\\s+".toRegex(), "").trim()
         // non-DAD, SLLA is multicast MAC -> drop
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(mcastMacSllaOptionNsPkt),
@@ -1846,8 +3306,9 @@
         }
 
         apfFilter.setLinkProperties(lp)
-        val program = consumeInstalledProgram(ipClientCallback, installCnt = 3)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
         val validIpv6Addresses = hostIpv6Addresses + hostAnycast6Addresses
+        val expectPackets = mutableListOf<ByteArray>()
         for (addr in validIpv6Addresses) {
             // unicast solicited NS request
             val receivedUcastNsPacket = generateNsPacket(
@@ -1858,14 +3319,13 @@
                 addr
             )
 
-            verifyProgramRun(
+            apfTestHelpers.verifyProgramRun(
                 apfFilter.mApfVersionSupported,
                 program,
                 receivedUcastNsPacket,
                 DROPPED_IPV6_NS_REPLIED_NON_DAD
             )
 
-            val transmittedUcastPacket = ApfJniUtils.getTransmittedPacket()
             val expectedUcastNaPacket = generateNaPacket(
                 apfFilter.mHardwareAddress,
                 senderMacAddress,
@@ -1874,11 +3334,7 @@
                 0xe0000000.toInt(), //  R=1, S=1, O=1
                 addr
             )
-
-            assertContentEquals(
-                expectedUcastNaPacket,
-                transmittedUcastPacket
-            )
+            expectPackets.add(expectedUcastNaPacket)
 
             val solicitedMcastAddr = NetworkStackUtils.ipv6AddressToSolicitedNodeMulticast(
                 InetAddress.getByAddress(addr) as Inet6Address
@@ -1895,14 +3351,13 @@
                 addr
             )
 
-            verifyProgramRun(
+            apfTestHelpers.verifyProgramRun(
                 apfFilter.mApfVersionSupported,
                 program,
                 receivedMcastNsPacket,
                 DROPPED_IPV6_NS_REPLIED_NON_DAD
             )
 
-            val transmittedMcastPacket = ApfJniUtils.getTransmittedPacket()
             val expectedMcastNaPacket = generateNaPacket(
                 apfFilter.mHardwareAddress,
                 senderMacAddress,
@@ -1911,11 +3366,12 @@
                 0xe0000000.toInt(), // R=1, S=1, O=1
                 addr
             )
+            expectPackets.add(expectedMcastNaPacket)
+        }
 
-            assertContentEquals(
-                expectedMcastNaPacket,
-                transmittedMcastPacket
-            )
+        val transmitPackets = apfTestHelpers.consumeTransmittedPackets(expectPackets.size)
+        for (i in transmitPackets.indices) {
+            assertContentEquals(expectPackets[i], transmitPackets[i])
         }
     }
 
@@ -1931,7 +3387,7 @@
             lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
         }
         apfFilter.setLinkProperties(lp)
-        val program = consumeInstalledProgram(ipClientCallback, installCnt = 3)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
         // 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)
@@ -1943,14 +3399,14 @@
             0200001A11223344FF0200000000000000000001FF4411228700952D0000
             000020010000000000000200001A334411220101000102030405
         """.replace("\\s+".toRegex(), "").trim()
-        verifyProgramRun(
+        apfTestHelpers.verifyProgramRun(
             apfFilter.mApfVersionSupported,
             program,
             HexDump.hexStringToByteArray(hostMcastDstIpNsPkt),
             DROPPED_IPV6_NS_REPLIED_NON_DAD
         )
 
-        val transmitPkt = ApfJniUtils.getTransmittedPacket()
+        val transmitPkts = apfTestHelpers.consumeTransmittedPackets(1)
         // 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)
@@ -1964,14 +3420,14 @@
         """.replace("\\s+".toRegex(), "").trim()
         assertContentEquals(
             HexDump.hexStringToByteArray(expectedNaPacket),
-            transmitPkt
+            transmitPkts[0]
         )
     }
 
     @Test
     fun testNdOffloadDisabled() {
         val apfConfig = getDefaultConfig()
-        apfConfig.shouldHandleNdOffload = false
+        apfConfig.handleNdOffload = false
         val apfFilter = getApfFilter(apfConfig)
         val lp = LinkProperties()
         for (addr in hostIpv6Addresses) {
@@ -1979,7 +3435,7 @@
         }
 
         apfFilter.setLinkProperties(lp)
-        val program = consumeInstalledProgram(ipClientCallback, installCnt = 3)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
         val validIpv6Addresses = hostIpv6Addresses + hostAnycast6Addresses
         for (addr in validIpv6Addresses) {
             // unicast solicited NS request
@@ -1991,7 +3447,7 @@
                 addr
             )
 
-            verifyProgramRun(
+            apfTestHelpers.verifyProgramRun(
                 apfFilter.mApfVersionSupported,
                 program,
                 receivedUcastNsPacket,
@@ -2013,7 +3469,7 @@
                 addr
             )
 
-            verifyProgramRun(
+            apfTestHelpers.verifyProgramRun(
                 apfFilter.mApfVersionSupported,
                 program,
                 receivedMcastNsPacket,
@@ -2022,77 +3478,1864 @@
         }
     }
 
+    private fun getApfWithIpv6PingOffloadEnabled(
+        enableMultiCastFilter: Boolean = true,
+        inDozeMode: Boolean = false
+    ): Pair<ApfFilter, ByteArray> {
+        val apfConfig = getDefaultConfig()
+        apfConfig.multicastFilter = enableMultiCastFilter
+        apfConfig.handleIpv6PingOffload = true
+        val apfFilter = getApfFilter(apfConfig)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
+        if (inDozeMode) {
+            apfFilter.setDozeMode(inDozeMode)
+            apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+        }
+        val lp = LinkProperties()
+        lp.addLinkAddress(LinkAddress(hostLinkLocalIpv6Address, 64))
+        apfFilter.setLinkProperties(lp)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+        return Pair(apfFilter, program)
+    }
+
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     @Test
-    fun testRegisterOffloadEngine() {
+    fun testIpv6EchoRequestReplied() {
+        doReturn(64).`when`(dependencies).getIpv6DefaultHopLimit(ifParams.name)
+        val (apfFilter, program) = getApfWithIpv6PingOffloadEnabled()
+        // Using scapy to generate IPv6 echo request packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="02:03:04:05:06:07")
+        // ip = IPv6(src="fe80::1", dst="fe80::03")
+        // icmp = ICMPv6EchoRequest(id=1, seq=123)
+        // pkt = eth/ip/icmp/b"hello"
+        val ipv6EchoRequestPkt = """
+            02030405060701020304050686dd60000000000d3a40fe80000000000000000
+            0000000000001fe80000000000000000000000000000380003e640001007b68
+            656c6c6f
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(ipv6EchoRequestPkt),
+            DROPPED_IPV6_ICMP6_ECHO_REQUEST_REPLIED
+        )
+        val transmitPkt = apfTestHelpers.consumeTransmittedPackets(1)[0]
+
+        // ###[ Ethernet ]###
+        //  dst       = 01:02:03:04:05:06
+        //  src       = 02:03:04:05:06:07
+        //  type      = IPv6
+        // ###[ IPv6 ]###
+        //      version   = 6
+        //      tc        = 0
+        //      fl        = 0
+        //      plen      = 13
+        //      nh        = ICMPv6
+        //      hlim      = 64
+        //      src       = fe80::3
+        //      dst       = fe80::1
+        // ###[ ICMPv6 Echo Reply ]###
+        //         type      = Echo Reply
+        //         code      = 0
+        //         cksum     = 0x3d64
+        //         id        = 0x1
+        //         seq       = 0x7b
+        //         data      = b'hello'
+        val expectedReply = """
+            01020304050602030405060786DD60000000000D3A40FE80000000000000000
+            0000000000003FE80000000000000000000000000000181003D640001007B68
+            656C6C6F
+        """.replace("\\s+".toRegex(), "").trim()
+        assertContentEquals(
+            HexDump.hexStringToByteArray(expectedReply),
+            transmitPkt
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIpv6EchoRequestRepliedInDozeMode() {
+        doReturn(64).`when`(dependencies).getIpv6DefaultHopLimit(ifParams.name)
+        val (apfFilter, program) = getApfWithIpv6PingOffloadEnabled(inDozeMode = true)
+        // Using scapy to generate IPv6 echo request packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="02:03:04:05:06:07")
+        // ip = IPv6(src="fe80::1", dst="fe80::03")
+        // icmp = ICMPv6EchoRequest(id=1, seq=123)
+        // pkt = eth/ip/icmp/b"hello"
+        val ipv6EchoRequestPkt = """
+            02030405060701020304050686dd60000000000d3a40fe80000000000000000
+            0000000000001fe80000000000000000000000000000380003e640001007b68
+            656c6c6f
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(ipv6EchoRequestPkt),
+            DROPPED_IPV6_ICMP6_ECHO_REQUEST_REPLIED
+        )
+        val transmitPkt = apfTestHelpers.consumeTransmittedPackets(1)[0]
+
+        // ###[ Ethernet ]###
+        //  dst       = 01:02:03:04:05:06
+        //  src       = 02:03:04:05:06:07
+        //  type      = IPv6
+        // ###[ IPv6 ]###
+        //      version   = 6
+        //      tc        = 0
+        //      fl        = 0
+        //      plen      = 13
+        //      nh        = ICMPv6
+        //      hlim      = 64
+        //      src       = fe80::3
+        //      dst       = fe80::1
+        // ###[ ICMPv6 Echo Reply ]###
+        //         type      = Echo Reply
+        //         code      = 0
+        //         cksum     = 0x3d64
+        //         id        = 0x1
+        //         seq       = 0x7b
+        //         data      = b'hello'
+        val expectedReply = """
+            01020304050602030405060786DD60000000000D3A40FE80000000000000000
+            0000000000003FE80000000000000000000000000000181003D640001007B68
+            656C6C6F
+        """.replace("\\s+".toRegex(), "").trim()
+        assertContentEquals(
+            HexDump.hexStringToByteArray(expectedReply),
+            transmitPkt
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testCorruptedIpv6IcmpPacketDropped() {
+        val (apfFilter, program) = getApfWithIpv6PingOffloadEnabled()
+        // Using scapy to generate corrupted IPv6 ping packet
+        // eth = Ether(src="01:02:03:04:05:06", dst="02:03:04:05:06:07")
+        // ip = IPv6(src="fe80::1", dst="ff02::fb")
+        // icmp = ICMPv6EchoRequest(id=1, seq=123)
+        // pkt = eth/ip/icmp
+        // (drop the last byte in the packet)
+        val ipv6EchoRequestPkt = """
+            02030405060701020304050686dd6000000000083a40fe80000000000000000
+            0000000000001fe8000000000000000000000000000038000823b000100
+        """.replace("\\s+".toRegex(), "").trim()
+
+         apfTestHelpers.verifyProgramRun(
+             apfFilter.mApfVersionSupported,
+             program,
+             HexDump.hexStringToByteArray(ipv6EchoRequestPkt),
+             DROPPED_IPV6_ICMP6_ECHO_REQUEST_INVALID
+         )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIpv6EchoRequestToOtherHostPassed() {
+        val (apfFilter, program) = getApfWithIpv4PingOffloadEnabled()
+        // Using scapy to generate IPv6 echo request packet to other host:
+        // eth = Ether(src="01:02:03:04:05:06", dst="02:03:04:05:06:07")
+        // ip = IPv6(src="fe80::1", dst="fe80::02")
+        // icmp = ICMPv6EchoRequest(id=1, seq=123)
+        // pkt = eth/ip/icmp/b"hello"
+        val ipv6EchoRequestPkt = """
+            02030405060701020304050686dd60000000000d3a40fe80000000000000000
+            0000000000001fe80000000000000000000000000000280003e650001007b68
+            656c6c6f
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(ipv6EchoRequestPkt),
+            PASSED_IPV6_ICMP
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIpv6EchoReplyPassed() {
+        val (apfFilter, program) = getApfWithIpv4PingOffloadEnabled()
+        // Using scapy to generate IPv6 echo reply packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="02:03:04:05:06:07")
+        // ip = IPv6(src="fe80::1", dst="fe80::03")
+        // icmp = ICMPv6EchoReply(id=1, seq=123)
+        // pkt = eth/ip/icmp
+        val ipv6EchoReplyPkt = """
+            02030405060701020304050686dd6000000000083a40fe80000000000000000
+            0000000000001fe8000000000000000000000000000038100813b0001007b
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(ipv6EchoReplyPkt),
+            PASSED_IPV6_ICMP
+        )
+    }
+
+    private fun getApfWithIpv4PingOffloadEnabled(
+        enableMultiCastFilter: Boolean = true
+    ): Pair<ApfFilter, ByteArray> {
         val apfConfig = getDefaultConfig()
-        apfConfig.shouldHandleMdnsOffload = true
+        apfConfig.multicastFilter = enableMultiCastFilter
+        apfConfig.handleIpv4PingOffload = true
         val apfFilter = getApfFilter(apfConfig)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
+        val linkAddress = LinkAddress(InetAddress.getByAddress(hostIpv4Address), 24)
+        val lp = LinkProperties()
+        lp.addLinkAddress(linkAddress)
+        apfFilter.setLinkProperties(lp)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+        return Pair(apfFilter, program)
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIpv4EchoRequestReplied() {
+        doReturn(64).`when`(dependencies).ipv4DefaultTtl
+        val (apfFilter, program) = getApfWithIpv4PingOffloadEnabled()
+        // Using scapy to generate IPv4 echo request packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="02:03:04:05:06:07")
+        // ip = IP(src="10.0.0.2", dst="10.0.0.1")
+        // icmp = ICMP(id=1, seq=123)
+        // pkt = eth/ip/icmp/b"hello"
+        val ipv4EchoRequestPkt = """
+            02030405060701020304050608004500002100010000400166d90a0000020a0
+            000010800b3b10001007b68656c6c6f
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(ipv4EchoRequestPkt),
+            DROPPED_IPV4_PING_REQUEST_REPLIED
+        )
+
+        val transmitPkt = apfTestHelpers.consumeTransmittedPackets(1)[0]
+
+        // ###[ Ethernet ]###
+        //   dst       = 01:02:03:04:05:06
+        //   src       = 02:03:04:05:06:07
+        //   type      = IPv4
+        // ###[ IP ]###
+        //      version   = 4
+        //      ihl       = 5
+        //      tos       = 0x0
+        //      len       = 33
+        //      id        = 1
+        //      flags     =
+        //      frag      = 0
+        //      ttl       = 64
+        //      proto     = icmp
+        //      chksum    = 0x66d9
+        //      src       = 10.0.0.1
+        //      dst       = 10.0.0.2
+        //      \options   \
+        // ###[ ICMP ]###
+        //         type      = echo-reply
+        //         code      = 0
+        //         chksum    = 0xbbb1
+        //         id        = 0x1
+        //         seq       = 0x7b
+        //         unused    = b''
+        // ###[ Raw ]###
+        //            load      = b'hello'
+        val expectedReply = """
+            01020304050602030405060708004500002100010000400166D90A0000010A0
+            000020000BBB10001007B68656C6C6F
+        """.replace("\\s+".toRegex(), "").trim()
+        assertContentEquals(
+            HexDump.hexStringToByteArray(expectedReply),
+            transmitPkt
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testCorruptedIpv4IcmpPacketDropped() {
+        val (apfFilter, program) = getApfWithIpv4PingOffloadEnabled()
+        // Using scapy to generate corrupted icmp packet
+        // eth = Ether(src="01:02:03:04:05:06", dst="02:03:04:05:06:07")
+        // ip = IP(proto=1, src="10.0.0.2", dst="10.0.0.1")
+        // pkt = eth/ip/b"hello"
+        val ipv4EchoRequestPkt = """
+            02030405060701020304050608004500001900010000400166e10a0000020a0
+            0000168656c6c6f
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(ipv4EchoRequestPkt),
+            DROPPED_IPV4_ICMP_INVALID
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIpv4EchoRequestWithOptionPassed() {
+        val (apfFilter, program) = getApfWithIpv4PingOffloadEnabled()
+        // Using scapy to generate IPv4 echo request packet with option:
+        // eth = Ether(src="01:02:03:04:05:06", dst="02:03:04:05:06:07")
+        // ip = IP(src="10.0.0.2", dst="10.0.0.1", options=IPOption(b'\x94\x04\x00\x00'))
+        // icmp = ICMP(id=1, seq=123)
+        // pkt = eth/ip/icmp/b"hello"
+        val ipv4EchoRequestPkt = """
+            020304050607010203040506080046000025000100004001d1d00a0000020a0
+            00001940400000800b3b10001007b68656c6c6f
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(ipv4EchoRequestPkt),
+            PASSED_IPV4_UNICAST
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIpv4EchoRequestToOtherHostPassed() {
+        val (apfFilter, program) = getApfWithIpv4PingOffloadEnabled()
+        // Using scapy to generate IPv4 echo request packet to other host:
+        // eth = Ether(src="01:02:03:04:05:06", dst="02:03:04:05:06:07")
+        // ip = IP(src="10.0.0.2", dst="10.0.0.111")
+        // icmp = ICMP(id=1, seq=123)
+        // pkt = eth/ip/icmp/b"hello"
+        val ipv4EchoRequestPkt = """
+            020304050607010203040506080045000021000100004001666b0a0000020a0
+            0006f0800b3b10001007b68656c6c6f
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(ipv4EchoRequestPkt),
+            PASSED_IPV4_UNICAST
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testBroadcastIpv4EchoRequestPassed() {
+        val (apfFilter, program) = getApfWithIpv4PingOffloadEnabled(enableMultiCastFilter = false)
+        // Using scapy to generate broadcast IPv4 echo request packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="ff:ff:ff:ff:ff:ff")
+        // ip = IP(src="10.0.0.2", dst="10.0.0.255")
+        // icmp = ICMP(id=1, seq=123)
+        // pkt = eth/ip/icmp/b"hello"
+        val ipv4EchoRequestPkt = """
+            ffffffffffff01020304050608004500002100010000400165db0a0000020a0
+            000ff0800b3b10001007b68656c6c6f
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(ipv4EchoRequestPkt),
+            PASSED_IPV4
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIpv4EchoReplyPassed() {
+        val (apfFilter, program) = getApfWithIpv4PingOffloadEnabled()
+        // Using scapy to generate IPv4 echo reply packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="02:03:04:05:06:07")
+        // ip = IP(src="10.0.0.2", dst="10.0.0.1")
+        // icmp = ICMP(type=0, id=1, seq=123)
+        // pkt = eth/ip/icmp/b"hello"
+        val ipv4EchoReplyPkt = """
+            02030405060701020304050608004500002100010000400166d90a0000020a0
+            000010000bbb10001007b68656c6c6f
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(ipv4EchoReplyPkt),
+            PASSED_IPV4_UNICAST
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testOffloadServiceInfoUpdateTriggersProgramInstall() {
+        val apfConfig = getDefaultConfig()
+        apfConfig.handleMdnsOffload = true
+        val apfFilter = getApfFilter(apfConfig)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
         val captor = ArgumentCaptor.forClass(OffloadEngine::class.java)
         verify(nsdManager).registerOffloadEngine(
-                eq(ifParams.name),
-                anyLong(),
-                anyLong(),
-                any(),
-                captor.capture()
+            eq(ifParams.name),
+            anyLong(),
+            anyLong(),
+            any(),
+            captor.capture()
         )
         val offloadEngine = captor.value
-        val info1 = OffloadServiceInfo(
-                OffloadServiceInfo.Key("TestServiceName", "_advertisertest._tcp"),
-                listOf(),
-                "Android_test.local",
-                byteArrayOf(0x01, 0x02, 0x03, 0x04),
-                0,
-                OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
-        )
-        val info2 = OffloadServiceInfo(
-                OffloadServiceInfo.Key("TestServiceName2", "_advertisertest._tcp"),
-                listOf(),
-                "Android_test.local",
-                byteArrayOf(0x01, 0x02, 0x03, 0x04),
-                0,
-                OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
-        )
-        val updatedInfo1 = OffloadServiceInfo(
-                OffloadServiceInfo.Key("TestServiceName", "_advertisertest._tcp"),
-                listOf(),
-                "Android_test.local",
-                byteArrayOf(),
-                0,
-                OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
-        )
-        handler.post { offloadEngine.onOffloadServiceUpdated(info1) }
-        handlerThread.waitForIdle(TIMEOUT_MS)
-        assertContentEquals(listOf(info1), apfFilter.mOffloadServiceInfos)
-        handler.post { offloadEngine.onOffloadServiceUpdated(info2) }
-        handlerThread.waitForIdle(TIMEOUT_MS)
-        assertContentEquals(listOf(info1, info2), apfFilter.mOffloadServiceInfos)
-        handler.post { offloadEngine.onOffloadServiceUpdated(updatedInfo1) }
-        handlerThread.waitForIdle(TIMEOUT_MS)
-        assertContentEquals(listOf(info2, updatedInfo1), apfFilter.mOffloadServiceInfos)
-        handler.post { offloadEngine.onOffloadServiceRemoved(updatedInfo1) }
-        handlerThread.waitForIdle(TIMEOUT_MS)
-        assertContentEquals(listOf(info2), apfFilter.mOffloadServiceInfos)
+        visibleOnHandlerThread(handler) {
+            offloadEngine.onOffloadServiceUpdated(castOffloadInfo.value)
+        }
 
-        handler.post { apfFilter.shutdown() }
-        handlerThread.waitForIdle(TIMEOUT_MS)
-        verify(nsdManager).unregisterOffloadEngine(any())
+        verify(apfController).installPacketFilter(any(), any())
+
+        visibleOnHandlerThread(handler) { apfFilter.shutdown() }
+        verify(nsdManager).unregisterOffloadEngine(eq(offloadEngine))
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testCorruptedOffloadServiceInfoUpdateNotTriggerNewProgramInstall() {
+        val apfConfig = getDefaultConfig()
+        apfConfig.handleMdnsOffload = true
+        val apfFilter = getApfFilter(apfConfig)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
+        val captor = ArgumentCaptor.forClass(OffloadEngine::class.java)
+        verify(nsdManager).registerOffloadEngine(
+            eq(ifParams.name),
+            anyLong(),
+            anyLong(),
+            any(),
+            captor.capture()
+        )
+        val offloadEngine = captor.value
+        visibleOnHandlerThread(handler) {
+            offloadEngine.onOffloadServiceUpdated(castOffloadInfo.value)
+        }
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+        val corruptedOffloadInfo = OffloadServiceInfo(
+            OffloadServiceInfo.Key("gambit", "_${"a".repeat(63)}._tcp"),
+            listOf(),
+            "Android_f47ac10b58cc4b88bc3f5e7a81e59872.local",
+            byteArrayOf(0x01, 0x02, 0x03, 0x04),
+            0,
+            OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+        )
+        visibleOnHandlerThread(handler) {
+            offloadEngine.onOffloadServiceUpdated(corruptedOffloadInfo)
+        }
+        verify(apfController, never()).installPacketFilter(any(), any())
+    }
+
+    private fun getApfWithMdnsOffloadEnabled(
+        apfRam: Int = 4096,
+        mcFilter: Boolean = true,
+        v6Only: Boolean = false,
+        addedOffloadInfos: List<FromU<OffloadServiceInfo>> = listOf(
+            castOffloadInfo,
+            tvRemoteOffloadInfo,
+            manySubtypeOffloadInfo,
+            manySubtypeOffloadInfo
+        ),
+        removedOffloadInfos: List<FromU<OffloadServiceInfo>> = listOf(),
+        raReaderSocket: FileDescriptor = raReadSocket
+    ): Pair<ApfFilter, ByteArray> {
+        val localNsdManager = mock(NsdManager::class.java)
+        doReturn(localNsdManager).`when`(context).getSystemService(NsdManager::class.java)
+        doReturn(raReaderSocket).`when`(dependencies).createPacketReaderSocket(anyInt())
+        val apfConfig = getDefaultConfig()
+        apfConfig.apfRamSize = apfRam
+        apfConfig.handleMdnsOffload = true
+        if (mcFilter) {
+            apfConfig.multicastFilter = true
+        }
+        val apfFilter = getApfFilter(apfConfig)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
+        val captor = ArgumentCaptor.forClass(OffloadEngine::class.java)
+        verify(localNsdManager).registerOffloadEngine(
+            eq(ifParams.name),
+            anyLong(),
+            anyLong(),
+            any(),
+            captor.capture()
+        )
+        val offloadEngine = captor.value
+        val lp = LinkProperties()
+        if (v6Only) {
+            apfFilter.updateClatInterfaceState(true)
+            apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+        } else {
+            val ipv4LinkAddress = LinkAddress(InetAddress.getByAddress(hostIpv4Address), 24)
+            lp.addLinkAddress(ipv4LinkAddress)
+        }
+        val ipv6LinkAddress = LinkAddress(hostLinkLocalIpv6Address, 64)
+        lp.addLinkAddress(ipv6LinkAddress)
+        apfFilter.setLinkProperties(lp)
+        var program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+
+        if (addedOffloadInfos.isNotEmpty()) {
+            visibleOnHandlerThread(handler) {
+                addedOffloadInfos.forEach { offloadEngine.onOffloadServiceUpdated(it.value) }
+            }
+            program = apfTestHelpers.consumeInstalledProgram(
+                apfController,
+                installCnt = addedOffloadInfos.size
+            )
+        }
+        if (removedOffloadInfos.isNotEmpty()) {
+            visibleOnHandlerThread(handler) {
+                removedOffloadInfos.forEach { offloadEngine.onOffloadServiceRemoved(it.value) }
+            }
+            program = apfTestHelpers.consumeInstalledProgram(
+                apfController,
+                installCnt = removedOffloadInfos.size
+            )
+        }
+        return Pair(apfFilter, program)
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIPv4MdnsQueryReplied() {
+        val (apfFilter, program) = getApfWithMdnsOffloadEnabled(mcFilter = false)
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="01:00:5e:00:00:fb")
+        // ip = IP(src="10.0.0.3", dst="224.0.0.251")
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qd=DNSQR(qname="_googlecast._tcp.local", qtype="PTR"))
+        // pkt = eth/ip/udp/dns
+        val castIPv4MdnsPtrQuery = """
+            01005e0000fb0102030405060800450000440001000040118faa0a000003e00
+            000fb14e914e900309fa50000010000010000000000000b5f676f6f676c6563
+            617374045f746370056c6f63616c00000c0001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(castIPv4MdnsPtrQuery),
+            DROPPED_MDNS_REPLIED
+        )
+
+        var transmitPkt = apfTestHelpers.consumeTransmittedPackets(1)[0]
+
+        // ###[ Ethernet ]###
+        //   dst       = 01:00:5e:00:00:fb
+        //   src       = 02:03:04:05:06:07
+        //   type      = IPv4
+        // ###[ IP ]###
+        //      version   = 4
+        //      ihl       = 5
+        //      tos       = 0x0
+        //      len       = 514
+        //      id        = 0
+        //      flags     = DF
+        //      frag      = 0
+        //      ttl       = 255
+        //      proto     = udp
+        //      chksum    = 0x8eee
+        //      src       = 10.0.0.1
+        //      dst       = 224.0.0.251
+        //      \options   \
+        // ###[ UDP ]###
+        //         sport     = mdns
+        //         dport     = mdns
+        //         len       = 494
+        //         chksum    = 0x2f0d
+        // ###[ DNS ]###
+        //           id        = 0
+        //           qr        = 1
+        //           opcode    = QUERY
+        //           aa        = 1
+        //           tc        = 0
+        //           rd        = 0
+        //           ra        = 0
+        //           z         = 0
+        //           ad        = 0
+        //           cd        = 0
+        //           rcode     = ok
+        //           qdcount   = 0
+        //           ancount   = 7
+        //           nscount   = 0
+        //           arcount   = 0
+        //           \qd        \
+        //           \an        \
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b'_googlecast._tcp.local.'
+        //            |  type      = PTR
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = b'gambit-3cb56c6253638b3641e3d289013cc0ae._googlecast._tcp.local.'
+        //            |###[ DNS SRV Resource Record ]###
+        //            |  rrname    = b'\xc0.'
+        //            |  type      = SRV
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  priority  = 12320
+        //            |  weight    = 12320
+        //            |  port      = 14384
+        //            |  target    = b'9 3cb56c62-5363-8b36-41e3-d289013cc0ae.local..'
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b'\xc0.'
+        //            |  type      = TXT
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = [b' "id=3cb56c6253638b3641e3d289013cc0ae cd=8ECC37F6755390D005DFC02F8EC0D4FA rm=4ABD579644ACFCCF ve=05 md=gambit ic=/setup/icon.png fn=gambit a=264709 st=0 bs=FA8FFD2242A7 nf=1 rs= ']
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b'Android_f47ac10b58cc4b88bc3f5e7a81e59872.local.'
+        //            |  type      = A
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = 100.89.85.228
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b' (Android_f47ac10b58cc4b88bc3f5e7a81e59872\xc0\x1d\x00\x01\x00\x01\x00\x00\x00x\x00\x04dYU\xe4\xc1W\x00.\x00\x01\x00\x00\x00x\x00\x10\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xc1W\x00\x1c.'
+        //            |  type      = AAAA
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = fe80::3
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b' (Android_f47ac10b58cc4b88bc3f5e7a81e59872\xc0\x1d\x00\x01\x00\x01\x00\x00\x00x\x00\x04dYU\xe4\xc1W\x00.\x00\x01\x00\x00\x00x\x00\x10\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xc1W\x00\x1c.'
+        //            |  type      = AAAA
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = 200a::3
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b' (Android_f47ac10b58cc4b88bc3f5e7a81e59872\xc0\x1d\x00\x01\x00\x01\x00\x00\x00x\x00\x04dYU\xe4\xc1W\x00.\x00\x01\x00\x00\x00x\x00\x10\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xc1W\x00\x1c.'
+        //            |  type      = AAAA
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = 200b::3
+        //           \ns        \
+        //           \ar        \
+        val expectedIPv4CastMdnsReply = """
+            01005E0000FB02030405060708004500020200004000FF118EEE0A000001E00
+            000FB14E914E901EE2F0D0000840000000007000000000B5F676F6F676C6563
+            617374045F746370056C6F63616C00000C000100000078002A2767616D62697
+            42D336362353663363235333633386233363431653364323839303133636330
+            6165C00C01C0000021000100000078003430203020383030392033636235366
+            336322D353336332D386233362D343165332D6432383930313363633061652E
+            6C6F63616C2E01C000001000010000007800B3B2202269643D3363623536633
+            6323533363338623336343165336432383930313363633061652063643D3845
+            434333374636373535333930443030354446433032463845433044344641207
+            26D3D344142443537393634344143464343462076653D3035206D643D67616D
+            6269742069633D2F73657475702F69636F6E2E706E6720666E3D67616D62697
+            420613D3236343730392073743D302062733D46413846464432323432413720
+            6E663D312072733D2028416E64726F69645F663437616331306235386363346
+            2383862633366356537613831653539383732C01D0001000100000078000464
+            5955E4C157001C0001000000780010FE800000000000000000000000000003C
+            157001C0001000000780010200A0000000000000000000000000003C157001C
+            0001000000780010200B0000000000000000000000000003
+        """.replace("\\s+".toRegex(), "").trim()
+
+        assertContentEquals(
+            HexDump.hexStringToByteArray(expectedIPv4CastMdnsReply),
+            transmitPkt
+        )
+
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="01:00:5e:00:00:fb")
+        // ip = IP(src="10.0.0.3", dst="224.0.0.251")
+        // udp = UDP(dport=5353, sport=5353)
+        // questions = [
+        //   DNSQR(qname="_airplay._tcp.local", qtype="PTR"),
+        //   DNSQR(qname="gambit-3cb56c6253638b3641e3d289013cc0ae._googlecast._tcp.local", qtype="TXT")
+        // ]
+        // dns = dns_compress(DNS(qd=questions))
+        // pkt = eth/ip/udp/dns
+        val castIPv4MdnsTxtQuery = """
+            01005e0000fb01020304050608004500007b0001000040118f730a000003e00
+            000fb14e914e900675712000001000002000000000000085f616972706c6179
+            045f746370056c6f63616c00000c00012767616d6269742d336362353663363
+            23533363338623336343165336432383930313363633061650b5f676f6f676c
+            6563617374c01500100001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(castIPv4MdnsTxtQuery),
+            DROPPED_MDNS_REPLIED
+        )
+
+        transmitPkt = apfTestHelpers.consumeTransmittedPackets(1)[0]
+
+        assertContentEquals(
+            HexDump.hexStringToByteArray(expectedIPv4CastMdnsReply),
+            transmitPkt
+        )
+
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="01:00:5e:00:00:fb")
+        // ip = IP(src="10.0.0.3", dst="224.0.0.251")
+        // udp = UDP(dport=5353, sport=5353)
+        // questions = [
+        //   DNSQR(qname="_airplay._tcp.local", qtype="PTR"),
+        //   DNSQR(qname="gambit-3cb56c6253638b3641e3d289013cc0ae._googlecast._tcp.local", qtype="SRV")
+        // ]
+        // dns = dns_compress(DNS(qd=questions))
+        // pkt = eth/ip/udp/dns
+        val castIPv4MdnsSRVQuery = """
+            01005e0000fb01020304050608004500007b0001000040118f730a000003e00
+            000fb14e914e900674612000001000002000000000000085f616972706c6179
+            045f746370056c6f63616c00000c00012767616d6269742d336362353663363
+            23533363338623336343165336432383930313363633061650b5f676f6f676c
+            6563617374c01500210001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(castIPv4MdnsSRVQuery),
+            DROPPED_MDNS_REPLIED
+        )
+
+        transmitPkt = apfTestHelpers.consumeTransmittedPackets(1)[0]
+
+        assertContentEquals(
+            HexDump.hexStringToByteArray(expectedIPv4CastMdnsReply),
+            transmitPkt
+        )
+
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="01:00:5e:00:00:fb")
+        // ip = IP(src="10.0.0.3", dst="224.0.0.251")
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qd=DNSQR(qname="_androidtvremote2._tcp.local", qtype="PTR"))
+        // pkt = eth/ip/udp/dns
+        val tvRemoteIPv4MdnsPtrQuery = """
+            01005e0000fb01020304050608004500004a0001000040118fa40a000003e00
+            000fb14e914e900366966000001000001000000000000115f616e64726f6964
+            747672656d6f746532045f746370056c6f63616c00000c0001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(tvRemoteIPv4MdnsPtrQuery),
+            DROPPED_MDNS_REPLIED
+        )
+
+        transmitPkt = apfTestHelpers.consumeTransmittedPackets(1)[0]
+
+        // ###[ Ethernet ]###
+        //  dst       = 01:00:5e:00:00:fb
+        //  src       = 02:03:04:05:06:07
+        //  type      = IPv4
+        // ###[ IP ]###
+        //      version   = 4
+        //      ihl       = 5
+        //      tos       = 0x0
+        //      len       = 332
+        //      id        = 0
+        //      flags     = DF
+        //      frag      = 0
+        //      ttl       = 255
+        //      proto     = udp
+        //      chksum    = 0x8fa4
+        //      src       = 10.0.0.1
+        //      dst       = 224.0.0.251
+        //      \options   \
+        // ###[ UDP ]###
+        //         sport     = mdns
+        //         dport     = mdns
+        //         len       = 312
+        //         chksum    = 0xf867
+        // ###[ DNS ]###
+        //            id        = 0
+        //           qr        = 1
+        //           opcode    = QUERY
+        //           aa        = 1
+        //           tc        = 0
+        //           rd        = 0
+        //           ra        = 0
+        //           z         = 0
+        //           ad        = 0
+        //           cd        = 0
+        //           rcode     = ok
+        //           qdcount   = 0
+        //           ancount   = 7
+        //           nscount   = 0
+        //           arcount   = 0
+        //           \qd        \
+        //           \an        \
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b'_androidtvremote2._tcp.local.'
+        //            |  type      = PTR
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = b'gambit._androidtvremote2._tcp.local.'
+        //            |###[ DNS SRV Resource Record ]###
+        //            |  rrname    = b'gambit._androidtvremote2._tcp.local.'
+        //            |  type      = SRV
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  priority  = 12320
+        //            |  weight    = 12320
+        //            |  port      = 13876
+        //            |  target    = b'6 Android_2570595cc11d4af4a4b7146b946eeb9e.local.'
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b'gambit._androidtvremote2._tcp.local.'
+        //            |  type      = TXT
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = [b'"bt=3C:4E:56:76:1E:E9"']
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b'Android_f47ac10b58cc4b88bc3f5e7a81e59872.local.'
+        //            |  type      = A
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = 100.89.85.228
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b'Android_f47ac10b58cc4b88bc3f5e7a81e59872.local.'
+        //            |  type      = AAAA
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = fe80::3
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b'Android_f47ac10b58cc4b88bc3f5e7a81e59872.local.'
+        //            |  type      = AAAA
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = 200a::3
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b'Android_f47ac10b58cc4b88bc3f5e7a81e59872.local.'
+        //            |  type      = AAAA
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = 200b::3
+        //           \ns        \
+        //           \ar        \
+        val expectedIPv4tvRemoteMdnsReply = """
+            01005E0000FB02030405060708004500014C00004000FF118FA40A000001E00
+            000FB14E914E90138F867000084000000000700000000115F616E64726F6964
+            747672656D6F746532045F746370056C6F63616C00000C00010000007800090
+            667616D626974C00CC03400210001000000780037302030203634363620416E
+            64726F69645F323537303539356363313164346166346134623731343662393
+            43665656239652E6C6F63616CC03400100001000000780017162262743D3343
+            3A34453A35363A37363A31453A45392228416E64726F69645F6634376163313
+            062353863633462383862633366356537613831653539383732C02300010001
+            000000780004645955E4C0A3001C0001000000780010FE80000000000000000
+            0000000000003C0A3001C0001000000780010200A0000000000000000000000
+            000003C0A3001C0001000000780010200B0000000000000000000000000003
+        """.replace("\\s+".toRegex(), "").trim()
+
+        assertContentEquals(
+            HexDump.hexStringToByteArray(expectedIPv4tvRemoteMdnsReply),
+            transmitPkt
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIPv4MdnsQueryDropped() {
+        val (apfFilter, program) = getApfWithMdnsOffloadEnabled(
+            removedOffloadInfos = listOf(tvRemoteOffloadInfo)
+        )
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="01:00:5e:00:00:fb")
+        // ip = IP(src="10.0.0.3", dst="224.0.0.251")
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qd=DNSQR(qname="_airplay._tcp.local", qtype="PTR"))
+        // pkt = eth/ip/udp/dns
+        val airplayIPv4MdnsPtrQuery = """
+            01005e0000fb0102030405060800450000410001000040118fad0a000003e00
+            000fb14e914e9002d8203000001000001000000000000085f616972706c6179
+            045f746370056c6f63616c00000c0001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(airplayIPv4MdnsPtrQuery),
+            DROPPED_MDNS
+        )
+
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="01:00:5e:00:00:fb")
+        // ip = IP(src="10.0.0.3", dst="224.0.0.251")
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qd=DNSQR(qname="_androidtvremote2._tcp.local", qtype="PTR"))
+        // pkt = eth/ip/udp/dns
+        val tvRemoteIPv4MdnsPtrQuery = """
+            01005e0000fb01020304050608004500004a0001000040118fa40a000003e00
+            000fb14e914e900366966000001000001000000000000115f616e64726f6964
+            747672656d6f746532045f746370056c6f63616c00000c0001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(tvRemoteIPv4MdnsPtrQuery),
+            DROPPED_MDNS
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIPv4MdnsQueryWithOptionPassed() {
+        val (apfFilter, program) = getApfWithMdnsOffloadEnabled(mcFilter = false)
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="01:00:5e:00:00:fb")
+        // ip = IP(src="10.0.0.3", dst="224.0.0.251", options=IPOption(b'\x94\x04\x00\x00'))
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qd=DNSQR(qname="_googlecast._tcp.local", qtype="PTR"))
+        // pkt = eth/ip/udp/dns
+        val castIPv4MdnsPtrQueryWithOption = """
+            01005e0000fb010203040506080046000048000100004011faa10a000003e00
+            000fb9404000014e914e900309fa50000010000010000000000000b5f676f6f
+            676c6563617374045f746370056c6f63616c00000c0001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(castIPv4MdnsPtrQueryWithOption),
+            PASSED_IPV4
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIPv4MdnsQueryDroppedOnV6OnlyNetwork() {
+        val (apfFilter, program) = getApfWithMdnsOffloadEnabled(mcFilter = false, v6Only = true)
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="01:00:5e:00:00:fb")
+        // ip = IP(src="10.0.0.3", dst="224.0.0.251")
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qd=DNSQR(qname="_googlecast._tcp.local", qtype="PTR"))
+        // pkt = eth/ip/udp/dns
+        val castIPv4MdnsPtrQuery = """
+            01005e0000fb0102030405060800450000440001000040118faa0a000003e00
+            000fb14e914e900309fa50000010000010000000000000b5f676f6f676c6563
+            617374045f746370056c6f63616c00000c0001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(castIPv4MdnsPtrQuery),
+            DROPPED_IPV4_NON_DHCP4
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIPv4MdnsReplyPassed() {
+        val (apfFilter, program) = getApfWithMdnsOffloadEnabled(mcFilter = false)
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="01:00:5e:00:00:fb")
+        // ip = IP(src="10.0.0.3", dst="224.0.0.251")
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qr=1, aa=1, rd=0, qd=None, an=DNSRR(rrname="_androidtvremote2._tcp.local", type="PTR", rdata="gambit._androidtvremote2._tcp.local", ttl=120))
+        // pkt = eth/ip/udp/dns
+        val tvRemoteIPv4MdnsPtrAnswer = """
+            01005e0000fb0102030405060800450000750001000040118f790a000003e00
+            000fb14e914e9006169b4000084000000000100000000115f616e64726f6964
+            747672656d6f746532045f746370056c6f63616c00000c00010000007800250
+            667616d626974115f616e64726f6964747672656d6f746532045f746370056c
+            6f63616c00
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(tvRemoteIPv4MdnsPtrAnswer),
+            PASSED_MDNS
+        )
+    }
+
+    fun testIPv6MdnsQueryReplied() {
+        val (apfFilter, program) = getApfWithMdnsOffloadEnabled(mcFilter = false)
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="33:33:00:00:00:FB")
+        // ip = IPv6(src="fe80::1", dst="ff02::fb")
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qd=DNSQR(qname="_googlecast._tcp.local", qtype="PTR"))
+        // pkt = eth/ip/udp/dns
+        val castIPv6MdnsPtrQuery = """
+            3333000000fb01020304050686dd6000000000301140fe80000000000000000
+            0000000000001ff0200000000000000000000000000fb14e914e900308c2400
+            00010000010000000000000b5f676f6f676c6563617374045f746370056c6f6
+            3616c00000c0001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(castIPv6MdnsPtrQuery),
+            DROPPED_MDNS_REPLIED
+        )
+
+        var transmitPkt = apfTestHelpers.consumeTransmittedPackets(1)[0]
+
+        // ###[ Ethernet ]###
+        //  dst       = 33:33:00:00:00:fb
+        //  src       = 02:03:04:05:06:07
+        //  type      = IPv6
+        // ###[ IPv6 ]###
+        //      version   = 6
+        //      tc        = 0
+        //      fl        = 0
+        //      plen      = 494
+        //      nh        = UDP
+        //      hlim      = 255
+        //      src       = fe80::3
+        //      dst       = ff02::fb
+        // ###[ UDP ]###
+        //         sport     = mdns
+        //         dport     = mdns
+        //         len       = 494
+        //         chksum    = 0x1b88
+        // ###[ DNS ]###
+        //           id        = 0
+        //           qr        = 1
+        //           opcode    = QUERY
+        //           aa        = 1
+        //           tc        = 0
+        //           rd        = 0
+        //           ra        = 0
+        //           z         = 0
+        //           ad        = 0
+        //           cd        = 0
+        //           rcode     = ok
+        //           qdcount   = 0
+        //           ancount   = 7
+        //           nscount   = 0
+        //           arcount   = 0
+        //           \qd        \
+        //           \an        \
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b'_googlecast._tcp.local.'
+        //            |  type      = PTR
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = b'gambit-3cb56c6253638b3641e3d289013cc0ae._googlecast._tcp.local.'
+        //            |###[ DNS SRV Resource Record ]###
+        //            |  rrname    = b'\xc0.'
+        //            |  type      = SRV
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  priority  = 12320
+        //            |  weight    = 12320
+        //            |  port      = 14384
+        //            |  target    = b'9 3cb56c62-5363-8b36-41e3-d289013cc0ae.local..'
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b'\xc0.'
+        //            |  type      = TXT
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = [b' "id=3cb56c6253638b3641e3d289013cc0ae cd=8ECC37F6755390D005DFC02F8EC0D4FA rm=4ABD579644ACFCCF ve=05 md=gambit ic=/setup/icon.png fn=gambit a=264709 st=0 bs=FA8FFD2242A7 nf=1 rs= ']
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b'Android_f47ac10b58cc4b88bc3f5e7a81e59872.local.'
+        //            |  type      = A
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = 100.89.85.228
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b' (Android_f47ac10b58cc4b88bc3f5e7a81e59872\xc0\x1d\x00\x01\x00\x01\x00\x00\x00x\x00\x04dYU\xe4\xc1W\x00.\x00\x01\x00\x00\x00x\x00\x10\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xc1W\x00\x1c.'
+        //            |  type      = AAAA
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = fe80::3
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b' (Android_f47ac10b58cc4b88bc3f5e7a81e59872\xc0\x1d\x00\x01\x00\x01\x00\x00\x00x\x00\x04dYU\xe4\xc1W\x00.\x00\x01\x00\x00\x00x\x00\x10\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xc1W\x00\x1c.'
+        //            |  type      = AAAA
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = 200a::3
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b' (Android_f47ac10b58cc4b88bc3f5e7a81e59872\xc0\x1d\x00\x01\x00\x01\x00\x00\x00x\x00\x04dYU\xe4\xc1W\x00.\x00\x01\x00\x00\x00x\x00\x10\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\xc1W\x00\x1c.'
+        //            |  type      = AAAA
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = 200b::3
+        //           \ns        \
+        //           \ar        \
+        val expectedIPv6CastMdnsReply = """
+            3333000000FB02030405060786DD6000000001EE11FFFE80000000000000000
+            0000000000003FF0200000000000000000000000000FB14E914E901EE1B8800
+            00840000000007000000000B5F676F6F676C6563617374045F746370056C6F6
+            3616C00000C000100000078002A2767616D6269742D33636235366336323533
+            36333862333634316533643238393031336363306165C00C01C000002100010
+            0000078003430203020383030392033636235366336322D353336332D386233
+            362D343165332D6432383930313363633061652E6C6F63616C2E01C00000100
+            0010000007800B3B2202269643D336362353663363235333633386233363431
+            65336432383930313363633061652063643D384543433337463637353533393
+            044303035444643303246384543304434464120726D3D344142443537393634
+            344143464343462076653D3035206D643D67616D6269742069633D2F7365747
+            5702F69636F6E2E706E6720666E3D67616D62697420613D3236343730392073
+            743D302062733D464138464644323234324137206E663D312072733D2028416
+            E64726F69645F66343761633130623538636334623838626333663565376138
+            31653539383732C01D00010001000000780004645955E4C157001C000100000
+            0780010FE800000000000000000000000000003C157001C0001000000780010
+            200A0000000000000000000000000003C157001C0001000000780010200B000
+            0000000000000000000000003
+        """.replace("\\s+".toRegex(), "").trim()
+
+        assertContentEquals(
+            HexDump.hexStringToByteArray(expectedIPv6CastMdnsReply),
+            transmitPkt
+        )
+
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="33:33:00:00:00:FB")
+        // ip = IPv6(src="fe80::1", dst="ff02::fb")
+        // udp = UDP(dport=5353, sport=5353)
+        // questions = [
+        //   DNSQR(qname="_airplay._tcp.local", qtype="PTR"),
+        //   DNSQR(qname="gambit-3cb56c6253638b3641e3d289013cc0ae._googlecast._tcp.local", qtype="TXT")
+        // ]
+        // dns = dns_compress(DNS(qd=questions))
+        // pkt = eth/ip/udp/dns
+        val castIPv6MdnsTxtQuery = """
+            3333000000fb01020304050686dd6000000000671140fe80000000000000000
+            0000000000001ff0200000000000000000000000000fb14e914e90067439100
+            0001000002000000000000085f616972706c6179045f746370056c6f63616c0
+            0000c00012767616d6269742d33636235366336323533363338623336343165
+            336432383930313363633061650b5f676f6f676c6563617374c01500100001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(castIPv6MdnsTxtQuery),
+            DROPPED_MDNS_REPLIED
+        )
+
+        transmitPkt = apfTestHelpers.consumeTransmittedPackets(1)[0]
+
+        assertContentEquals(
+            HexDump.hexStringToByteArray(expectedIPv6CastMdnsReply),
+            transmitPkt
+        )
+
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="33:33:00:00:00:FB")
+        // ip = IPv6(src="fe80::1", dst="ff02::fb")
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qd=DNSQR(qname="_androidtvremote2._tcp.local", qtype="PTR"))
+        // pkt = eth/ip/udp/dns
+        val tvRemoteIPv6MdnsPtrQuery = """
+            3333000000fb01020304050686dd6000000000361140fe80000000000000000
+            0000000000001ff0200000000000000000000000000fb14e914e9003655e500
+            0001000001000000000000115f616e64726f6964747672656d6f746532045f7
+            46370056c6f63616c00000c0001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(tvRemoteIPv6MdnsPtrQuery),
+            DROPPED_MDNS_REPLIED
+        )
+
+        transmitPkt = apfTestHelpers.consumeTransmittedPackets(1)[0]
+
+        // ###[ Ethernet ]###
+        // dst       = 33:33:00:00:00:fb
+        // src       = 02:03:04:05:06:07
+        // type      = IPv6
+        // ###[ IPv6 ]###
+        // version   = 6
+        // tc        = 0
+        // fl        = 0
+        // plen      = 312
+        // nh        = UDP
+        // hlim      = 255
+        // src       = fe80::3
+        // dst       = ff02::fb
+        // ###[ UDP ]###
+        //         sport     = mdns
+        //         dport     = mdns
+        //         len       = 312
+        //         chksum    = 0xf867
+        // ###[ DNS ]###
+        //            id        = 0
+        //           qr        = 1
+        //           opcode    = QUERY
+        //           aa        = 1
+        //           tc        = 0
+        //           rd        = 0
+        //           ra        = 0
+        //           z         = 0
+        //           ad        = 0
+        //           cd        = 0
+        //           rcode     = ok
+        //           qdcount   = 0
+        //           ancount   = 7
+        //           nscount   = 0
+        //           arcount   = 0
+        //           \qd        \
+        //           \an        \
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b'_androidtvremote2._tcp.local.'
+        //            |  type      = PTR
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = b'gambit._androidtvremote2._tcp.local.'
+        //            |###[ DNS SRV Resource Record ]###
+        //            |  rrname    = b'gambit._androidtvremote2._tcp.local.'
+        //            |  type      = SRV
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  priority  = 12320
+        //            |  weight    = 12320
+        //            |  port      = 13876
+        //            |  target    = b'6 Android_2570595cc11d4af4a4b7146b946eeb9e.local.'
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b'gambit._androidtvremote2._tcp.local.'
+        //            |  type      = TXT
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = [b'"bt=3C:4E:56:76:1E:E9"']
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b'Android_f47ac10b58cc4b88bc3f5e7a81e59872.local.'
+        //            |  type      = A
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = 100.89.85.228
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b'Android_f47ac10b58cc4b88bc3f5e7a81e59872.local.'
+        //            |  type      = AAAA
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = fe80::3
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b'Android_f47ac10b58cc4b88bc3f5e7a81e59872.local.'
+        //            |  type      = AAAA
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = 200a::3
+        //            |###[ DNS Resource Record ]###
+        //            |  rrname    = b'Android_f47ac10b58cc4b88bc3f5e7a81e59872.local.'
+        //            |  type      = AAAA
+        //            |  cacheflush= 0
+        //            |  rclass    = IN
+        //            |  ttl       = 120
+        //            |  rdlen     = None
+        //            |  rdata     = 200b::3
+        //           \ns        \
+        //           \ar        \
+        val expectedIPv6tvRemoteMdnsReply = """
+            3333000000FB02030405060786DD60000000013811FFFE80000000000000000
+            0000000000003FF0200000000000000000000000000FB14E914E90138E4E200
+            0084000000000700000000115F616E64726F6964747672656D6F746532045F7
+            46370056C6F63616C00000C00010000007800090667616D626974C00CC03400
+            210001000000780037302030203634363620416E64726F69645F32353730353
+            935636331316434616634613462373134366239343665656239652E6C6F6361
+            6CC03400100001000000780017162262743D33433A34453A35363A37363A314
+            53A45392228416E64726F69645F663437616331306235386363346238386263
+            3366356537613831653539383732C02300010001000000780004645955E4C0A
+            3001C0001000000780010FE800000000000000000000000000003C0A3001C00
+            01000000780010200A0000000000000000000000000003C0A3001C000100000
+            0780010200B0000000000000000000000000003
+        """.replace("\\s+".toRegex(), "").trim()
+
+        assertContentEquals(
+            HexDump.hexStringToByteArray(expectedIPv6tvRemoteMdnsReply),
+            transmitPkt
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIPv6MdnsQueryDropped() {
+        val (apfFilter, program) = getApfWithMdnsOffloadEnabled(
+            removedOffloadInfos = listOf(tvRemoteOffloadInfo)
+        )
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="33:33:00:00:00:FB")
+        // ip = IPv6(src="fe80::1", dst="ff02::fb")
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qd=DNSQR(qname="_airplay._tcp.local", qtype="PTR"))
+        // pkt = eth/ip/udp/dns
+        val airplayIPv6MdnsPtrQuery = """
+            3333000000fb01020304050686dd60000000002d1140fe80000000000000000
+            0000000000001ff0200000000000000000000000000fb14e914e9002d6e8200
+            0001000001000000000000085f616972706c6179045f746370056c6f63616c0
+            0000c0001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(airplayIPv6MdnsPtrQuery),
+            DROPPED_MDNS
+        )
+
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="33:33:00:00:00:FB")
+        // ip = IPv6(src="fe80::1", dst="ff02::fb")
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qd=DNSQR(qname="_androidtvremote2._tcp.local", qtype="PTR"))
+        // pkt = eth/ip/udp/dns
+        val tvRemoteIPv6MdnsPtrQuery = """
+            3333000000fb01020304050686dd6000000000361140fe80000000000000000
+            0000000000001ff0200000000000000000000000000fb14e914e9003655e500
+            0001000001000000000000115f616e64726f6964747672656d6f746532045f7
+            46370056c6f63616c00000c0001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(tvRemoteIPv6MdnsPtrQuery),
+            DROPPED_MDNS
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testIPv6MdnsReplyPassed() {
+        val (apfFilter, program) = getApfWithMdnsOffloadEnabled(mcFilter = false)
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="33:33:00:00:00:FB")
+        // ip = IPv6(src="fe80::1", dst="ff02::fb")
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qr=1, aa=1, rd=0, qd=None, an=DNSRR(rrname="_androidtvremote2._tcp.local", type="PTR", rdata="gambit._androidtvremote2._tcp.local", ttl=120))
+        // pkt = eth/ip/udp/dns
+        val tvRemoteIPv6MdnsPtrAnswer = """
+            3333000000fb01020304050686dd6000000000611140fe80000000000000000
+            0000000000001ff0200000000000000000000000000fb14e914e90061563300
+            0084000000000100000000115f616e64726f6964747672656d6f746532045f7
+            46370056c6f63616c00000c00010000007800250667616d626974115f616e64
+            726f6964747672656d6f746532045f746370056c6f63616c00
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(tvRemoteIPv6MdnsPtrAnswer),
+            PASSED_MDNS
+        )
+    }
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testRaFilterWorksWhenMdnsOffloadEnabled() {
+        var (apfFilter, program) = getApfWithMdnsOffloadEnabled()
+        // ###[ Ethernet ]###
+        //  dst       = 33:33:00:00:00:01
+        //  src       = f4:34:f0:64:52:fe
+        //  type      = IPv6
+        // ###[ IPv6 ]###
+        //      version   = 6
+        //      tc        = 0
+        //      fl        = 68608
+        //      plen      = 80
+        //      nh        = ICMPv6
+        //      hlim      = 255
+        //      src       = fe80::1cb6:b5bc:353b:7cfd
+        //      dst       = ff02::1
+        // ###[ ICMPv6 Neighbor Discovery - Router Advertisement ]###
+        //         type      = Router Advertisement
+        //         code      = 0
+        //         cksum     = 0xfab
+        //         chlim     = 0
+        //         M         = 0
+        //         O         = 0
+        //         H         = 0
+        //         prf       = Medium (default)
+        //         P         = 0
+        //         res       = 0
+        //         routerlifetime= 0
+        //         reachabletime= 0
+        //         retranstimer= 0
+        // ###[ ICMPv6 Neighbor Discovery Option - Prefix Information ]###
+        //            type      = 3
+        //            len       = 4
+        //            prefixlen = 64
+        //            L         = 1
+        //            A         = 1
+        //            R         = 0
+        //            res1      = 0
+        //            validlifetime= 0x708
+        //            preferredlifetime= 0x708
+        //            res2      = 0x0
+        //            prefix    = fdee:d0c4:7546:5344::
+        // ###[ ICMPv6 Neighbor Discovery Option - Route Information Option ]###
+        //               type      = 24
+        //               len       = 2
+        //               plen      = 64
+        //               res1      = 0
+        //               prf       = Medium (default)
+        //               res2      = 0
+        //               rtlifetime= 1800
+        //               prefix    = fd0c:8be6:43ee::
+        // ###[ ICMPv6 Neighbor Discovery Option - Expanded Flags Option ]###
+        //                  type      = 26
+        //                  len       = 1
+        //                  res       = 140737488355328
+        // ###[ ICMPv6 Neighbor Discovery Option - Source Link-Layer Address ]###
+        //                     type      = 1
+        //                     len       = 1
+        //                     lladdr    = f4:34:f0:64:52:fe
+        val ra = """
+            333300000001f434f06452fe86dd60010c0000503afffe800000000000001cb6b5bc353b7cfdff0
+            2000000000000000000000000000186000fab000000000000000000000000030440c00000070800
+            00070800000000fdeed0c47546534400000000000000001802400000000708fd0c8be643ee00001
+            a018000000000000101f434f06452fe
+        """.replace("\\s+".toRegex(), "").trim()
+        val raBytes = HexDump.hexStringToByteArray(ra)
+        Os.write(raWriterSocket, raBytes, 0, raBytes.size)
+
+        program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            raBytes,
+            DROPPED_RA
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testMdnsOffloadFailOpenForTooManySubtype() {
+        val (apfFilter, program) = getApfWithMdnsOffloadEnabled()
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="01:00:5e:00:00:fb")
+        // ip = IP(src="10.0.0.3", dst="224.0.0.251")
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qd=DNSQR(qname="_testsubtype._tcp.local", qtype="PTR"))
+        // pkt = eth/ip/udp/dns
+        val typePtrQuery = """
+            01005e0000fb0102030405060800450000450001000040118fa90a000003e00
+            000fb14e914e900319b020000010000010000000000000c5f74657374737562
+            74797065045f746370056c6f63616c00000c0001
+        """.replace("\\s+".toRegex(), "").trim()
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(typePtrQuery),
+            PASSED_MDNS
+        )
+
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="01:00:5e:00:00:fb")
+        // ip = IP(src="10.0.0.3", dst="224.0.0.251")
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qd=DNSQR(qname="sub1._sub._testsubtype._tcp.local", qtype="PTR"))
+        // pkt = eth/ip/udp/dns
+        val subTypePtrQuery = """
+            01005e0000fb01020304050608004500004f0001000040118f9f0a000003e00
+            000fb14e914e9003b1b3f0000010000010000000000000473756231045f7375
+            620c5f7465737473756274797065045f746370056c6f63616c00000c0001
+        """.replace("\\s+".toRegex(), "").trim()
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(subTypePtrQuery),
+            PASSED_MDNS
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testMdnsOffloadRulePrioritizedOverRaFilter() {
+        val (apfFilterForEstimation, _) = getApfWithMdnsOffloadEnabled(
+            apfRam = 4096,
+            addedOffloadInfos = listOf(
+                castOffloadInfo,
+                tvRemoteOffloadInfo,
+                manySubtypeOffloadInfo
+            ),
+            raReaderSocket = FileDescriptor(),
+        )
+
+        val apfRam = apfFilterForEstimation.overEstimatedProgramSize + counterTotalSize
+
+        val (apfFilter, _) = getApfWithMdnsOffloadEnabled(
+            apfRam = apfRam,
+            addedOffloadInfos = listOf(
+                castOffloadInfo,
+                tvRemoteOffloadInfo,
+                manySubtypeOffloadInfo
+            ),
+        )
+        // ###[ Ethernet ]###
+        //  dst       = 33:33:00:00:00:01
+        //  src       = f4:34:f0:64:52:fe
+        //  type      = IPv6
+        // ###[ IPv6 ]###
+        //      version   = 6
+        //      tc        = 0
+        //      fl        = 68608
+        //      plen      = 80
+        //      nh        = ICMPv6
+        //      hlim      = 255
+        //      src       = fe80::1cb6:b5bc:353b:7cfd
+        //      dst       = ff02::1
+        // ###[ ICMPv6 Neighbor Discovery - Router Advertisement ]###
+        //         type      = Router Advertisement
+        //         code      = 0
+        //         cksum     = 0xfab
+        //         chlim     = 0
+        //         M         = 0
+        //         O         = 0
+        //         H         = 0
+        //         prf       = Medium (default)
+        //         P         = 0
+        //         res       = 0
+        //         routerlifetime= 0
+        //         reachabletime= 0
+        //         retranstimer= 0
+        // ###[ ICMPv6 Neighbor Discovery Option - Prefix Information ]###
+        //            type      = 3
+        //            len       = 4
+        //            prefixlen = 64
+        //            L         = 1
+        //            A         = 1
+        //            R         = 0
+        //            res1      = 0
+        //            validlifetime= 0x708
+        //            preferredlifetime= 0x708
+        //            res2      = 0x0
+        //            prefix    = fdee:d0c4:7546:5344::
+        // ###[ ICMPv6 Neighbor Discovery Option - Route Information Option ]###
+        //               type      = 24
+        //               len       = 2
+        //               plen      = 64
+        //               res1      = 0
+        //               prf       = Medium (default)
+        //               res2      = 0
+        //               rtlifetime= 1800
+        //               prefix    = fd0c:8be6:43ee::
+        // ###[ ICMPv6 Neighbor Discovery Option - Expanded Flags Option ]###
+        //                  type      = 26
+        //                  len       = 1
+        //                  res       = 140737488355328
+        // ###[ ICMPv6 Neighbor Discovery Option - Source Link-Layer Address ]###
+        //                     type      = 1
+        //                     len       = 1
+        //                     lladdr    = f4:34:f0:64:52:fe
+        val ra = """
+            333300000001f434f06452fe86dd60010c0000503afffe800000000000001cb6b5bc353b7cfdff0
+            2000000000000000000000000000186000fab000000000000000000000000030440c00000070800
+            00070800000000fdeed0c47546534400000000000000001802400000000708fd0c8be643ee00001
+            a018000000000000101f434f06452fe
+        """.replace("\\s+".toRegex(), "").trim()
+        val raBytes = HexDump.hexStringToByteArray(ra)
+        Os.write(raWriterSocket, raBytes, 0, raBytes.size)
+
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+        assertThat(program.size).isLessThan(apfRam + 1)
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            raBytes,
+            PASSED_IPV6_ICMP
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testMdnsOffloadRulePrioritizationAllRulesOffloaded() {
+        val (apfFilter, program) = getApfWithMdnsOffloadEnabled(
+            apfRam = 4096,
+            addedOffloadInfos = listOf(
+                castOffloadInfo,
+                tvRemoteOffloadInfo,
+                manySubtypeOffloadInfo
+            ),
+        )
+        assertThat(program.size).isLessThan(4097)
+
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="01:00:5e:00:00:fb")
+        // ip = IP(src="10.0.0.3", dst="224.0.0.251")
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qd=DNSQR(qname="_googlecast._tcp.local", qtype="PTR"))
+        // pkt = eth/ip/udp/dns
+        val castIPv4MdnsPtrQueryForOffload = """
+            01005e0000fb0102030405060800450000440001000040118faa0a000003e00
+            000fb14e914e900309fa50000010000010000000000000b5f676f6f676c6563
+            617374045f746370056c6f63616c00000c0001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(castIPv4MdnsPtrQueryForOffload),
+            DROPPED_MDNS_REPLIED
+        )
+
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="01:00:5e:00:00:fb")
+        // ip = IP(src="10.0.0.3", dst="224.0.0.251")
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qd=DNSQR(qname="_androidtvremote2._tcp.local", qtype="PTR"))
+        // pkt = eth/ip/udp/dns
+        val tvRemoteIPv4MdnsPtrQueryForOffload = """
+            01005e0000fb01020304050608004500004a0001000040118fa40a000003e00
+            000fb14e914e900366966000001000001000000000000115f616e64726f6964
+            747672656d6f746532045f746370056c6f63616c00000c0001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(tvRemoteIPv4MdnsPtrQueryForOffload),
+            DROPPED_MDNS_REPLIED
+        )
+
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="01:00:5e:00:00:fb")
+        // ip = IP(src="10.0.0.3", dst="224.0.0.251")
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qd=DNSQR(qname="sub1._sub._testsubtype._tcp.local", qtype="PTR"))
+        // pkt = eth/ip/udp/dns
+        val subTypePtrQueryForPassthrough = """
+            01005e0000fb01020304050608004500004f0001000040118f9f0a000003e00
+            000fb14e914e9003b1b3f0000010000010000000000000473756231045f7375
+            620c5f7465737473756274797065045f746370056c6f63616c00000c0001
+        """.replace("\\s+".toRegex(), "").trim()
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(subTypePtrQueryForPassthrough),
+            PASSED_MDNS
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testMdnsOffloadRulePrioritizationSomeRulesFailOpened() {
+        val (apfFilterForEstimation, _) = getApfWithMdnsOffloadEnabled(
+            apfRam = 4096,
+            addedOffloadInfos = listOf(
+                castOffloadInfo,
+                tvRemoteOffloadInfo,
+                manySubtypeOffloadInfo
+            ),
+        )
+
+        val apfRam = apfFilterForEstimation.overEstimatedProgramSize + counterTotalSize - 1
+
+        val (apfFilter, program) = getApfWithMdnsOffloadEnabled(
+            apfRam = apfRam,
+            addedOffloadInfos = listOf(
+                castOffloadInfo,
+                tvRemoteOffloadInfo,
+                manySubtypeOffloadInfo
+            ),
+        )
+        assertThat(program.size).isLessThan(apfRam + 1)
+
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="01:00:5e:00:00:fb")
+        // ip = IP(src="10.0.0.3", dst="224.0.0.251")
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qd=DNSQR(qname="_googlecast._tcp.local", qtype="PTR"))
+        // pkt = eth/ip/udp/dns
+        val castIPv4MdnsPtrQueryForOffload = """
+            01005e0000fb0102030405060800450000440001000040118faa0a000003e00
+            000fb14e914e900309fa50000010000010000000000000b5f676f6f676c6563
+            617374045f746370056c6f63616c00000c0001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(castIPv4MdnsPtrQueryForOffload),
+            DROPPED_MDNS_REPLIED
+        )
+
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="01:00:5e:00:00:fb")
+        // ip = IP(src="10.0.0.3", dst="224.0.0.251")
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qd=DNSQR(qname="_androidtvremote2._tcp.local", qtype="PTR"))
+        // pkt = eth/ip/udp/dns
+        val tvRemoteIPv4MdnsPtrQueryForPassthrough = """
+            01005e0000fb01020304050608004500004a0001000040118fa40a000003e00
+            000fb14e914e900366966000001000001000000000000115f616e64726f6964
+            747672656d6f746532045f746370056c6f63616c00000c0001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(tvRemoteIPv4MdnsPtrQueryForPassthrough),
+            PASSED_MDNS
+        )
+
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="01:00:5e:00:00:fb")
+        // ip = IP(src="10.0.0.3", dst="224.0.0.251")
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qd=DNSQR(qname="sub1._sub._testsubtype._tcp.local", qtype="PTR"))
+        // pkt = eth/ip/udp/dns
+        val subTypePtrQueryForPassthrough = """
+            01005e0000fb01020304050608004500004f0001000040118f9f0a000003e00
+            000fb14e914e9003b1b3f0000010000010000000000000473756231045f7375
+            620c5f7465737473756274797065045f746370056c6f63616c00000c0001
+        """.replace("\\s+".toRegex(), "").trim()
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(subTypePtrQueryForPassthrough),
+            PASSED_MDNS
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testMdnsOffloadRulePrioritizationAllRulesFailOpened() {
+        val (apfFilterForEstimation, _) = getApfWithMdnsOffloadEnabled(
+            apfRam = 4096,
+            addedOffloadInfos = listOf(passthroughCastOffloadInfo),
+        )
+
+        val apfRam = apfFilterForEstimation.overEstimatedProgramSize + counterTotalSize
+        val (apfFilter, program) = getApfWithMdnsOffloadEnabled(
+            apfRam = apfRam,
+            addedOffloadInfos = listOf(
+                castOffloadInfo,
+                tvRemoteOffloadInfo,
+                manySubtypeOffloadInfo
+            ),
+        )
+        assertThat(program.size).isLessThan(apfRam + 1)
+
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="01:00:5e:00:00:fb")
+        // ip = IP(src="10.0.0.3", dst="224.0.0.251")
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qd=DNSQR(qname="_googlecast._tcp.local", qtype="PTR"))
+        // pkt = eth/ip/udp/dns
+        val castIPv4MdnsPtrQueryForPassthrough = """
+            01005e0000fb0102030405060800450000440001000040118faa0a000003e00
+            000fb14e914e900309fa50000010000010000000000000b5f676f6f676c6563
+            617374045f746370056c6f63616c00000c0001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(castIPv4MdnsPtrQueryForPassthrough),
+            PASSED_MDNS
+        )
+
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="01:00:5e:00:00:fb")
+        // ip = IP(src="10.0.0.3", dst="224.0.0.251")
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qd=DNSQR(qname="_androidtvremote2._tcp.local", qtype="PTR"))
+        // pkt = eth/ip/udp/dns
+        val tvRemoteIPv4MdnsPtrQueryForPassthrough = """
+            01005e0000fb01020304050608004500004a0001000040118fa40a000003e00
+            000fb14e914e900366966000001000001000000000000115f616e64726f6964
+            747672656d6f746532045f746370056c6f63616c00000c0001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(tvRemoteIPv4MdnsPtrQueryForPassthrough),
+            PASSED_MDNS
+        )
+
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="01:00:5e:00:00:fb")
+        // ip = IP(src="10.0.0.3", dst="224.0.0.251")
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qd=DNSQR(qname="sub1._sub._testsubtype._tcp.local", qtype="PTR"))
+        // pkt = eth/ip/udp/dns
+        val subTypePtrQueryForPassthrough = """
+            01005e0000fb01020304050608004500004f0001000040118f9f0a000003e00
+            000fb14e914e9003b1b3f0000010000010000000000000473756231045f7375
+            620c5f7465737473756274797065045f746370056c6f63616c00000c0001
+        """.replace("\\s+".toRegex(), "").trim()
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(subTypePtrQueryForPassthrough),
+            PASSED_MDNS
+        )
+
+        // Using scapy to generate packet:
+        // eth = Ether(src="01:02:03:04:05:06", dst="01:00:5e:00:00:fb")
+        // ip = IP(src="10.0.0.3", dst="224.0.0.251")
+        // udp = UDP(dport=5353, sport=5353)
+        // dns = DNS(qd=DNSQR(qname="_airplay._tcp.local", qtype="PTR"))
+        // pkt = eth/ip/udp/dns
+        val airplayIPv4MdnsPtrQueryForPassthrough = """
+            01005e0000fb0102030405060800450000410001000040118fad0a000003e00
+            000fb14e914e9002d8203000001000001000000000000085f616972706c6179
+            045f746370056c6f63616c00000c0001
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(airplayIPv4MdnsPtrQueryForPassthrough),
+            PASSED_MDNS
+        )
     }
 
     @Test
     fun testApfProgramUpdate() {
         val apfFilter = getApfFilter()
-        consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
         // 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)
-        consumeInstalledProgram(ipClientCallback, installCnt = 1)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
 
         // add the same IPv4 address, expect to have no apf program update
         apfFilter.setLinkProperties(lp)
-        verify(ipClientCallback, never()).installPacketFilter(any())
+        verify(apfController, never()).installPacketFilter(any(), any())
 
         // add IPv6 addresses, expect to have apf program update
         for (addr in hostIpv6Addresses) {
@@ -2100,11 +5343,11 @@
         }
 
         apfFilter.setLinkProperties(lp)
-        consumeInstalledProgram(ipClientCallback, installCnt = 1)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
 
         // add the same IPv6 addresses, expect to have no apf program update
         apfFilter.setLinkProperties(lp)
-        verify(ipClientCallback, never()).installPacketFilter(any())
+        verify(apfController, never()).installPacketFilter(any(), any())
 
         // add more tentative IPv6 addresses, expect to have apf program update
         for (addr in hostIpv6TentativeAddresses) {
@@ -2119,18 +5362,86 @@
         }
 
         apfFilter.setLinkProperties(lp)
-        consumeInstalledProgram(ipClientCallback, installCnt = 1)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
 
         // add the same IPv6 addresses, expect to have no apf program update
         apfFilter.setLinkProperties(lp)
-        verify(ipClientCallback, never()).installPacketFilter(any())
+        verify(apfController, never()).installPacketFilter(any(), any())
+    }
+
+    // The APFv6 code path is only turned on in V+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testApfProgramUpdateWithMulticastAddressChange() {
+        val mcastAddrs = mutableListOf(
+            InetAddress.getByName("224.0.0.1") as Inet4Address
+        )
+        doReturn(mcastAddrs).`when`(dependencies).getIPv4MulticastAddresses(any())
+        val apfConfig = getDefaultConfig()
+        apfConfig.handleIgmpOffload = true
+        val apfFilter = getApfFilter(apfConfig)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
+        val addr = InetAddress.getByName("239.0.0.1") as Inet4Address
+        mcastAddrs.add(addr)
+        doReturn(mcastAddrs).`when`(dependencies).getIPv4MulticastAddresses(any())
+        val testPacket = HexDump.hexStringToByteArray("000000")
+        Os.write(mcastWriteSocket, testPacket, 0, testPacket.size)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+
+        Os.write(mcastWriteSocket, testPacket, 0, testPacket.size)
+        Thread.sleep(NO_CALLBACK_TIMEOUT_MS)
+        verify(apfController, never()).installPacketFilter(any(), any())
+
+        mcastAddrs.remove(addr)
+        doReturn(mcastAddrs).`when`(dependencies).getIPv4MulticastAddresses(any())
+        Os.write(mcastWriteSocket, testPacket, 0, testPacket.size)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testApfProgramUpdateWithIPv6MulticastAddressChange() {
+        val mcastAddrs = mutableListOf(
+            IPV6_ADDR_ALL_NODES_MULTICAST,
+            IPV6_ADDR_NODE_LOCAL_ALL_NODES_MULTICAST
+        )
+        doReturn(mcastAddrs).`when`(dependencies).getIPv6MulticastAddresses(any())
+        val apfConfig = getDefaultConfig()
+        apfConfig.handleMldOffload = true
+        val apfFilter = getApfFilter(apfConfig)
+        val ipv6LinkAddress = LinkAddress(hostLinkLocalIpv6Address, 64)
+        val lp = LinkProperties()
+        lp.addLinkAddress(ipv6LinkAddress)
+        apfFilter.setLinkProperties(lp)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 3)
+        val addr = InetAddress.getByName("ff0e::1") as Inet6Address
+        mcastAddrs.add(addr)
+        updateIPv6MulticastAddrs(apfFilter, mcastAddrs)
+        val testPacket = HexDump.hexStringToByteArray("000000")
+        Os.write(mcastWriteSocket, testPacket, 0, testPacket.size)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+
+        var solicitedNodeMcastAddr = InetAddress.getByName("ff02::1:ff12:3456") as Inet6Address
+        mcastAddrs.add(solicitedNodeMcastAddr)
+        Os.write(mcastWriteSocket, testPacket, 0, testPacket.size)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+
+        Os.write(mcastWriteSocket, testPacket, 0, testPacket.size)
+        Thread.sleep(NO_CALLBACK_TIMEOUT_MS)
+        verify(apfController, never()).installPacketFilter(any(), any())
+
+        mcastAddrs.remove(addr)
+        updateIPv6MulticastAddrs(apfFilter, mcastAddrs)
+        Os.write(mcastWriteSocket, testPacket, 0, testPacket.size)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
     }
 
     @Test
     fun testApfFilterInitializationCleanUpTheApfMemoryRegion() {
         val apfFilter = getApfFilter()
         val programCaptor = ArgumentCaptor.forClass(ByteArray::class.java)
-        verify(ipClientCallback, times(2)).installPacketFilter(programCaptor.capture())
+        verify(apfController, times(2))
+            .installPacketFilter(programCaptor.capture(), any())
         val program = programCaptor.allValues.first()
         assertContentEquals(ByteArray(4096) { 0 }, program)
     }
@@ -2138,9 +5449,524 @@
     @Test
     fun testApfFilterResumeWillCleanUpTheApfMemoryRegion() {
         val apfFilter = getApfFilter()
-        consumeInstalledProgram(ipClientCallback, installCnt = 2)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
         apfFilter.resume()
-        val program = consumeInstalledProgram(ipClientCallback, installCnt = 1)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
         assertContentEquals(ByteArray(4096) { 0 }, program)
     }
+
+    @Test
+    fun testApfIPv4MulticastAddrsUpdate() {
+        // mock IPv4 multicast address from /proc/net/igmp
+        val mcastAddrs = mutableListOf(
+            InetAddress.getByName("224.0.0.1") as Inet4Address,
+            InetAddress.getByName("239.0.0.1") as Inet4Address
+        )
+        val mcastAddrsExcludeAllHost = mutableListOf(
+            InetAddress.getByName("239.0.0.1") as Inet4Address
+        )
+        doReturn(mcastAddrs).`when`(dependencies).getIPv4MulticastAddresses(any())
+        val apfFilter = getApfFilter()
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
+        assertEquals(mcastAddrs.toSet(), apfFilter.mIPv4MulticastAddresses)
+        assertEquals(mcastAddrsExcludeAllHost.toSet(), apfFilter.mIPv4McastAddrsExcludeAllHost)
+
+        val addr = InetAddress.getByName("239.0.0.2") as Inet4Address
+        mcastAddrs.add(addr)
+        mcastAddrsExcludeAllHost.add(addr)
+        updateIPv4MulticastAddrs(apfFilter, mcastAddrs)
+        apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+        assertEquals(mcastAddrs.toSet(), apfFilter.mIPv4MulticastAddresses)
+        assertEquals(mcastAddrsExcludeAllHost.toSet(), apfFilter.mIPv4McastAddrsExcludeAllHost)
+
+        updateIPv4MulticastAddrs(apfFilter, mcastAddrs)
+        verify(apfController, never()).installPacketFilter(any(), any())
+    }
+
+    @Test
+    fun testApfFailOpenOnLimitedRAM() {
+        val apfConfig = getDefaultConfig()
+        apfConfig.apfRamSize = 512
+        val apfFilter = getApfFilter(apfConfig)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
+        assertContentEquals(
+            ByteArray(apfConfig.apfRamSize - ApfCounterTracker.Counter.totalSize()) { 0 },
+            program
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testCreateEgressReportReaderSocket() {
+        var apfFilter = getApfFilter()
+        verify(dependencies, never()).createEgressIgmpReportsReaderSocket(anyInt())
+        verify(dependencies, never()).createEgressMulticastReportsReaderSocket(anyInt())
+        clearInvocations(dependencies)
+
+        val apfConfig = getDefaultConfig()
+        apfConfig.handleMldOffload = true
+        apfFilter = getApfFilter(apfConfig)
+
+        verify(dependencies, never()).createEgressIgmpReportsReaderSocket(anyInt())
+        verify(dependencies, times(1)).createEgressMulticastReportsReaderSocket(anyInt())
+        clearInvocations(dependencies)
+
+        apfConfig.handleIgmpOffload = true
+        apfConfig.handleMldOffload = false
+        apfFilter = getApfFilter(apfConfig)
+
+        verify(dependencies, never()).createEgressMulticastReportsReaderSocket(anyInt())
+        verify(dependencies, times(1)).createEgressIgmpReportsReaderSocket(anyInt())
+        clearInvocations(dependencies)
+
+        apfConfig.handleIgmpOffload = true
+        apfConfig.handleMldOffload = true
+        apfFilter = getApfFilter(apfConfig)
+        verify(dependencies, never()).createEgressIgmpReportsReaderSocket(anyInt())
+        verify(dependencies, times(1)).createEgressMulticastReportsReaderSocket(anyInt())
+    }
+
+    fun getProgramWithAllFeatureEnabled(
+        apfRamSize: Int = 8192,
+        apfVersion: Int = apfInterpreterVersion
+    ): Pair<ByteArray, Long> {
+        val localNsdManager = mock(NsdManager::class.java)
+        doReturn(localNsdManager).`when`(context).getSystemService(NsdManager::class.java)
+        val localRaWriterSocket = FileDescriptor()
+        val localRaReaderSocket = FileDescriptor()
+        Os.socketpair(AF_UNIX, SOCK_STREAM, 0, localRaWriterSocket, localRaReaderSocket)
+        doReturn(localRaReaderSocket).`when`(dependencies).createPacketReaderSocket(anyInt())
+        var program = byteArrayOf(0)
+        var generationTime = 0L
+        val ipv4McastAddrs = listOf(
+            InetAddress.getByName("224.0.0.1") as Inet4Address,
+            InetAddress.getByName("224.0.0.251") as Inet4Address,
+            InetAddress.getByName("239.255.255.250") as Inet4Address
+        )
+        doReturn(ipv4McastAddrs).`when`(dependencies).getIPv4MulticastAddresses(any())
+        val ipv6McastAddrs = listOf(
+            InetAddress.getByName("ff02::1:ff11:33e1") as Inet6Address,
+            InetAddress.getByName("ff02::1:ff11:33e2") as Inet6Address,
+            InetAddress.getByName("ff02::fb") as Inet6Address,
+            InetAddress.getByName("ff02::c") as Inet6Address,
+            InetAddress.getByName("ff05::c") as Inet6Address,
+            InetAddress.getByName("ff02::1") as Inet6Address,
+            InetAddress.getByName("ff01::1") as Inet6Address,
+        )
+        // mock IPv6 multicast address from /proc/net/igmp6
+        doReturn(ipv6McastAddrs).`when`(dependencies).getIPv6MulticastAddresses(any())
+        tryTest {
+            val apfConfig = getDefaultConfig()
+            apfConfig.apfRamSize = apfRamSize
+            apfConfig.apfVersionSupported = apfVersion
+            apfConfig.multicastFilter = true
+            apfConfig.handleArpOffload = true
+            apfConfig.handleNdOffload = true
+            apfConfig.handleIgmpOffload = true
+            apfConfig.handleMldOffload = true
+            apfConfig.handleIpv4PingOffload = true
+            apfConfig.handleIpv6PingOffload = true
+            apfConfig.handleMdnsOffload = true
+            val apfFilter = getApfFilter(apfConfig)
+            apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
+
+            val srcAddr = byteArrayOf(10, 0, 0, 5)
+            val dstAddr = byteArrayOf(10, 0, 0, 6)
+            val srcPort = 1024
+            val dstPort = 4500
+            val parcel = NattKeepalivePacketDataParcelable()
+            parcel.srcAddress = InetAddress.getByAddress(srcAddr).address
+            parcel.srcPort = srcPort
+            parcel.dstAddress = InetAddress.getByAddress(dstAddr).address
+            parcel.dstPort = dstPort
+            apfFilter.addNattKeepalivePacketFilter(1, parcel)
+            apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+
+            val captor = ArgumentCaptor.forClass(OffloadEngine::class.java)
+            verify(localNsdManager).registerOffloadEngine(
+                eq(ifParams.name),
+                anyLong(),
+                anyLong(),
+                any(),
+                captor.capture()
+            )
+            val offloadEngine = captor.value
+
+            val lp = LinkProperties()
+            val ipv4LinkAddress = LinkAddress(InetAddress.getByAddress(hostIpv4Address), 24)
+            lp.addLinkAddress(ipv4LinkAddress)
+            val ipv6LinkAddress = LinkAddress(hostLinkLocalIpv6Address, 64)
+            lp.addLinkAddress(ipv6LinkAddress)
+            for (addr in hostIpv6Addresses) {
+                lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
+            }
+            apfFilter.setLinkProperties(lp)
+            apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+
+            visibleOnHandlerThread(handler) {
+                offloadEngine.onOffloadServiceUpdated(castOffloadInfo.value)
+                offloadEngine.onOffloadServiceUpdated(tvRemoteOffloadInfo.value)
+                offloadEngine.onOffloadServiceUpdated(airplayOffloadInfo.value)
+                offloadEngine.onOffloadServiceUpdated(raopOffloadInfo.value)
+            }
+
+            apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 4)
+
+            val ra1 = """
+                333300000001f434f06452fe86dd60010c0000503afffe800000000000001cb6b5bc353b7cfdff0
+                2000000000000000000000000000186000fab000000000000000000000000030440c00000070800
+                00070800000000fdeed0c47546534400000000000000001802400000000708fd0c8be643ee00001
+                a018000000000000101f434f06452fe
+            """.replace("\\s+".toRegex(), "").trim()
+            val ra1Bytes = HexDump.hexStringToByteArray(ra1)
+            Os.write(localRaWriterSocket, ra1Bytes, 0, ra1Bytes.size)
+            apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+
+            // Using scapy to generate packet:
+            // eth = Ether(src="E8:9F:80:66:60:BC", dst="f2:9c:70:2c:39:5a")
+            // ip6 = IPv6(src="fe80::2", dst="ff02::1")
+            // icmpra = ICMPv6ND_RA(routerlifetime=360, retranstimer=360)
+            // pio1 = ICMPv6NDOptPrefixInfo(prefixlen=64, prefix="2002:db8::")
+            // rio = ICMPv6NDOptRouteInfo(prefix="2002:db8:cafe::")
+            // ra = eth/ip6/icmpra/pio1/rio
+            val ra2 = """
+                f29c702c395ae89f806660bc86dd6000000000483afffe800000000000000000000000000002ff0
+                200000000000000000000000000018600f6e3000801680000000000000168030440c0ffffffffff
+                ffffff0000000020020db800000000000000000000000018030000ffffffff20020db8cafe00000
+                000000000000000
+            """.replace("\\s+".toRegex(), "").trim()
+            val ra2Bytes = HexDump.hexStringToByteArray(ra2)
+            val beforeNs = SystemClock.elapsedRealtimeNanos()
+            Os.write(localRaWriterSocket, ra2Bytes, 0, ra2Bytes.size)
+            program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+            val afterNs = SystemClock.elapsedRealtimeNanos()
+            generationTime = (afterNs - beforeNs) / 1000000
+        } cleanup {
+            IoUtils.closeQuietly(localRaWriterSocket)
+        }
+        return Pair(program, generationTime)
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testAllOffloadFeatureEnabled() {
+        val (program, generationTimeMs) = getProgramWithAllFeatureEnabled()
+        val programSize = program.size
+        val counterSize = ApfCounterTracker.Counter.totalSize()
+        val totalSize = programSize + counterSize
+        Log.i(
+            TAG,
+            "all feature on, program size: $programSize, counter size: $counterSize," +
+                " total size:$totalSize, program:"
+        )
+        val programChunk = program.toList().chunked(2000)
+        programChunk.forEach {
+            Log.i(TAG, HexDump.toHexString(it.toByteArray()))
+        }
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testAllOffloadFeatureEnabledPerformanceEstimation() {
+        val (program, generationTimeMs) = getProgramWithAllFeatureEnabled()
+        // Ignore the first iteration as it may take longer time for JVM warm up
+        if (apfInterpreterVersion == ApfJniUtils.APF_INTERPRETER_VERSION_NEXT) {
+            Log.i(
+                TAG,
+                "all offload on: program size ${program.size}, " +
+                    "generation time: $generationTimeMs ms"
+            )
+        }
+    }
+
+    fun getProgramWithAllFeatureOff(
+        apfRamSize: Int = 8192,
+        apfVersion: Int = apfInterpreterVersion
+    ): Pair<ByteArray, Long> {
+        val localRaWriterSocket = FileDescriptor()
+        val localRaReaderSocket = FileDescriptor()
+        Os.socketpair(AF_UNIX, SOCK_STREAM, 0, localRaWriterSocket, localRaReaderSocket)
+        doReturn(localRaReaderSocket).`when`(dependencies).createPacketReaderSocket(anyInt())
+        var program = byteArrayOf(0)
+        var generationTime = 0L
+        tryTest {
+            val ipv4McastAddrs = listOf(
+                InetAddress.getByName("224.0.0.1") as Inet4Address,
+                InetAddress.getByName("224.0.0.251") as Inet4Address,
+                InetAddress.getByName("239.255.255.250") as Inet4Address
+            )
+            doReturn(ipv4McastAddrs).`when`(dependencies).getIPv4MulticastAddresses(any())
+            val ipv6McastAddrs = listOf(
+                InetAddress.getByName("ff02::1:ff11:33e1") as Inet6Address,
+                InetAddress.getByName("ff02::1:ff11:33e2") as Inet6Address,
+                InetAddress.getByName("ff02::fb") as Inet6Address,
+                InetAddress.getByName("ff02::c") as Inet6Address,
+                InetAddress.getByName("ff05::c") as Inet6Address,
+                InetAddress.getByName("ff02::1") as Inet6Address,
+                InetAddress.getByName("ff01::1") as Inet6Address,
+            )
+            // mock IPv6 multicast address from /proc/net/igmp6
+            doReturn(ipv6McastAddrs).`when`(dependencies).getIPv6MulticastAddresses(any())
+            val apfConfig = getDefaultConfig()
+            apfConfig.apfRamSize = apfRamSize
+            apfConfig.apfVersionSupported = apfVersion
+            apfConfig.multicastFilter = true
+            apfConfig.handleArpOffload = false
+            apfConfig.handleNdOffload = false
+            apfConfig.handleIgmpOffload = false
+            apfConfig.handleMldOffload = false
+            apfConfig.handleIpv4PingOffload = false
+            apfConfig.handleIpv6PingOffload = false
+            apfConfig.handleMdnsOffload = false
+            val apfFilter = getApfFilter(apfConfig)
+            if (apfVersion > 2) {
+                apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
+            } else {
+                // If the APF version is less than 3, only one program will be installed because
+                // APFv2 lacks counter support, and therefore, counter region cleanup is unnecessary
+                apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+            }
+
+            val lp = LinkProperties()
+            val ipv4LinkAddress = LinkAddress(InetAddress.getByAddress(hostIpv4Address), 24)
+            lp.addLinkAddress(ipv4LinkAddress)
+            val ipv6LinkAddress = LinkAddress(hostLinkLocalIpv6Address, 64)
+            lp.addLinkAddress(ipv6LinkAddress)
+            for (addr in hostIpv6Addresses) {
+                lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
+            }
+            apfFilter.setLinkProperties(lp)
+            apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+
+            val ra1 = """
+                 333300000001f434f06452fe86dd60010c0000503afffe800000000000001cb6b5bc353b7cfdff0
+                 2000000000000000000000000000186000fab000000000000000000000000030440c00000070800
+                 00070800000000fdeed0c47546534400000000000000001802400000000708fd0c8be643ee00001
+                 a018000000000000101f434f06452fe
+             """.replace("\\s+".toRegex(), "").trim()
+            val ra1Bytes = HexDump.hexStringToByteArray(ra1)
+            Os.write(localRaWriterSocket, ra1Bytes, 0, ra1Bytes.size)
+            apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+
+            // Using scapy to generate packet:
+            // eth = Ether(src="E8:9F:80:66:60:BC", dst="f2:9c:70:2c:39:5a")
+            // ip6 = IPv6(src="fe80::2", dst="ff02::1")
+            // icmpra = ICMPv6ND_RA(routerlifetime=360, retranstimer=360)
+            // pio1 = ICMPv6NDOptPrefixInfo(prefixlen=64, prefix="2002:db8::")
+            // rio = ICMPv6NDOptRouteInfo(prefix="2002:db8:cafe::")
+            // ra = eth/ip6/icmpra/pio1/rio
+            val ra2 = """
+                f29c702c395ae89f806660bc86dd6000000000483afffe800000000000000000000000000002ff0
+                200000000000000000000000000018600f6e3000801680000000000000168030440c0ffffffffff
+                ffffff0000000020020db800000000000000000000000018030000ffffffff20020db8cafe00000
+                000000000000000
+            """.replace("\\s+".toRegex(), "").trim()
+            val ra2Bytes = HexDump.hexStringToByteArray(ra2)
+            val beforeNs = SystemClock.elapsedRealtimeNanos()
+            Os.write(localRaWriterSocket, ra2Bytes, 0, ra2Bytes.size)
+            program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+            val afterNs = SystemClock.elapsedRealtimeNanos()
+            generationTime = (afterNs - beforeNs) / 1000000
+        } cleanup {
+            IoUtils.closeQuietly(localRaWriterSocket)
+        }
+        return Pair(program, generationTime)
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testAllOffloadFeatureDisabledPerformanceEstimation() {
+        val (program, generationTimeMs) = getProgramWithAllFeatureOff()
+        // Ignore the first iteration as it may take longer time for JVM warm up
+        if (apfInterpreterVersion == ApfJniUtils.APF_INTERPRETER_VERSION_NEXT) {
+            Log.i(
+                TAG,
+                "all offload off: program size ${program.size}, " +
+                    "generation time: $generationTimeMs ms"
+            )
+        }
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testAPFv2GenerateValidProgram() {
+        assumeTrue(apfInterpreterVersion == ApfJniUtils.APF_INTERPRETER_VERSION_NEXT)
+        var apfRamSize = 600
+        val maxApfRamSize = 2048
+
+        while (apfRamSize <= maxApfRamSize) {
+            val (program, _) = getProgramWithAllFeatureOff(
+                apfRamSize = apfRamSize,
+                apfVersion = 2
+            )
+            assertThat(program.size).isLessThan(apfRamSize + 1)
+            assertThat(program).isNotEqualTo(ByteArray(apfRamSize) { 0 })
+            // TODO: reduce after fixing 'Failed to receive adb shell test output within 66000 ms'
+            val step = Random.nextInt(1, 64)
+            apfRamSize += step
+        }
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testAPFv4GenerateValidProgram() {
+        assumeTrue(apfInterpreterVersion == ApfJniUtils.APF_INTERPRETER_VERSION_NEXT)
+        var apfRamSize = 1024
+        val maxApfRamSize = 4096
+
+        while (apfRamSize <= maxApfRamSize) {
+            val (program, _) = getProgramWithAllFeatureOff(
+                apfRamSize = apfRamSize,
+                apfVersion = 4
+            )
+            val availableRam = apfRamSize - ApfCounterTracker.Counter.totalSize()
+            assertThat(program.size).isLessThan(availableRam + 1)
+            assertThat(program).isNotEqualTo(ByteArray(availableRam) { 0 })
+            // TODO: reduce after fixing 'Failed to receive adb shell test output within 66000 ms'
+            val step = Random.nextInt(1, 64)
+            apfRamSize += step
+        }
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testAPFv6GenerateValidProgram() {
+        var apfRamSize = 3000
+        val maxApfRamSize = 6000
+
+        while (apfRamSize <= maxApfRamSize) {
+            val (program, _) = getProgramWithAllFeatureEnabled(
+                apfRamSize = apfRamSize,
+                apfVersion = apfInterpreterVersion
+            )
+            val availableRam = apfRamSize - ApfCounterTracker.Counter.totalSize()
+            assertThat(program.size).isLessThan(availableRam + 1)
+            assertThat(program).isNotEqualTo(ByteArray(availableRam) { 0 })
+            // TODO: reduce after fixing 'Failed to receive adb shell test output within 66000 ms'
+            val step = Random.nextInt(1, 64)
+            apfRamSize += step
+        }
+    }
+
+    private fun getProgramForRaSizeEstimation(
+        apfRamSize: Int,
+    ): Pair<Int, ByteArray> {
+        val localRaWriterSocket = FileDescriptor()
+        val localRaReaderSocket = FileDescriptor()
+        Os.socketpair(AF_UNIX, SOCK_STREAM, 0, localRaWriterSocket, localRaReaderSocket)
+        doReturn(localRaReaderSocket).`when`(dependencies).createPacketReaderSocket(anyInt())
+        var overEstimatedProgramSize = 0
+        var program = byteArrayOf(0)
+        tryTest {
+            val apfConfig = getDefaultConfig()
+            apfConfig.apfRamSize = apfRamSize
+            val apfFilter = getApfFilter(apfConfig)
+            apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
+
+            val lp = LinkProperties()
+            val ipv4LinkAddress = LinkAddress(InetAddress.getByAddress(hostIpv4Address), 24)
+            lp.addLinkAddress(ipv4LinkAddress)
+            val ipv6LinkAddress = LinkAddress(hostLinkLocalIpv6Address, 64)
+            lp.addLinkAddress(ipv6LinkAddress)
+            for (addr in hostIpv6Addresses) {
+                lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
+            }
+            apfFilter.setLinkProperties(lp)
+            program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+
+            val ra1 = """
+                333300000001f434f06452fe86dd60010c0000503afffe800000000000001cb6b5bc353b7cfdff0
+                2000000000000000000000000000186000fab000000000000000000000000030440c00000070800
+                00070800000000fdeed0c47546534400000000000000001802400000000708fd0c8be643ee00001
+                a018000000000000101f434f06452fe
+            """.replace("\\s+".toRegex(), "").trim()
+            val ra1Bytes = HexDump.hexStringToByteArray(ra1)
+            Os.write(localRaWriterSocket, ra1Bytes, 0, ra1Bytes.size)
+            apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+
+            // Using scapy to generate packet:
+            // eth = Ether(src="E8:9F:80:66:60:BC", dst="f2:9c:70:2c:39:5a")
+            // ip6 = IPv6(src="fe80::2", dst="ff02::1")
+            // icmpra = ICMPv6ND_RA(routerlifetime=360, retranstimer=360)
+            // pio1 = ICMPv6NDOptPrefixInfo(prefixlen=64, prefix="2002:db8::")
+            // rio = ICMPv6NDOptRouteInfo(prefix="2002:db8:cafe::")
+            // ra = eth/ip6/icmpra/pio1/rio
+            val ra2 = """
+                f29c702c395ae89f806660bc86dd6000000000483afffe800000000000000000000000000002ff0
+                200000000000000000000000000018600f6e3000801680000000000000168030440c0ffffffffff
+                ffffff0000000020020db800000000000000000000000018030000ffffffff20020db8cafe00000
+                000000000000000
+            """.replace("\\s+".toRegex(), "").trim()
+            val ra2Bytes = HexDump.hexStringToByteArray(ra2)
+            Os.write(localRaWriterSocket, ra2Bytes, 0, ra2Bytes.size)
+            program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 1)
+            overEstimatedProgramSize = apfFilter.overEstimatedProgramSize
+        } cleanup {
+            IoUtils.closeQuietly(localRaWriterSocket)
+        }
+        return Pair(overEstimatedProgramSize, program)
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testRaFilterSizeEstimation() {
+        val (overEstimatedProgramSize, _) = getProgramForRaSizeEstimation(apfRamSize = 8096)
+        val apfRam = overEstimatedProgramSize - 1
+        val (_, program) = getProgramForRaSizeEstimation(apfRamSize = apfRam)
+
+        val ra1 = """
+            333300000001f434f06452fe86dd60010c0000503afffe800000000000001cb6b5bc353b7cfdff0
+            2000000000000000000000000000186000fab000000000000000000000000030440c00000070800
+            00070800000000fdeed0c47546534400000000000000001802400000000708fd0c8be643ee00001
+            a018000000000000101f434f06452fe
+        """.replace("\\s+".toRegex(), "").trim()
+
+        val ra2 = """
+            f29c702c395ae89f806660bc86dd6000000000483afffe800000000000000000000000000002ff0
+            200000000000000000000000000018600f6e3000801680000000000000168030440c0ffffffffff
+            ffffff0000000020020db800000000000000000000000018030000ffffffff20020db8cafe00000
+            000000000000000
+        """.replace("\\s+".toRegex(), "").trim()
+
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            HexDump.hexStringToByteArray(ra2),
+            DROPPED_RA
+        )
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            HexDump.hexStringToByteArray(ra1),
+            PASSED_IPV6_ICMP
+        )
+    }
+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testFilteringNonUnicastTDLSPacket() {
+        val apfConfig = getDefaultConfig()
+        apfConfig.apfRamSize = 1500
+        val apfFilter = getApfFilter(apfConfig)
+        val program = apfTestHelpers.consumeInstalledProgram(apfController, installCnt = 2)
+        // Using scapy to generate packet:
+        // pkt = Ether(dst="ff:ff:ff:ff:ff:ff", type=0x890D)/Raw(load="01")
+        val bcastTDLSPkt = "ffffffffffff000000000000890d3031"
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(bcastTDLSPkt),
+            DROPPED_NON_UNICAST_TDLS
+        )
+
+        // Using scapy to generate packet:
+        // pkt = Ether(dst="02:03:04:05:06:07", type=0x890D)/Raw(load="01")
+        val ucastTDLSPkt = "020304050607000000000000890d3031"
+        apfTestHelpers.verifyProgramRun(
+            apfFilter.mApfVersionSupported,
+            program,
+            HexDump.hexStringToByteArray(ucastTDLSPkt),
+            PASSED_NON_IP_UNICAST
+        )
+    }
 }
diff --git a/tests/unit/src/android/net/apf/ApfGeneratorTest.kt b/tests/unit/src/android/net/apf/ApfGeneratorTest.kt
index 98b2a42..1c383bc 100644
--- a/tests/unit/src/android/net/apf/ApfGeneratorTest.kt
+++ b/tests/unit/src/android/net/apf/ApfGeneratorTest.kt
@@ -19,21 +19,18 @@
 import android.net.apf.ApfCounterTracker.Counter.CORRUPT_DNS_PACKET
 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_RA
 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_MDNS
 import android.net.apf.ApfCounterTracker.Counter.PASSED_TRANSMIT_FAILURE
 import android.net.apf.ApfCounterTracker.Counter.TOTAL_PACKETS
 import android.net.apf.ApfTestHelpers.Companion.DROP
 import android.net.apf.ApfTestHelpers.Companion.MIN_PKT_SIZE
 import android.net.apf.ApfTestHelpers.Companion.PASS
-import android.net.apf.ApfTestHelpers.Companion.assertDrop
-import android.net.apf.ApfTestHelpers.Companion.assertPass
-import android.net.apf.ApfTestHelpers.Companion.assertVerdict
 import android.net.apf.ApfTestHelpers.Companion.decodeCountersIntoMap
-import android.net.apf.ApfTestHelpers.Companion.verifyProgramRun
 import android.net.apf.BaseApfGenerator.APF_VERSION_2
 import android.net.apf.BaseApfGenerator.APF_VERSION_3
-import android.net.apf.BaseApfGenerator.APF_VERSION_6
 import android.net.apf.BaseApfGenerator.DROP_LABEL
 import android.net.apf.BaseApfGenerator.IllegalInstructionException
 import android.net.apf.BaseApfGenerator.MemorySlot
@@ -52,10 +49,13 @@
 import kotlin.test.assertContentEquals
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mockito.times
+import org.junit.runners.Parameterized
 
 const val ETH_HLEN = 14
 const val IPV4_HLEN = 20
@@ -68,25 +68,52 @@
 @SmallTest
 class ApfGeneratorTest {
 
+    companion object {
+        @Parameterized.Parameters
+        @JvmStatic
+        fun data(): Iterable<Any?> {
+            return mutableListOf<Int?>(
+                ApfJniUtils.APF_INTERPRETER_VERSION_V6,
+                ApfJniUtils.APF_INTERPRETER_VERSION_NEXT
+            )
+        }
+    }
+
     @get:Rule val ignoreRule = DevSdkIgnoreRule()
 
+    // Indicates which apfInterpreter to load.
+    @Parameterized.Parameter(0)
+    @JvmField
+    var apfInterpreterVersion: Int = ApfJniUtils.APF_INTERPRETER_VERSION_NEXT
+
     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 lateinit var apfTestHelpers: ApfTestHelpers
+
+    @Before
+    fun setUp() {
+        apfTestHelpers = ApfTestHelpers(apfInterpreterVersion)
+    }
+
+    @After
+    fun tearDown() {
+        apfTestHelpers.resetTransmittedPacketMemory()
+    }
 
     @Test
     fun testDataInstructionMustComeFirst() {
-        var gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        var gen = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
         gen.addAllocateR0()
         assertFailsWith<IllegalInstructionException> { gen.addData(ByteArray(3) { 0x01 }) }
     }
 
     @Test
     fun testApfInstructionEncodingSizeCheck() {
-        var gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        var gen = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
         assertFailsWith<IllegalArgumentException> {
-            ApfV6Generator(ByteArray(65536) { 0x01 }, APF_VERSION_6, ramSize, clampSize)
+            ApfV6Generator(ByteArray(65536) { 0x01 }, apfInterpreterVersion, ramSize, clampSize)
         }
         assertFailsWith<IllegalArgumentException> { gen.addAllocate(65536) }
         assertFailsWith<IllegalArgumentException> { gen.addAllocate(-1) }
@@ -242,67 +269,67 @@
         assertFailsWith<IllegalArgumentException> {
             gen.addJumpIfBytesAtR0NotEqual(ByteArray(2048) { 1 }, DROP_LABEL)
         }
-        assertFailsWith<IllegalArgumentException> { gen.addCountAndDrop(PASSED_ARP) }
+        assertFailsWith<IllegalArgumentException> { gen.addCountAndDrop(PASSED_ARP_REQUEST) }
         assertFailsWith<IllegalArgumentException> { gen.addCountAndPass(DROPPED_ETH_BROADCAST) }
         assertFailsWith<IllegalArgumentException> {
-            gen.addCountAndDropIfR0Equals(3, PASSED_ARP)
+            gen.addCountAndDropIfR0Equals(3, PASSED_ARP_REQUEST)
         }
         assertFailsWith<IllegalArgumentException> {
             gen.addCountAndPassIfR0Equals(3, DROPPED_ETH_BROADCAST)
         }
         assertFailsWith<IllegalArgumentException> {
-            gen.addCountAndDropIfR0NotEquals(3, PASSED_ARP)
+            gen.addCountAndDropIfR0NotEquals(3, PASSED_ARP_REQUEST)
         }
         assertFailsWith<IllegalArgumentException> {
             gen.addCountAndPassIfR0NotEquals(3, DROPPED_ETH_BROADCAST)
         }
         assertFailsWith<IllegalArgumentException> {
-            gen.addCountAndDropIfR0LessThan(3, PASSED_ARP)
+            gen.addCountAndDropIfR0LessThan(3, PASSED_ARP_REQUEST)
         }
         assertFailsWith<IllegalArgumentException> {
             gen.addCountAndPassIfR0LessThan(3, DROPPED_ETH_BROADCAST)
         }
         assertFailsWith<IllegalArgumentException> {
-            gen.addCountAndDropIfR0GreaterThan(3, PASSED_ARP)
+            gen.addCountAndDropIfR0GreaterThan(3, PASSED_ARP_REQUEST)
         }
         assertFailsWith<IllegalArgumentException> {
             gen.addCountAndPassIfR0GreaterThan(3, DROPPED_ETH_BROADCAST)
         }
         assertFailsWith<IllegalArgumentException> {
-            gen.addCountAndDropIfBytesAtR0NotEqual(byteArrayOf(1), PASSED_ARP)
+            gen.addCountAndDropIfBytesAtR0NotEqual(byteArrayOf(1), PASSED_ARP_REQUEST)
         }
         assertFailsWith<IllegalArgumentException> {
-            gen.addCountAndDropIfBytesAtR0Equal(byteArrayOf(1), PASSED_ARP)
+            gen.addCountAndDropIfBytesAtR0Equal(byteArrayOf(1), PASSED_ARP_REQUEST)
         }
         assertFailsWith<IllegalArgumentException> {
             gen.addCountAndPassIfBytesAtR0Equal(byteArrayOf(1), DROPPED_ETH_BROADCAST)
         }
         assertFailsWith<IllegalArgumentException> {
-            gen.addCountAndDropIfR0AnyBitsSet(3, PASSED_ARP)
+            gen.addCountAndDropIfR0AnyBitsSet(3, PASSED_ARP_REQUEST)
         }
         assertFailsWith<IllegalArgumentException> {
             gen.addCountAndPassIfR0AnyBitsSet(3, DROPPED_ETH_BROADCAST)
         }
         assertFailsWith<IllegalArgumentException> {
-            gen.addCountAndDropIfR0IsOneOf(setOf(3), PASSED_ARP)
+            gen.addCountAndDropIfR0IsOneOf(setOf(3), PASSED_ARP_REQUEST)
         }
         assertFailsWith<IllegalArgumentException> {
             gen.addCountAndPassIfR0IsOneOf(setOf(3), DROPPED_ETH_BROADCAST)
         }
         assertFailsWith<IllegalArgumentException> {
-            gen.addCountAndDropIfR0IsNoneOf(setOf(3), PASSED_ARP)
+            gen.addCountAndDropIfR0IsNoneOf(setOf(3), PASSED_ARP_REQUEST)
         }
         assertFailsWith<IllegalArgumentException> {
             gen.addCountAndPassIfR0IsNoneOf(setOf(3), DROPPED_ETH_BROADCAST)
         }
         assertFailsWith<IllegalArgumentException> {
-            gen.addCountAndDropIfBytesAtR0EqualsAnyOf(listOf(byteArrayOf(1)), PASSED_ARP)
+            gen.addCountAndDropIfBytesAtR0EqualsAnyOf(listOf(byteArrayOf(1)), PASSED_ARP_REQUEST)
         }
         assertFailsWith<IllegalArgumentException> {
             gen.addCountAndPassIfBytesAtR0EqualsAnyOf(listOf(byteArrayOf(1)), DROPPED_ETH_BROADCAST)
         }
         assertFailsWith<IllegalArgumentException> {
-            gen.addCountAndDropIfBytesAtR0EqualsNoneOf(listOf(byteArrayOf(1)), PASSED_ARP)
+            gen.addCountAndDropIfBytesAtR0EqualsNoneOf(listOf(byteArrayOf(1)), PASSED_ARP_REQUEST)
         }
         assertFailsWith<IllegalArgumentException> {
             gen.addCountAndPassIfBytesAtR0EqualsNoneOf(
@@ -336,61 +363,61 @@
         }
 
         val v4gen = ApfV4Generator(APF_VERSION_3, ramSize, clampSize)
-        assertFailsWith<IllegalArgumentException> { v4gen.addCountAndDrop(PASSED_ARP) }
+        assertFailsWith<IllegalArgumentException> { v4gen.addCountAndDrop(PASSED_ARP_REQUEST) }
         assertFailsWith<IllegalArgumentException> { v4gen.addCountAndPass(DROPPED_ETH_BROADCAST) }
         assertFailsWith<IllegalArgumentException> {
-            v4gen.addCountAndDropIfR0Equals(3, PASSED_ARP)
+            v4gen.addCountAndDropIfR0Equals(3, PASSED_ARP_REQUEST)
         }
         assertFailsWith<IllegalArgumentException> {
             v4gen.addCountAndPassIfR0Equals(3, DROPPED_ETH_BROADCAST)
         }
         assertFailsWith<IllegalArgumentException> {
-            v4gen.addCountAndDropIfR0NotEquals(3, PASSED_ARP)
+            v4gen.addCountAndDropIfR0NotEquals(3, PASSED_ARP_REQUEST)
         }
         assertFailsWith<IllegalArgumentException> {
             v4gen.addCountAndPassIfR0NotEquals(3, DROPPED_ETH_BROADCAST)
         }
         assertFailsWith<IllegalArgumentException> {
-            v4gen.addCountAndDropIfBytesAtR0Equal(byteArrayOf(1), PASSED_ARP)
+            v4gen.addCountAndDropIfBytesAtR0Equal(byteArrayOf(1), PASSED_ARP_REQUEST)
         }
         assertFailsWith<IllegalArgumentException> {
             v4gen.addCountAndPassIfBytesAtR0Equal(byteArrayOf(1), DROPPED_ETH_BROADCAST)
         }
         assertFailsWith<IllegalArgumentException> {
-            v4gen.addCountAndDropIfR0LessThan(3, PASSED_ARP)
+            v4gen.addCountAndDropIfR0LessThan(3, PASSED_ARP_REQUEST)
         }
         assertFailsWith<IllegalArgumentException> {
             v4gen.addCountAndPassIfR0LessThan(3, DROPPED_ETH_BROADCAST)
         }
         assertFailsWith<IllegalArgumentException> {
-            v4gen.addCountAndDropIfR0GreaterThan(3, PASSED_ARP)
+            v4gen.addCountAndDropIfR0GreaterThan(3, PASSED_ARP_REQUEST)
         }
         assertFailsWith<IllegalArgumentException> {
             v4gen.addCountAndPassIfR0GreaterThan(3, DROPPED_ETH_BROADCAST)
         }
         assertFailsWith<IllegalArgumentException> {
-            v4gen.addCountAndDropIfBytesAtR0NotEqual(byteArrayOf(1), PASSED_ARP)
+            v4gen.addCountAndDropIfBytesAtR0NotEqual(byteArrayOf(1), PASSED_ARP_REQUEST)
         }
         assertFailsWith<IllegalArgumentException> {
-            v4gen.addCountAndDropIfR0AnyBitsSet(3, PASSED_ARP)
+            v4gen.addCountAndDropIfR0AnyBitsSet(3, PASSED_ARP_REQUEST)
         }
         assertFailsWith<IllegalArgumentException> {
             v4gen.addCountAndPassIfR0AnyBitsSet(3, DROPPED_ETH_BROADCAST)
         }
         assertFailsWith<IllegalArgumentException> {
-            v4gen.addCountAndDropIfR0IsOneOf(setOf(3), PASSED_ARP)
+            v4gen.addCountAndDropIfR0IsOneOf(setOf(3), PASSED_ARP_REQUEST)
         }
         assertFailsWith<IllegalArgumentException> {
             v4gen.addCountAndPassIfR0IsOneOf(setOf(3), DROPPED_ETH_BROADCAST)
         }
         assertFailsWith<IllegalArgumentException> {
-            v4gen.addCountAndDropIfR0IsNoneOf(setOf(3), PASSED_ARP)
+            v4gen.addCountAndDropIfR0IsNoneOf(setOf(3), PASSED_ARP_REQUEST)
         }
         assertFailsWith<IllegalArgumentException> {
             v4gen.addCountAndPassIfR0IsNoneOf(setOf(3), DROPPED_ETH_BROADCAST)
         }
         assertFailsWith<IllegalArgumentException> {
-            v4gen.addCountAndDropIfBytesAtR0EqualsAnyOf(listOf(byteArrayOf(1)), PASSED_ARP)
+            v4gen.addCountAndDropIfBytesAtR0EqualsAnyOf(listOf(byteArrayOf(1)), PASSED_ARP_REQUEST)
         }
         assertFailsWith<IllegalArgumentException> {
             v4gen.addCountAndPassIfBytesAtR0EqualsAnyOf(
@@ -399,7 +426,7 @@
             )
         }
         assertFailsWith<IllegalArgumentException> {
-            v4gen.addCountAndDropIfBytesAtR0EqualsNoneOf(listOf(byteArrayOf(1)), PASSED_ARP)
+            v4gen.addCountAndDropIfBytesAtR0EqualsNoneOf(listOf(byteArrayOf(1)), PASSED_ARP_REQUEST)
         }
         assertFailsWith<IllegalArgumentException> {
             v4gen.addCountAndPassIfBytesAtR0EqualsNoneOf(
@@ -435,10 +462,10 @@
         )
         assertContentEquals(
                 listOf("0: pass"),
-                ApfJniUtils.disassembleApf(program).map { it.trim() }
+                apfTestHelpers.disassembleApf(program).map { it.trim() }
         )
 
-        var gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        var gen = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
         gen.addDrop()
         program = gen.generate().skipDataAndDebug()
         // encoding DROP opcode: opcode=0, imm_len=0, R=1
@@ -448,10 +475,10 @@
         )
         assertContentEquals(
                 listOf("0: drop"),
-                ApfJniUtils.disassembleApf(program).map { it.trim() }
+                apfTestHelpers.disassembleApf(program).map { it.trim() }
         )
 
-        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        gen = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
         gen.addCountAndPass(129)
         program = gen.generate().skipDataAndDebug()
         // encoding COUNT(PASS) opcode: opcode=0, imm_len=size_of(imm), R=0, imm=counterNumber
@@ -464,10 +491,10 @@
         )
         assertContentEquals(
                 listOf("0: pass        counter=129"),
-                ApfJniUtils.disassembleApf(program).map { it.trim() }
+                apfTestHelpers.disassembleApf(program).map { it.trim() }
         )
 
-        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        gen = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
         gen.addCountAndDrop(1000)
         program = gen.generate().skipDataAndDebug()
         // encoding COUNT(DROP) opcode: opcode=0, imm_len=size_of(imm), R=1, imm=counterNumber
@@ -481,26 +508,27 @@
         )
         assertContentEquals(
                 listOf("0: drop        counter=1000"),
-                ApfJniUtils.disassembleApf(program).map { it.trim() }
+                apfTestHelpers.disassembleApf(program).map { it.trim() }
         )
 
-        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
-        gen.addCountAndPass(PASSED_ARP)
+        gen = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
+        gen.addCountAndPass(PASSED_ARP_REQUEST)
         program = gen.generate().skipDataAndDebug()
         // encoding COUNT(PASS) opcode: opcode=0, imm_len=size_of(imm), R=0, imm=counterNumber
         assertContentEquals(
                 byteArrayOf(
                         encodeInstruction(opcode = 0, immLength = 1, register = 0),
-                        PASSED_ARP.value().toByte()
+                        PASSED_ARP_REQUEST.value().toByte()
                 ),
                 program
         )
+        val expectedCounterValue1 = PASSED_ARP_REQUEST.value()
         assertContentEquals(
-                listOf("0: pass        counter=10"),
-                ApfJniUtils.disassembleApf(program).map { it.trim() }
+                listOf("0: pass        counter=$expectedCounterValue1"),
+                apfTestHelpers.disassembleApf(program).map { it.trim() }
         )
 
-        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        gen = ApfV6Generator(apfInterpreterVersion, 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
@@ -511,12 +539,13 @@
                 ),
                 program
         )
+        val expectedCounterValue2 = DROPPED_ETHERTYPE_NOT_ALLOWED.value()
         assertContentEquals(
-                listOf("0: drop        counter=47"),
-                ApfJniUtils.disassembleApf(program).map { it.trim() }
+                listOf("0: drop        counter=$expectedCounterValue2"),
+                apfTestHelpers.disassembleApf(program).map { it.trim() }
         )
 
-        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        gen = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
         gen.addAllocateR0()
         gen.addAllocate(1500)
         program = gen.generate().skipDataAndDebug()
@@ -536,9 +565,9 @@
         assertContentEquals(listOf(
                 "0: allocate    r0",
                 "2: allocate    1500"
-        ), ApfJniUtils.disassembleApf(program).map { it.trim() })
+        ), apfTestHelpers.disassembleApf(program).map { it.trim() })
 
-        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        gen = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
         gen.addTransmitWithoutChecksum()
         gen.addTransmitL4(30, 40, 50, 256, true)
         program = gen.generate().skipDataAndDebug()
@@ -552,28 +581,30 @@
         assertContentEquals(listOf(
                 "0: transmit    ip_ofs=255",
                 "4: transmitudp ip_ofs=30, csum_ofs=40, csum_start=50, partial_csum=0x0100",
-        ), ApfJniUtils.disassembleApf(program).map { it.trim() })
+        ), apfTestHelpers.disassembleApf(program).map { it.trim() })
 
         val largeByteArray = ByteArray(256) { 0x01 }
-        gen = ApfV6Generator(largeByteArray, APF_VERSION_6, ramSize, clampSize)
+        gen = ApfV6Generator(largeByteArray, apfInterpreterVersion, ramSize, clampSize)
         program = gen.generate()
+        val debugBufferSize = ramSize - program.size - Counter.totalSize()
         assertContentEquals(
                 byteArrayOf(
                         encodeInstruction(opcode = 14, immLength = 2, register = 1), 1, 0
                 ) + largeByteArray + byteArrayOf(
-                        encodeInstruction(opcode = 21, immLength = 1, register = 0), 48, 6, 9
+                        encodeInstruction(opcode = 21, immLength = 1, register = 0),
+                        48, (debugBufferSize shr 8).toByte(), debugBufferSize.toByte()
                 ),
                 program
         )
         assertContentEquals(
                 listOf(
                         "0: data        256, " + "01".repeat(256),
-                        "259: debugbuf    size=1545"
+                        "259: debugbuf    size=$debugBufferSize"
                 ),
-                ApfJniUtils.disassembleApf(program).map { it.trim() }
+                apfTestHelpers.disassembleApf(program).map { it.trim() }
         )
 
-        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        gen = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
         gen.addWriteU8(0x01)
         gen.addWriteU16(0x0102)
         gen.addWriteU32(0x01020304)
@@ -612,9 +643,9 @@
                 "25: write       0x80000000",
                 "30: write       0xfffffffe",
                 "35: write       0xfffefdfc"
-        ), ApfJniUtils.disassembleApf(program).map { it.trim() })
+        ), apfTestHelpers.disassembleApf(program).map { it.trim() })
 
-        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        gen = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
         gen.addWriteU8(R0)
         gen.addWriteU16(R0)
         gen.addWriteU32(R0)
@@ -637,26 +668,26 @@
                 "6: ewrite1     r1",
                 "8: ewrite2     r1",
                 "10: ewrite4     r1"
-        ), ApfJniUtils.disassembleApf(program).map { it.trim() })
+        ), apfTestHelpers.disassembleApf(program).map { it.trim() })
 
-        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
-        gen.addDataCopy(0, 10)
-        gen.addDataCopy(1, 5)
+        gen = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
+        gen.addDataCopy(0, 2)
+        gen.addDataCopy(1, 1)
         gen.addPacketCopy(1000, 255)
         program = gen.generate().skipDataAndDebug()
         assertContentEquals(byteArrayOf(
-                encodeInstruction(25, 0, 1), 10,
-                encodeInstruction(25, 1, 1), 1, 5,
+                encodeInstruction(25, 0, 1), 2,
+                encodeInstruction(25, 1, 1), 1, 1,
                 encodeInstruction(25, 2, 0),
                 0x03.toByte(), 0xe8.toByte(), 0xff.toByte(),
         ), program)
         assertContentEquals(listOf(
-                "0: datacopy    src=0, len=10",
-                "2: datacopy    src=1, len=5",
+                "0: datacopy    src=0, (2)c902",
+                "2: datacopy    src=1, (1)02",
                 "5: pktcopy     src=1000, len=255"
-        ), ApfJniUtils.disassembleApf(program).map { it.trim() })
+        ), apfTestHelpers.disassembleApf(program).map { it.trim() })
 
-        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        gen = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
         gen.addDataCopyFromR0(5)
         gen.addPacketCopyFromR0(5)
         gen.addDataCopyFromR0LenR1()
@@ -669,13 +700,13 @@
                 encodeInstruction(21, 1, 0), 42,
         ), program)
         assertContentEquals(listOf(
-                "0: edatacopy    src=r0, len=5",
-                "3: epktcopy     src=r0, len=5",
-                "6: edatacopy    src=r0, len=r1",
-                "8: epktcopy     src=r0, len=r1"
-        ), ApfJniUtils.disassembleApf(program).map{ it.trim() })
+                "0: edatacopy   src=r0, len=5",
+                "3: epktcopy    src=r0, len=5",
+                "6: edatacopy   src=r0, len=r1",
+                "8: epktcopy    src=r0, len=r1"
+        ), apfTestHelpers.disassembleApf(program).map{ it.trim() })
 
-        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        gen = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
         gen.addJumpIfBytesAtR0Equal(byteArrayOf('a'.code.toByte()), ApfV4Generator.DROP_LABEL)
         program = gen.generate().skipDataAndDebug()
         assertContentEquals(byteArrayOf(
@@ -685,11 +716,11 @@
                 'a'.code.toByte()
         ), program)
         assertContentEquals(listOf(
-                "0: jbseq       r0, 0x1, DROP, 61"
-        ), ApfJniUtils.disassembleApf(program).map{ it.trim() })
+                "0: jbseq       r0, (1), DROP, 61"
+        ), apfTestHelpers.disassembleApf(program).map{ it.trim() })
 
         val qnames = byteArrayOf(1, 'A'.code.toByte(), 1, 'B'.code.toByte(), 0, 0)
-        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        gen = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
         gen.addJumpIfPktAtR0DoesNotContainDnsQ(qnames, 0x0c, ApfV4Generator.DROP_LABEL)
         gen.addJumpIfPktAtR0ContainDnsQ(qnames, 0x0c, ApfV4Generator.DROP_LABEL)
         program = gen.generate().skipDataAndDebug()
@@ -699,11 +730,11 @@
                 encodeInstruction(21, 1, 1), 43, 1, 0x0c.toByte(),
         ) + qnames, program)
         assertContentEquals(listOf(
-                "0: jdnsqne     r0, DROP, 12, (1)A(1)B(0)(0)",
-                "10: jdnsqeq     r0, DROP, 12, (1)A(1)B(0)(0)"
-        ), ApfJniUtils.disassembleApf(program).map{ it.trim() })
+                "0: jdnsqne     r0, DROP, PTR, (1)A(1)B(0)(0)",
+                "10: jdnsqeq     r0, DROP, PTR, (1)A(1)B(0)(0)"
+        ), apfTestHelpers.disassembleApf(program).map{ it.trim() })
 
-        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        gen = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
         gen.addJumpIfPktAtR0DoesNotContainDnsQSafe(qnames, 0x0c, ApfV4Generator.DROP_LABEL)
         gen.addJumpIfPktAtR0ContainDnsQSafe(qnames, 0x0c, ApfV4Generator.DROP_LABEL)
         program = gen.generate().skipDataAndDebug()
@@ -713,11 +744,11 @@
                 encodeInstruction(21, 1, 1), 45, 1, 0x0c.toByte(),
         ) + qnames, program)
         assertContentEquals(listOf(
-                "0: jdnsqnesafe r0, DROP, 12, (1)A(1)B(0)(0)",
-                "10: jdnsqeqsafe r0, DROP, 12, (1)A(1)B(0)(0)"
-        ), ApfJniUtils.disassembleApf(program).map{ it.trim() })
+                "0: jdnsqnesafe r0, DROP, PTR, (1)A(1)B(0)(0)",
+                "10: jdnsqeqsafe r0, DROP, PTR, (1)A(1)B(0)(0)"
+        ), apfTestHelpers.disassembleApf(program).map{ it.trim() })
 
-        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        gen = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
         gen.addJumpIfPktAtR0DoesNotContainDnsA(qnames, ApfV4Generator.DROP_LABEL)
         gen.addJumpIfPktAtR0ContainDnsA(qnames, ApfV4Generator.DROP_LABEL)
         program = gen.generate().skipDataAndDebug()
@@ -729,9 +760,9 @@
         assertContentEquals(listOf(
                 "0: jdnsane     r0, DROP, (1)A(1)B(0)(0)",
                 "9: jdnsaeq     r0, DROP, (1)A(1)B(0)(0)"
-        ), ApfJniUtils.disassembleApf(program).map{ it.trim() })
+        ), apfTestHelpers.disassembleApf(program).map{ it.trim() })
 
-        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        gen = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
         gen.addJumpIfPktAtR0DoesNotContainDnsASafe(qnames, ApfV4Generator.DROP_LABEL)
         gen.addJumpIfPktAtR0ContainDnsASafe(qnames, ApfV4Generator.DROP_LABEL)
         program = gen.generate().skipDataAndDebug()
@@ -743,9 +774,9 @@
         assertContentEquals(listOf(
                 "0: jdnsanesafe r0, DROP, (1)A(1)B(0)(0)",
                 "9: jdnsaeqsafe r0, DROP, (1)A(1)B(0)(0)"
-        ), ApfJniUtils.disassembleApf(program).map{ it.trim() })
+        ), apfTestHelpers.disassembleApf(program).map{ it.trim() })
 
-        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        gen = ApfV6Generator(apfInterpreterVersion, 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)
@@ -759,19 +790,19 @@
                 encodeInstruction(21, 1, 0), 47, 1, 9, 1, 2, 3
         ), program)
 
-        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        gen = ApfV6Generator(apfInterpreterVersion, 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()
         assertContentEquals(listOf(
                 "0: joneof      r0, DROP, { 0, 128, 256, 65536 }",
                 "20: jnoneof     r1, DROP, { 0, 128, 256, 65536 }"
-        ), ApfJniUtils.disassembleApf(program).map{ it.trim() })
+        ), apfTestHelpers.disassembleApf(program).map{ it.trim() })
 
-        gen = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        gen = ApfV6Generator(apfInterpreterVersion, 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)
+        gen.addJumpIfBytesAtR0EqualsNoneOf(listOf(byteArrayOf(1, 2), byteArrayOf(3, 4)), DROP_LABEL)
+        gen.addJumpIfBytesAtR0EqualsNoneOf(listOf(byteArrayOf(1, 1), byteArrayOf(1, 1)), DROP_LABEL)
         program = gen.generate().skipDataAndDebug()
         assertContentEquals(byteArrayOf(
                 encodeInstruction(opcode = 20, immLength = 2, register = 1),
@@ -782,15 +813,36 @@
                 1, 2, 1, 1
         ), program)
         assertContentEquals(listOf(
-                "0: jbseq       r0, 0x2, DROP, { 0102, 0304 }",
-                "9: jbsne       r0, 0x2, DROP, { 0102, 0304 }",
-                "18: jbsne       r0, 0x2, DROP, 0101"
-        ), ApfJniUtils.disassembleApf(program).map{ it.trim() })
+                "0: jbseq       r0, (2), DROP, { 0102, 0304 }[2]",
+                "9: jbsne       r0, (2), DROP, { 0102, 0304 }[2]",
+                "18: jbsne       r0, (2), DROP, 0101"
+        ), apfTestHelpers.disassembleApf(program).map{ it.trim() })
+    }
+
+    @Test
+    fun testApf61InstructionEncoding() {
+        var gen = ApfV61Generator(apfInterpreterVersion, ramSize, clampSize)
+        gen.addCountAndDropIfR0Equals(1, DROPPED_RA)
+        var program = gen.generate().skipDataAndDebug()
+        assertContentEquals(byteArrayOf(
+            encodeInstruction(15, 1, 0),
+            DROPPED_RA.jumpDropLabel.toByte(),
+            1,
+        ), program)
+
+        gen = ApfV61Generator(apfInterpreterVersion, ramSize, clampSize)
+        gen.addCountAndPassIfR0Equals(1, PASSED_MDNS)
+        program = gen.generate().skipDataAndDebug()
+        assertContentEquals(byteArrayOf(
+            encodeInstruction(15, 1, 0),
+            PASSED_MDNS.jumpPassLabel.toByte(),
+            1,
+        ), program)
     }
 
     @Test
     fun testWriteToTxBuffer() {
-        var program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        var program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addAllocate(14)
                 .addWriteU8(0x01)
                 .addWriteU16(0x0203)
@@ -805,19 +857,25 @@
                 .addWriteU32(R1)
                 .addTransmitWithoutChecksum()
                 .generate()
-        assertPass(APF_VERSION_6, program, ByteArray(MIN_PKT_SIZE))
+        apfTestHelpers.assertPass(apfInterpreterVersion, program, ByteArray(MIN_PKT_SIZE))
+        val transmitPackets = apfTestHelpers.consumeTransmittedPackets(1)
         assertContentEquals(
                 byteArrayOf(
                         0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0xff.toByte(),
                         0xff.toByte(), 0xff.toByte(), 0xfe.toByte(), 0xff.toByte(), 0xfe.toByte(),
                         0xfd.toByte(), 0xfc.toByte(), 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07),
-                ApfJniUtils.getTransmittedPacket()
+                transmitPackets[0]
         )
     }
 
     @Test
     fun testCopyToTxBuffer() {
-        var program = ApfV6Generator(byteArrayOf(33, 34, 35), APF_VERSION_6, ramSize, clampSize)
+        var program = ApfV6Generator(
+            byteArrayOf(33, 34, 35),
+            apfInterpreterVersion,
+            ramSize,
+            clampSize
+        )
                 .addAllocate(14)
                 .addDataCopy(3, 2) // arg1=src, arg2=len
                 .addDataCopy(5, 1) // arg1=src, arg2=len
@@ -835,16 +893,17 @@
                 .addPacketCopyFromR0LenR1()
                 .addTransmitWithoutChecksum()
                 .generate()
-        assertPass(APF_VERSION_6, program, testPacket)
+        apfTestHelpers.assertPass(apfInterpreterVersion, program, testPacket)
+        val transmitPackets = apfTestHelpers.consumeTransmittedPackets(1)
         assertContentEquals(
                 byteArrayOf(33, 34, 35, 1, 2, 3, 4, 33, 34, 35, 1, 2, 3, 4),
-                ApfJniUtils.getTransmittedPacket()
+                transmitPackets[0]
         )
     }
 
     @Test
     fun testCopyContentToTxBuffer() {
-        val program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        val program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addAllocate(18)
                 .addDataCopy(HexDump.hexStringToByteArray("112233445566"))
                 .addDataCopy(HexDump.hexStringToByteArray("223344"))
@@ -854,36 +913,195 @@
                 .generate()
         assertContentEquals(listOf(
                 "0: data        9, 112233445566778899",
-                "12: debugbuf    size=1772",
+                "12: debugbuf    size=${ramSize - program.size - Counter.totalSize()}",
                 "16: allocate    18",
-                "20: datacopy    src=3, len=6",
-                "23: datacopy    src=4, len=3",
-                "26: datacopy    src=9, len=3",
-                "29: datacopy    src=3, len=6",
+                "20: datacopy    src=3, (6)112233445566",
+                "23: datacopy    src=4, (3)223344",
+                "26: datacopy    src=9, (3)778899",
+                "29: datacopy    src=3, (6)112233445566",
                 "32: transmit    ip_ofs=255"
-        ), ApfJniUtils.disassembleApf(program).map{ it.trim() })
-        assertPass(APF_VERSION_6, program, testPacket)
-        val transmitPkt = HexDump.toHexString(ApfJniUtils.getTransmittedPacket())
+        ), apfTestHelpers.disassembleApf(program).map{ it.trim() })
+        apfTestHelpers.assertPass(apfInterpreterVersion, program, testPacket)
+        val transmitPackets = apfTestHelpers.consumeTransmittedPackets(1)
+        val transmitPkt = HexDump.toHexString(transmitPackets[0])
         assertEquals("112233445566223344778899112233445566", transmitPkt)
     }
 
     @Test
+    fun testCopyLargeContentToTxBufferWithCompression() {
+        val program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
+            .addAllocate(300)
+            // Chunked into 255 and 35 bytes.
+            // 255 bytes appended to data region.
+            // 35 bytes compressed by reusing the duplicated chunk from the data region.
+            .addDataCopy(ByteArray(290) { 1 })
+            // Appended to the data region.
+            .addDataCopy(ByteArray(5) { 2 })
+            // Compressed by reusing the duplicated chunk from the data region.
+            .addDataCopy(ByteArray(3) { 1 } + ByteArray(2) { 2 })
+            .addTransmitWithoutChecksum()
+            .generate()
+
+        val byteHexString = "01".repeat(255) + "02".repeat(5)
+        assertContentEquals(listOf(
+            "0: data        260, $byteHexString",
+            "263: debugbuf    size=${ramSize - program.size - Counter.totalSize()}",
+            "267: allocate    300",
+            "271: datacopy    src=3, (255)" + "01".repeat(255),
+            "274: datacopy    src=3, (35)" + "01".repeat(35),
+            "277: datacopy    src=258, (5)" + "02".repeat(5),
+            "281: datacopy    src=255, (5)" + "01".repeat(3) + "02".repeat(2),
+            "284: transmit    ip_ofs=255"
+        ), apfTestHelpers.disassembleApf(program).map{ it.trim() })
+        apfTestHelpers.assertPass(apfInterpreterVersion, program, testPacket)
+        val transmitPackets = apfTestHelpers.consumeTransmittedPackets(1)
+        val transmitPkt = HexDump.toHexString(transmitPackets[0])
+        assertEquals(
+            "01".repeat(290) + "02".repeat(5) + "01".repeat(3) + "02".repeat(2),
+            transmitPkt
+        )
+    }
+
+    @Test
+    fun testCopyLargeContentToTxBufferWithoutCompression() {
+        val program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
+            .addAllocate(300)
+            // Chunked into 255 and 45 bytes and then appended to the data region.
+            .addDataCopy(ByteArray(255) { 3 } + ByteArray(45) { 4 })
+            .addTransmitWithoutChecksum()
+            .generate()
+
+        val byteHexString = "03".repeat(255) + "04".repeat(45)
+        assertContentEquals(listOf(
+            "0: data        300, $byteHexString",
+            "303: debugbuf    size=${ramSize - program.size - Counter.totalSize()}",
+            "307: allocate    300",
+            "311: datacopy    src=3, (255)" + "03".repeat(255),
+            "314: datacopy    src=258, (45)" + "04".repeat(45),
+            "318: transmit    ip_ofs=255"
+        ), apfTestHelpers.disassembleApf(program).map{ it.trim() })
+        apfTestHelpers.assertPass(apfInterpreterVersion, program, testPacket)
+        val transmitPackets = apfTestHelpers.consumeTransmittedPackets(1)
+        val transmitPkt = HexDump.toHexString(transmitPackets[0])
+        assertEquals( "03".repeat(255) + "04".repeat(45), transmitPkt)
+    }
+
+    @Test
+    fun testJBSPTRMATCHOpcodeEncoding() {
+        assumeTrue(apfInterpreterVersion != ApfJniUtils.APF_INTERPRETER_VERSION_V6)
+        val dataBytes = HexDump.hexStringToByteArray(
+            "01020304050607080910111213141516171819202122232425262728293031323334353637383940"
+        )
+        val bytes1 = HexDump.hexStringToByteArray("0102")
+        val bytes2 = HexDump.hexStringToByteArray("0304")
+        val bytes3 = HexDump.hexStringToByteArray("0506")
+        val bytes4 = HexDump.hexStringToByteArray("0708")
+        val bytes5 = HexDump.hexStringToByteArray("0910")
+        val bytes6 = HexDump.hexStringToByteArray("1112")
+        val bytes7 = HexDump.hexStringToByteArray("1314")
+        val bytes8 = HexDump.hexStringToByteArray("1516")
+        val bytes9 = HexDump.hexStringToByteArray("1718")
+        val bytes10 = HexDump.hexStringToByteArray("1920")
+        val bytes11 = HexDump.hexStringToByteArray("2122")
+        val bytes12 = HexDump.hexStringToByteArray("2324")
+        val bytes13 = HexDump.hexStringToByteArray("2526")
+        val bytes14 = HexDump.hexStringToByteArray("2728")
+        val bytes15 = HexDump.hexStringToByteArray("2930")
+        val bytes16 = HexDump.hexStringToByteArray("3132")
+        val bytes17 = HexDump.hexStringToByteArray("3334")
+        val bytesAtOddIndex = HexDump.hexStringToByteArray("0203")
+        val notExistBytes = HexDump.hexStringToByteArray("ffff")
+        val total17BytesList = listOf(
+            bytes1,
+            bytes2,
+            bytes3,
+            bytes4,
+            bytes5,
+            bytes6,
+            bytes7,
+            bytes8,
+            bytes9,
+            bytes10,
+            bytes11,
+            bytes12,
+            bytes13,
+            bytes14,
+            bytes15,
+            bytes16,
+            bytes17,
+        )
+        val joinedBytes: ByteArray = total17BytesList.flatMap { it.toList() }.toByteArray()
+        var program = ApfV61Generator(apfInterpreterVersion, ramSize, clampSize)
+            .addPreloadData(dataBytes)
+            .addJumpIfBytesAtOffsetEqualsNoneOf(0, listOf(bytes1, notExistBytes), PASS_LABEL)
+            .addJumpIfBytesAtOffsetEqualsAnyOf(1, listOf(bytes1, bytes2), PASS_LABEL)
+            .addJumpIfBytesAtOffsetEqualsNoneOf(2, listOf(notExistBytes), PASS_LABEL)
+            .addJumpIfBytesAtOffsetEqualsAnyOf(3, total17BytesList, PASS_LABEL)
+            .addJumpIfBytesAtOffsetEqualsNoneOf(4, listOf(bytesAtOddIndex), PASS_LABEL)
+            .addJumpIfBytesAtOffsetEqualsNoneOf(6, listOf(joinedBytes), PASS_LABEL)
+            .generate()
+        var debugBufferSize = ramSize - program.size - Counter.totalSize()
+        assertContentEquals(listOf(
+            "0: data        40, ${HexDump.toHexString(dataBytes)}",
+            "43: debugbuf    size=$debugBufferSize",
+            "47: jbsptrne    pktofs=0, (2), PASS, @0[0102]",
+            "52: li          r0, 0",
+            "53: jbsne       r0, (2), PASS, ffff",
+            "58: jbsptreq    pktofs=1, (2), PASS, { @0[0102], @2[0304] }[2]",
+            "64: li          r0, 2",
+            "66: jbsne       r0, (2), PASS, ffff",
+            "71: jbsptreq    pktofs=3, (2), PASS, { @0[0102], @2[0304], @4[0506], @6[0708], " +
+                "@8[0910], @10[1112], @12[1314], @14[1516], @16[1718], @18[1920], @20[2122], " +
+                "@22[2324], @24[2526], @26[2728], @28[2930], @30[3132] }[16]",
+            "91: jbsptreq    pktofs=3, (2), PASS, @32[3334]",
+            "96: li          r0, 4",
+            "98: jbsne       r0, (2), PASS, 0203",
+            "103: li          r0, 6",
+            "105: jbsne       r0, (34), PASS, ${HexDump.toHexString(joinedBytes)}",
+        ), apfTestHelpers.disassembleApf(program).map{ it.trim() })
+
+        val largePrefix = ByteArray(510) { 0 }
+        program = ApfV61Generator(apfInterpreterVersion, ramSize, clampSize)
+            .addPreloadData(largePrefix + dataBytes)
+            .addJumpIfBytesAtOffsetEqualsAnyOf(1, listOf(bytes1, bytes2), PASS_LABEL)
+            .generate()
+        debugBufferSize = ramSize - program.size - Counter.totalSize()
+        assertContentEquals(listOf(
+            "0: data        550, ${HexDump.toHexString(largePrefix + dataBytes)}",
+            "553: debugbuf    size=$debugBufferSize",
+            "557: jbsptreq    pktofs=1, (2), PASS, @510[0102]",
+            "562: li          r0, 1",
+            "564: jbseq       r0, (2), PASS, 0304",
+        ), apfTestHelpers.disassembleApf(program).map{ it.trim() })
+    }
+
+    @Test
     fun testPassDrop() {
-        var program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        var program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addDrop()
                 .addPass()
                 .generate()
-        assertDrop(APF_VERSION_6, program, testPacket)
+        apfTestHelpers.assertDrop(apfInterpreterVersion, program, testPacket)
 
-        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addCountAndDrop(Counter.DROPPED_ETH_BROADCAST)
                 .generate()
-        verifyProgramRun(APF_VERSION_6, program, testPacket, DROPPED_ETH_BROADCAST)
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            testPacket,
+            DROPPED_ETH_BROADCAST
+        )
 
-        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
-                .addCountAndPass(Counter.PASSED_ARP)
+        program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
+                .addCountAndPass(Counter.PASSED_ARP_REQUEST)
                 .generate()
-        verifyProgramRun(APF_VERSION_6, program, testPacket, PASSED_ARP)
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            testPacket,
+            PASSED_ARP_REQUEST
+        )
     }
 
     @Test
@@ -894,7 +1112,7 @@
         )
         doTestLoadStoreCounter (
                 { mutableMapOf(TOTAL_PACKETS to 1) },
-                { ApfV6Generator(APF_VERSION_6, ramSize, clampSize) }
+                { ApfV6Generator(apfInterpreterVersion, ramSize, clampSize) }
         )
     }
 
@@ -903,14 +1121,14 @@
             getGenerator: () -> ApfV4GeneratorBase<*>
     ) {
         val program = getGenerator()
-                .addIncrementCounter(PASSED_ARP, 2)
+                .addIncrementCounter(PASSED_ARP_REQUEST, 2)
                 .addPass()
                 .generate()
         var dataRegion = ByteArray(Counter.totalSize()) { 0 }
-        assertVerdict(APF_VERSION_6, PASS, program, testPacket, dataRegion)
+        apfTestHelpers.assertVerdict(apfInterpreterVersion, PASS, program, testPacket, dataRegion)
         var counterMap = decodeCountersIntoMap(dataRegion)
         var expectedMap = getInitialMap()
-        expectedMap[PASSED_ARP] = 2
+        expectedMap[PASSED_ARP_REQUEST] = 2
         assertEquals(expectedMap, counterMap)
     }
 
@@ -925,11 +1143,20 @@
     @Test
     fun testV6CountAndPassDropCompareR0() {
         doTestCountAndPassDropCompareR0(
-                getGenerator = { ApfV6Generator(APF_VERSION_6, ramSize, clampSize) },
+                getGenerator = { ApfV6Generator(apfInterpreterVersion, ramSize, clampSize) },
                 incTotal = true
         )
     }
 
+    @Test
+    fun testV61CountAndPassDropCompareR0() {
+        assumeTrue(apfInterpreterVersion > ApfJniUtils.APF_INTERPRETER_VERSION_V6)
+        doTestCountAndPassDropCompareR0(
+            getGenerator = { ApfV61Generator(apfInterpreterVersion, ramSize, clampSize) },
+            incTotal = true
+        )
+    }
+
     private fun doTestCountAndPassDropCompareR0(
             getGenerator: () -> ApfV4GeneratorBase<*>,
             incTotal: Boolean
@@ -940,8 +1167,8 @@
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(
-                APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+                apfInterpreterVersion,
                 program,
                 testPacket,
                 DROPPED_ETH_BROADCAST,
@@ -950,11 +1177,17 @@
 
         program = getGenerator()
                 .addLoadImmediate(R0, 123)
-                .addCountAndPassIfR0Equals(123, Counter.PASSED_ARP)
+                .addCountAndPassIfR0Equals(123, Counter.PASSED_ARP_REQUEST)
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(APF_VERSION_6, program, testPacket, PASSED_ARP, incTotal = incTotal)
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            testPacket,
+            PASSED_ARP_REQUEST,
+            incTotal = incTotal
+        )
 
         program = getGenerator()
                 .addLoadImmediate(R0, 123)
@@ -962,8 +1195,8 @@
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(
-                APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+                apfInterpreterVersion,
                 program,
                 testPacket,
                 DROPPED_ETH_BROADCAST,
@@ -972,11 +1205,17 @@
 
         program = getGenerator()
                 .addLoadImmediate(R0, 123)
-                .addCountAndPassIfR0NotEquals(124, Counter.PASSED_ARP)
+                .addCountAndPassIfR0NotEquals(124, Counter.PASSED_ARP_REQUEST)
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(APF_VERSION_6, program, testPacket, PASSED_ARP, incTotal = incTotal)
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            testPacket,
+            PASSED_ARP_REQUEST,
+            incTotal = incTotal
+        )
 
         program = getGenerator()
                 .addLoadImmediate(R0, 123)
@@ -984,8 +1223,8 @@
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(
-                APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+                apfInterpreterVersion,
                 program,
                 testPacket,
                 DROPPED_ETH_BROADCAST,
@@ -994,11 +1233,17 @@
 
         program = getGenerator()
                 .addLoadImmediate(R0, 123)
-                .addCountAndPassIfR0LessThan(124, Counter.PASSED_ARP)
+                .addCountAndPassIfR0LessThan(124, Counter.PASSED_ARP_REQUEST)
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(APF_VERSION_6, program, testPacket, PASSED_ARP, incTotal = incTotal)
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            testPacket,
+            PASSED_ARP_REQUEST,
+            incTotal = incTotal
+        )
 
         program = getGenerator()
                 .addLoadImmediate(R0, 123)
@@ -1006,8 +1251,8 @@
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(
-                APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+                apfInterpreterVersion,
                 program,
                 testPacket,
                 DROPPED_ETH_BROADCAST,
@@ -1016,11 +1261,17 @@
 
         program = getGenerator()
                 .addLoadImmediate(R0, 123)
-                .addCountAndPassIfR0GreaterThan(122, Counter.PASSED_ARP)
+                .addCountAndPassIfR0GreaterThan(122, Counter.PASSED_ARP_REQUEST)
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(APF_VERSION_6, program, testPacket, PASSED_ARP, incTotal = incTotal)
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            testPacket,
+            PASSED_ARP_REQUEST,
+            incTotal = incTotal
+        )
 
         program = getGenerator()
                 .addLoadImmediate(R0, 1)
@@ -1029,8 +1280,8 @@
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(
-                APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+                apfInterpreterVersion,
                 program,
                 testPacket,
                 DROPPED_ETH_BROADCAST,
@@ -1040,11 +1291,17 @@
         program = getGenerator()
                 .addLoadImmediate(R0, 1)
                 .addCountAndPassIfBytesAtR0NotEqual(
-                        byteArrayOf(5, 5), PASSED_ARP)
+                        byteArrayOf(5, 5), PASSED_ARP_REQUEST)
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(APF_VERSION_6, program, testPacket, PASSED_ARP, incTotal = incTotal)
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            testPacket,
+            PASSED_ARP_REQUEST,
+            incTotal = incTotal
+        )
 
         program = getGenerator()
                 .addLoadImmediate(R0, 1)
@@ -1052,8 +1309,8 @@
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(
-                APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+                apfInterpreterVersion,
                 program,
                 testPacket,
                 DROPPED_ETH_BROADCAST,
@@ -1062,11 +1319,17 @@
 
         program = getGenerator()
                 .addLoadImmediate(R0, 1)
-                .addCountAndPassIfR0AnyBitsSet(0xffff, PASSED_ARP)
+                .addCountAndPassIfR0AnyBitsSet(0xffff, PASSED_ARP_REQUEST)
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(APF_VERSION_6, program, testPacket, PASSED_ARP, incTotal = incTotal)
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            testPacket,
+            PASSED_ARP_REQUEST,
+            incTotal = incTotal
+        )
 
         program = getGenerator()
                 .addLoadImmediate(R0, 123)
@@ -1074,8 +1337,8 @@
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(
-                APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+                apfInterpreterVersion,
                 program,
                 testPacket,
                 DROPPED_ETH_BROADCAST,
@@ -1084,11 +1347,17 @@
 
         program = getGenerator()
                 .addLoadImmediate(R0, 123)
-                .addCountAndPassIfR0IsOneOf(setOf(123), PASSED_ARP)
+                .addCountAndPassIfR0IsOneOf(setOf(123), PASSED_ARP_REQUEST)
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(APF_VERSION_6, program, testPacket, PASSED_ARP, incTotal = incTotal)
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            testPacket,
+            PASSED_ARP_REQUEST,
+            incTotal = incTotal
+        )
 
         program = getGenerator()
                 .addLoadImmediate(R0, 123)
@@ -1096,8 +1365,8 @@
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(
-                APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+                apfInterpreterVersion,
                 program,
                 testPacket,
                 DROPPED_ETH_BROADCAST,
@@ -1106,11 +1375,17 @@
 
         program = getGenerator()
                 .addLoadImmediate(R0, 123)
-                .addCountAndPassIfR0IsNoneOf(setOf(124), PASSED_ARP)
+                .addCountAndPassIfR0IsNoneOf(setOf(124), PASSED_ARP_REQUEST)
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(APF_VERSION_6, program, testPacket, PASSED_ARP, incTotal = incTotal)
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            testPacket,
+            PASSED_ARP_REQUEST,
+            incTotal = incTotal
+        )
 
         program = getGenerator()
                 .addLoadImmediate(R0, 123)
@@ -1118,8 +1393,8 @@
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(
-                APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+                apfInterpreterVersion,
                 program,
                 testPacket,
                 DROPPED_ETH_BROADCAST,
@@ -1128,11 +1403,17 @@
 
         program = getGenerator()
                 .addLoadImmediate(R0, 123)
-                .addCountAndPassIfR0IsOneOf(setOf(123, 124), PASSED_ARP)
+                .addCountAndPassIfR0IsOneOf(setOf(123, 124), PASSED_ARP_REQUEST)
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(APF_VERSION_6, program, testPacket, PASSED_ARP, incTotal = incTotal)
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            testPacket,
+            PASSED_ARP_REQUEST,
+            incTotal = incTotal
+        )
 
         program = getGenerator()
                 .addLoadImmediate(R0, 123)
@@ -1140,8 +1421,8 @@
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(
-                APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+                apfInterpreterVersion,
                 program,
                 testPacket,
                 DROPPED_ETH_BROADCAST,
@@ -1150,11 +1431,17 @@
 
         program = getGenerator()
                 .addLoadImmediate(R0, 123)
-                .addCountAndPassIfR0IsNoneOf(setOf(122, 124), PASSED_ARP)
+                .addCountAndPassIfR0IsNoneOf(setOf(122, 124), PASSED_ARP_REQUEST)
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(APF_VERSION_6, program, testPacket, PASSED_ARP, incTotal = incTotal)
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            testPacket,
+            PASSED_ARP_REQUEST,
+            incTotal = incTotal
+        )
 
         program = getGenerator()
                 .addLoadImmediate(R0, 0)
@@ -1165,8 +1452,8 @@
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(
-                APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+                apfInterpreterVersion,
                 program,
                 testPacket,
                 DROPPED_ETH_BROADCAST,
@@ -1177,12 +1464,18 @@
                 .addLoadImmediate(R0, 0)
                 .addCountAndPassIfBytesAtR0EqualsAnyOf(
                         listOf(byteArrayOf(1, 2), byteArrayOf(3, 4)),
-                        PASSED_ARP
+                        PASSED_ARP_REQUEST
                 )
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(APF_VERSION_6, program, testPacket, PASSED_ARP, incTotal = incTotal)
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            testPacket,
+            PASSED_ARP_REQUEST,
+            incTotal = incTotal
+        )
 
         program = getGenerator()
                 .addLoadImmediate(R0, 0)
@@ -1193,8 +1486,8 @@
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(
-                APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+                apfInterpreterVersion,
                 program,
                 testPacket,
                 DROPPED_ETH_BROADCAST,
@@ -1205,12 +1498,18 @@
                 .addLoadImmediate(R0, 0)
                 .addCountAndPassIfBytesAtR0EqualsNoneOf(
                         listOf(byteArrayOf(1, 3), byteArrayOf(3, 4)),
-                        PASSED_ARP
+                        PASSED_ARP_REQUEST
                 )
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(APF_VERSION_6, program, testPacket, PASSED_ARP, incTotal = incTotal)
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            testPacket,
+            PASSED_ARP_REQUEST,
+            incTotal = incTotal
+        )
 
         program = getGenerator()
                 .addLoadImmediate(R0, 1)
@@ -1219,8 +1518,8 @@
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(
-                APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+                apfInterpreterVersion,
                 program,
                 testPacket,
                 DROPPED_ETH_BROADCAST,
@@ -1230,11 +1529,17 @@
         program = getGenerator()
                 .addLoadImmediate(R0, 1)
                 .addCountAndPassIfBytesAtR0Equal(
-                        byteArrayOf(2, 3), PASSED_ARP)
+                        byteArrayOf(2, 3), PASSED_ARP_REQUEST)
                 .addPass()
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(APF_VERSION_6, program, testPacket, PASSED_ARP, incTotal = incTotal)
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            testPacket,
+            PASSED_ARP_REQUEST,
+            incTotal = incTotal
+        )
     }
 
     @Test
@@ -1243,8 +1548,8 @@
                 .addCountAndDrop(Counter.DROPPED_ETH_BROADCAST)
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(
-                APF_VERSION_6,
+        apfTestHelpers.verifyProgramRun(
+                apfInterpreterVersion,
                 program,
                 testPacket,
                 DROPPED_ETH_BROADCAST,
@@ -1252,10 +1557,16 @@
         )
 
         program = ApfV4Generator(APF_VERSION_3, ramSize, clampSize)
-                .addCountAndPass(Counter.PASSED_ARP)
+                .addCountAndPass(Counter.PASSED_ARP_REQUEST)
                 .addCountTrampoline()
                 .generate()
-        verifyProgramRun(APF_VERSION_6, program, testPacket, PASSED_ARP, incTotal = false)
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            testPacket,
+            PASSED_ARP_REQUEST,
+            incTotal = false
+        )
     }
 
     @Test
@@ -1265,31 +1576,36 @@
                 .addCountTrampoline()
                 .generate()
         var dataRegion = ByteArray(Counter.totalSize()) { 0 }
-        assertVerdict(APF_VERSION_6, DROP, program, testPacket, dataRegion)
+        apfTestHelpers.assertVerdict(apfInterpreterVersion, DROP, program, testPacket, dataRegion)
         assertContentEquals(ByteArray(Counter.totalSize()) { 0 }, dataRegion)
 
         program = ApfV4Generator(APF_VERSION_2, ramSize, clampSize)
-                .addCountAndPass(PASSED_ARP)
+                .addCountAndPass(PASSED_ARP_REQUEST)
                 .addCountTrampoline()
                 .generate()
         dataRegion = ByteArray(Counter.totalSize()) { 0 }
-        assertVerdict(APF_VERSION_6, PASS, program, testPacket, dataRegion)
+        apfTestHelpers.assertVerdict(apfInterpreterVersion, PASS, program, testPacket, dataRegion)
         assertContentEquals(ByteArray(Counter.totalSize()) { 0 }, dataRegion)
     }
 
     @Test
     fun testAllocateFailure() {
-        val program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        val program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 // allocate size: 65535 > sizeof(apf_test_buffer): 1514, trigger allocate failure.
                 .addAllocate(65535)
                 .addDrop()
                 .generate()
-        verifyProgramRun(APF_VERSION_6, program, testPacket, PASSED_ALLOCATE_FAILURE)
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            testPacket,
+            PASSED_ALLOCATE_FAILURE
+        )
     }
 
     @Test
     fun testTransmitFailure() {
-        val program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        val program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addAllocate(14)
                 // len: 13 is less than ETH_HLEN, trigger transmit failure.
                 .addLoadImmediate(R0, 13)
@@ -1297,7 +1613,12 @@
                 .addTransmitWithoutChecksum()
                 .addDrop()
                 .generate()
-        verifyProgramRun(APF_VERSION_6, program, testPacket, PASSED_TRANSMIT_FAILURE)
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            testPacket,
+            PASSED_TRANSMIT_FAILURE
+        )
     }
 
     @Test
@@ -1325,7 +1646,7 @@
                 0x00, 0x00, 0x01, 0x80, 0x01, 0x00, 0x00, 0x00, 0x78, 0x00, 0x04, 0xc0, 0xa8, 0x01,
                 0x09,
         ).map { it.toByte() }.toByteArray()
-        val program = ApfV6Generator(etherIpv4UdpPacket, APF_VERSION_6, ramSize, clampSize)
+        val program = ApfV6Generator(etherIpv4UdpPacket, apfInterpreterVersion, ramSize, clampSize)
                 .addAllocate(etherIpv4UdpPacket.size)
                 .addDataCopy(3, etherIpv4UdpPacket.size) // arg1=src, arg2=len
                 .addTransmitL4(
@@ -1336,8 +1657,9 @@
                         true // isUdp
                 )
                 .generate()
-        assertPass(APF_VERSION_6, program, testPacket)
-        val txBuf = ByteBuffer.wrap(ApfJniUtils.getTransmittedPacket())
+        apfTestHelpers.assertPass(apfInterpreterVersion, program, testPacket)
+        val transmitPackets = apfTestHelpers.consumeTransmittedPackets(1)
+        val txBuf = ByteBuffer.wrap(transmitPackets[0])
         Struct.parse(EthernetHeader::class.java, txBuf)
         val ipv4Hdr = Struct.parse(Ipv4Header::class.java, txBuf)
         val udpHdr = Struct.parse(UdpHeader::class.java, txBuf)
@@ -1371,33 +1693,33 @@
                 0x00, 0x01, 0x00, 0x01 // type = A, class = 0x0001
         ).map { it.toByte() }.toByteArray()
 
-        var program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        var program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfPktAtR0ContainDnsQ(needlesMatch, 0x01, DROP_LABEL) // arg2=qtype
                 .addPass()
                 .generate()
-        assertDrop(APF_VERSION_6, program, udpPayload)
+        apfTestHelpers.assertDrop(apfInterpreterVersion, program, udpPayload)
 
-        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfPktAtR0ContainDnsQSafe(needlesMatch, 0x01, DROP_LABEL)
                 .addPass()
                 .generate()
-        assertDrop(APF_VERSION_6, program, udpPayload)
+        apfTestHelpers.assertDrop(apfInterpreterVersion, program, udpPayload)
 
-        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfPktAtR0DoesNotContainDnsQ(needlesMatch, 0x01, DROP_LABEL) // arg2=qtype
                 .addPass()
                 .generate()
-        assertPass(APF_VERSION_6, program, udpPayload)
+        apfTestHelpers.assertPass(apfInterpreterVersion, program, udpPayload)
 
-        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfPktAtR0DoesNotContainDnsQSafe(needlesMatch, 0x01, DROP_LABEL) // arg2=qtype
                 .addPass()
                 .generate()
-        assertPass(APF_VERSION_6, program, udpPayload)
+        apfTestHelpers.assertPass(apfInterpreterVersion, program, udpPayload)
 
         val badUdpPayload = intArrayOf(
                 0x00, 0x00, 0x00, 0x00, // tid = 0x00, flags = 0x00,
@@ -1414,19 +1736,31 @@
                 0x00, 0x01, 0x00, 0x01 // type = A, class = 0x0001
         ).map { it.toByte() }.toByteArray()
 
-        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        program = ApfV6Generator(apfInterpreterVersion, 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)
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            badUdpPayload,
+            CORRUPT_DNS_PACKET,
+            result = DROP
+        )
 
-        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfPktAtR0ContainDnsQSafe(needlesMatch, 0x01, DROP_LABEL) // arg2=qtype
                 .addPass()
                 .generate()
-        verifyProgramRun(APF_VERSION_6, program, badUdpPayload, CORRUPT_DNS_PACKET, result = PASS)
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            badUdpPayload,
+            CORRUPT_DNS_PACKET,
+            result = PASS
+        )
     }
 
     @Test
@@ -1460,33 +1794,33 @@
                 0x00, 0x04, 0xc0, 0xa8, 0x01, 0x09 // rdlengh = 4, rdata = 192.168.1.9
         ).map { it.toByte() }.toByteArray()
 
-        var program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        var program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfPktAtR0ContainDnsA(needlesMatch, DROP_LABEL)
                 .addPass()
                 .generate()
-        assertDrop(APF_VERSION_6, program, udpPayload)
+        apfTestHelpers.assertDrop(apfInterpreterVersion, program, udpPayload)
 
-        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfPktAtR0ContainDnsASafe(needlesMatch, DROP_LABEL)
                 .addPass()
                 .generate()
-        assertDrop(APF_VERSION_6, program, udpPayload)
+        apfTestHelpers.assertDrop(apfInterpreterVersion, program, udpPayload)
 
-        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfPktAtR0DoesNotContainDnsA(needlesMatch, DROP_LABEL)
                 .addPass()
                 .generate()
-        assertPass(APF_VERSION_6, program, udpPayload)
+        apfTestHelpers.assertPass(apfInterpreterVersion, program, udpPayload)
 
-        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfPktAtR0DoesNotContainDnsASafe(needlesMatch, DROP_LABEL)
                 .addPass()
                 .generate()
-        assertPass(APF_VERSION_6, program, udpPayload)
+        apfTestHelpers.assertPass(apfInterpreterVersion, program, udpPayload)
 
         val badUdpPayload = intArrayOf(
                 0x00, 0x00, 0x84, 0x00, // tid = 0x00, flags = 0x8400,
@@ -1507,19 +1841,31 @@
                 0x00, 0x04, 0xc0, 0xa8, 0x01, 0x09 // rdlengh = 4, rdata = 192.168.1.9
         ).map { it.toByte() }.toByteArray()
 
-        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfPktAtR0ContainDnsA(needlesMatch, DROP_LABEL)
                 .addPass()
                 .generate()
-        verifyProgramRun(APF_VERSION_6, program, badUdpPayload, CORRUPT_DNS_PACKET, result = DROP)
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            badUdpPayload,
+            CORRUPT_DNS_PACKET,
+            result = DROP
+        )
 
-        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfPktAtR0ContainDnsASafe(needlesMatch, DROP_LABEL)
                 .addPass()
                 .generate()
-        verifyProgramRun(APF_VERSION_6, program, badUdpPayload, CORRUPT_DNS_PACKET, result = PASS)
+        apfTestHelpers.verifyProgramRun(
+            apfInterpreterVersion,
+            program,
+            badUdpPayload,
+            CORRUPT_DNS_PACKET,
+            result = PASS
+        )
     }
 
     @Test
@@ -1531,7 +1877,7 @@
 
     @Test
     fun testJumpMultipleByteSequencesMatch() {
-        var program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        var program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
                 .addJumpIfBytesAtR0EqualsAnyOf(
                         listOf(byteArrayOf(1, 2, 3), byteArrayOf(6, 5, 4)),
@@ -1539,9 +1885,9 @@
                 )
                 .addPass()
                 .generate()
-        assertDrop(APF_VERSION_6, program, testPacket)
+        apfTestHelpers.assertDrop(apfInterpreterVersion, program, testPacket)
 
-        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addLoadImmediate(R0, 2)
                 .addJumpIfBytesAtR0EqualsAnyOf(
                         listOf(byteArrayOf(1, 2, 3), byteArrayOf(6, 5, 4)),
@@ -1549,72 +1895,104 @@
                 )
                 .addPass()
                 .generate()
-        assertPass(APF_VERSION_6, program, testPacket)
+        apfTestHelpers.assertPass(apfInterpreterVersion, program, testPacket)
 
-        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addLoadImmediate(R0, 1)
-                .addJumpIfBytesAtR0EqualNoneOf(
+                .addJumpIfBytesAtR0EqualsNoneOf(
                         listOf(byteArrayOf(1, 2, 3), byteArrayOf(6, 5, 4)),
                         DROP_LABEL
                 )
                 .addPass()
                 .generate()
-        assertDrop(APF_VERSION_6, program, testPacket)
+        apfTestHelpers.assertDrop(apfInterpreterVersion, program, testPacket)
 
-        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addLoadImmediate(R0, 0)
-                .addJumpIfBytesAtR0EqualNoneOf(
+                .addJumpIfBytesAtR0EqualsNoneOf(
                         listOf(byteArrayOf(1, 2, 3), byteArrayOf(6, 5, 4)),
                         DROP_LABEL
                 )
                 .addPass()
                 .generate()
-        assertPass(APF_VERSION_6, program, testPacket)
+        apfTestHelpers.assertPass(apfInterpreterVersion, program, testPacket)
     }
 
     @Test
     fun testJumpOneOf() {
-        var program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        var program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addLoadImmediate(R0, 255)
                 .addJumpIfOneOf(R0, setOf(1, 2, 3, 128, 255), DROP_LABEL)
                 .addPass()
                 .generate()
-        assertDrop(APF_VERSION_6, program, testPacket)
+        apfTestHelpers.assertDrop(apfInterpreterVersion, program, testPacket)
 
-        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addLoadImmediate(R0, 254)
                 .addJumpIfOneOf(R0, setOf(1, 2, 3, 128, 255), DROP_LABEL)
                 .addPass()
                 .generate()
-        assertPass(APF_VERSION_6, program, testPacket)
+        apfTestHelpers.assertPass(apfInterpreterVersion, program, testPacket)
 
-        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addLoadImmediate(R0, 254)
                 .addJumpIfNoneOf(R0, setOf(1, 2, 3, 128, 255), DROP_LABEL)
                 .addPass()
                 .generate()
-        assertDrop(APF_VERSION_6, program, testPacket)
+        apfTestHelpers.assertDrop(apfInterpreterVersion, program, testPacket)
 
-        program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
+        program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
                 .addLoadImmediate(R0, 255)
                 .addJumpIfNoneOf(R0, setOf(1, 2, 3, 128, 255), DROP_LABEL)
                 .addPass()
                 .generate()
-        assertPass(APF_VERSION_6, program, testPacket)
+        apfTestHelpers.assertPass(apfInterpreterVersion, program, testPacket)
     }
 
     @Test
     fun testDebugBuffer() {
-        val program = ApfV6Generator(APF_VERSION_6, ramSize, clampSize)
-                .addLoad8(R0, 255)
+        val program = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
+                .addLoad8intoR0(255)
                 .generate()
         val dataRegion = ByteArray(ramSize - program.size) { 0 }
 
-        assertVerdict(APF_VERSION_6, PASS, program, testPacket, dataRegion)
+        apfTestHelpers.assertVerdict(apfInterpreterVersion, 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 testGetApfV6BaseProgramSize() {
+        val gen = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
+        assertEquals(gen.baseProgramSize, gen.generate().size)
+        assertEquals(7, gen.baseProgramSize)
+    }
+
+    @Test
+    fun testGetApfV4BaseProgramSize() {
+        val gen = ApfV4Generator(apfInterpreterVersion, ramSize, clampSize)
+        assertEquals(gen.baseProgramSize, gen.generate().size)
+        assertEquals(0, gen.baseProgramSize)
+    }
+
+    @Test
+    fun testGetApfV6DefaultPacketHandlingSizeOverEstimate() {
+        val gen = ApfV6Generator(apfInterpreterVersion, ramSize, clampSize)
+        gen.addDefaultPacketHandling()
+        val size = gen.programLengthOverEstimate() - gen.baseProgramSize
+        assertEquals(2, size)
+        assertEquals(size, gen.defaultPacketHandlingSizeOverEstimate)
+    }
+
+    @Test
+    fun testGetApfV4DefaultPacketHandlingSizeOverEstimate() {
+        val gen = ApfV4Generator(apfInterpreterVersion, ramSize, clampSize)
+        gen.addDefaultPacketHandling()
+        val size = gen.programLengthOverEstimate() - gen.baseProgramSize
+        assertEquals(25, size)
+        assertEquals(size, gen.defaultPacketHandlingSizeOverEstimate)
+    }
+
     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()
diff --git a/tests/unit/src/android/net/apf/ApfJniUtils.java b/tests/unit/src/android/net/apf/ApfJniUtils.java
index e6a7ad7..85f76b9 100644
--- a/tests/unit/src/android/net/apf/ApfJniUtils.java
+++ b/tests/unit/src/android/net/apf/ApfJniUtils.java
@@ -15,28 +15,40 @@
  */
 package android.net.apf;
 
+import java.util.List;
+
 /**
  * The class contains the helper method for interacting with native apf code.
  */
 public class ApfJniUtils {
-
-    static {
+    static final int APF_INTERPRETER_VERSION_V6 = 6000;
+    static final int APF_INTERPRETER_VERSION_NEXT = 99999999;
+    public ApfJniUtils(int apfInterpreterVersion) {
         // Load up native shared library containing APF interpreter exposed via JNI.
-        System.loadLibrary("networkstacktestsjni");
+        if (apfInterpreterVersion == APF_INTERPRETER_VERSION_V6) {
+            System.loadLibrary("apfjniv6");
+        } else if (apfInterpreterVersion == APF_INTERPRETER_VERSION_NEXT) {
+            System.loadLibrary("apfjninext");
+        } else {
+            throw new IllegalArgumentException(
+                "apfInterpreterVersion must be "
+                    + APF_INTERPRETER_VERSION_V6 + " or "
+                    + APF_INTERPRETER_VERSION_NEXT);
+        }
     }
 
     /**
      * Call the APF interpreter to run {@code program} on {@code packet} with persistent memory
      * segment {@data} pretending the filter was installed {@code filter_age} seconds ago.
      */
-    public static native int apfSimulate(int apfVersion, byte[] program, byte[] packet,
+    public native int apfSimulate(int apfVersion, byte[] program, byte[] packet,
             byte[] data, int filterAge);
 
     /**
      * Compile a tcpdump human-readable filter (e.g. "icmp" or "tcp port 54") into a BPF
      * prorgam and return a human-readable dump of the BPF program identical to "tcpdump -d".
      */
-    public static native String compileToBpf(String filter);
+    public native String compileToBpf(String filter);
 
     /**
      * Open packet capture file {@code pcap_filename} and filter the packets using tcpdump
@@ -44,7 +56,7 @@
      * at the same time using APF program {@code apf_program}.  Return {@code true} if
      * both APF and BPF programs filter out exactly the same packets.
      */
-    public static native boolean compareBpfApf(int apfVersion, String filter,
+    public native boolean compareBpfApf(int apfVersion, String filter,
             String pcapFilename, byte[] apfProgram);
 
     /**
@@ -52,21 +64,21 @@
      * checks whether all the packets are dropped and populates data[] {@code data} with
      * the APF counters.
      */
-    public static native boolean dropsAllPackets(int apfVersion, byte[] program, byte[] data,
+    public native boolean dropsAllPackets(int apfVersion, byte[] program, byte[] data,
             String pcapFilename);
 
     /**
      * Disassemble the Apf program into human-readable text.
      */
-    public static native String[] disassembleApf(byte[] program);
+    public native String[] disassembleApf(byte[] program);
 
     /**
-     * Get the transmitted packet.
+     * Get all transmitted packets.
      */
-    public static native byte[] getTransmittedPacket();
+    public native List<byte[]> getAllTransmittedPackets();
 
     /**
      * Reset the memory region that stored the transmitted packet.
      */
-    public static native void resetTransmittedPacketMemory();
+    public native void resetTransmittedPacketMemory();
 }
diff --git a/tests/unit/src/android/net/apf/ApfMdnsOffloadEngineTest.kt b/tests/unit/src/android/net/apf/ApfMdnsOffloadEngineTest.kt
new file mode 100644
index 0000000..d2841ba
--- /dev/null
+++ b/tests/unit/src/android/net/apf/ApfMdnsOffloadEngineTest.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2025 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.ApfMdnsOffloadEngine.Callback
+import android.net.apf.ApfMdnsUtils.extractOffloadReplyRule
+import android.net.nsd.NsdManager
+import android.net.nsd.OffloadEngine
+import android.net.nsd.OffloadServiceInfo
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.visibleOnHandlerThread
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+private const val TIMEOUT_MS: Long = 1000
+
+/**
+ * Tests for ApfMdnsOffloadEngine.
+ */
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+class ApfMdnsOffloadEngineTest {
+
+    @get:Rule
+    val ignoreRule = DevSdkIgnoreRule()
+
+    private val TAG = ApfMdnsOffloadEngineTest::class.java.simpleName
+
+    private val handlerThread by lazy {
+        HandlerThread("$TAG handler thread").apply { start() }
+    }
+    private val handler by lazy { Handler(handlerThread.looper) }
+
+    private val interfaceName = "test_interface"
+
+    @Mock
+    private lateinit var nsdManager: NsdManager
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @After
+    fun tearDown() {
+        handlerThread.quitSafely()
+        handlerThread.join()
+        Mockito.framework().clearInlineMocks()
+    }
+
+    @Test
+    fun testOffloadEngineRegistration() {
+        val callback = mock(Callback::class.java)
+        val apfOffloadEngine = ApfMdnsOffloadEngine(interfaceName, handler, nsdManager, callback)
+        apfOffloadEngine.registerOffloadEngine()
+        verify(nsdManager).registerOffloadEngine(
+            eq(interfaceName),
+            anyLong(),
+            anyLong(),
+            any(),
+            eq(apfOffloadEngine)
+        )
+        val info1 = OffloadServiceInfo(
+            OffloadServiceInfo.Key("TestServiceName", "_advertisertest._tcp"),
+            listOf(),
+            "Android_test.local",
+            byteArrayOf(0x01, 0x02, 0x03, 0x04),
+            0,
+            OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+        )
+        val info2 = OffloadServiceInfo(
+            OffloadServiceInfo.Key("TestServiceName2", "_advertisertest._tcp"),
+            listOf(),
+            "Android_test.local",
+            byteArrayOf(0x01, 0x02, 0x03, 0x04),
+            0,
+            OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+        )
+        val updatedInfo1 = OffloadServiceInfo(
+            OffloadServiceInfo.Key("TestServiceName", "_advertisertest._tcp"),
+            listOf(),
+            "Android_test.local",
+            byteArrayOf(),
+            0,
+            OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+        )
+        visibleOnHandlerThread(handler) { apfOffloadEngine.onOffloadServiceUpdated(info1) }
+        verify(callback).onOffloadRulesUpdated(eq(extractOffloadReplyRule(listOf(info1))))
+        visibleOnHandlerThread(handler) { apfOffloadEngine.onOffloadServiceUpdated(info2) }
+        verify(callback).onOffloadRulesUpdated(eq(extractOffloadReplyRule(listOf(info1, info2))))
+        visibleOnHandlerThread(handler) { apfOffloadEngine.onOffloadServiceUpdated(updatedInfo1) }
+        verify(callback).onOffloadRulesUpdated(
+            eq(extractOffloadReplyRule(listOf(info2, updatedInfo1)))
+        )
+        visibleOnHandlerThread(handler) { apfOffloadEngine.onOffloadServiceRemoved(updatedInfo1) }
+        verify(callback).onOffloadRulesUpdated(eq(extractOffloadReplyRule(listOf(info2))))
+
+        visibleOnHandlerThread(handler) { apfOffloadEngine.unregisterOffloadEngine() }
+        verify(nsdManager).unregisterOffloadEngine(eq(apfOffloadEngine))
+    }
+
+    @Test
+    fun testCorruptedOffloadServiceInfoUpdateNotTriggerUpdate() {
+        val callback = mock(Callback::class.java)
+        val apfOffloadEngine = ApfMdnsOffloadEngine(interfaceName, handler, nsdManager, callback)
+        apfOffloadEngine.registerOffloadEngine()
+        val corruptedOffloadInfo = OffloadServiceInfo(
+            OffloadServiceInfo.Key("gambit", "_${"a".repeat(63)}._tcp"),
+            listOf(),
+            "Android_f47ac10b58cc4b88bc3f5e7a81e59872.local",
+            byteArrayOf(0x01, 0x02, 0x03, 0x04),
+            0,
+            OffloadEngine.OFFLOAD_TYPE_REPLY.toLong()
+        )
+        visibleOnHandlerThread(handler) {
+            apfOffloadEngine.onOffloadServiceUpdated(corruptedOffloadInfo)
+        }
+        verify(callback, never()).onOffloadRulesUpdated(any())
+    }
+}
diff --git a/tests/unit/src/android/net/apf/ApfMdnsUtilsTest.kt b/tests/unit/src/android/net/apf/ApfMdnsUtilsTest.kt
index edf4f43..e8bbac1 100644
--- a/tests/unit/src/android/net/apf/ApfMdnsUtilsTest.kt
+++ b/tests/unit/src/android/net/apf/ApfMdnsUtilsTest.kt
@@ -32,7 +32,6 @@
 import java.io.IOException
 import kotlin.test.assertContentEquals
 import kotlin.test.assertFailsWith
-import kotlin.test.assertTrue
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -49,7 +48,7 @@
 
     private val testServiceName1 = "NsdChat"
     private val testServiceName2 = "NsdCall"
-    private val testServiceType = "_http._tcp.local"
+    private val testServiceType = "_http._tcp"
     private val testSubType = "tsub"
     private val testHostName = "Android.local"
     private val testRawPacket1 = byteArrayOf(1, 2, 3, 4, 5)
@@ -91,17 +90,10 @@
             0, 0).map { it.toByte() }.toByteArray()
 
     @Test
-    fun testExtractOffloadReplyRule_noPriorityReturnsEmptySet() {
-        val info = createOffloadServiceInfo(Int.MAX_VALUE)
-        val rules = extractOffloadReplyRule(listOf(info))
-        assertTrue(rules.isEmpty())
-    }
-
-    @Test
-    fun testExtractOffloadReplyRule_extractRulesWithValidPriority() {
+    fun testExtractOffloadReplyRule_extractRules() {
         val info1 = createOffloadServiceInfo(10)
         val info2 = createOffloadServiceInfo(
-                11,
+                Integer.MAX_VALUE,
                 testServiceName2,
                 listOf("a", "b", "c", "d"),
                 testRawPacket2
@@ -109,22 +101,36 @@
         val rules = extractOffloadReplyRule(listOf(info2, info1))
         val expectedResult = listOf(
                 MdnsOffloadRule(
+                        "${info1.key.serviceName}.${info1.key.serviceType}",
                         listOf(
-                                MdnsOffloadRule.Matcher(encodedServiceType, TYPE_PTR),
-                                MdnsOffloadRule.Matcher(encodedServiceTypeWithSub1, TYPE_PTR),
-                                MdnsOffloadRule.Matcher(encodedFullServiceName1, TYPE_SRV),
-                                MdnsOffloadRule.Matcher(encodedFullServiceName1, TYPE_TXT),
-                                MdnsOffloadRule.Matcher(encodedTestHostName, TYPE_A),
-                                MdnsOffloadRule.Matcher(encodedTestHostName, TYPE_AAAA),
+                                MdnsOffloadRule.Matcher(encodedServiceType, intArrayOf(TYPE_PTR)),
+                                MdnsOffloadRule.Matcher(
+                                    encodedServiceTypeWithSub1,
+                                    intArrayOf(TYPE_PTR)
+                                ),
+                                MdnsOffloadRule.Matcher(
+                                    encodedFullServiceName1,
+                                    intArrayOf(TYPE_SRV, TYPE_TXT)
+                                ),
+                                MdnsOffloadRule.Matcher(
+                                    encodedTestHostName,
+                                    intArrayOf(TYPE_A, TYPE_AAAA)
+                                ),
 
                         ),
                         testRawPacket1,
                 ),
                 MdnsOffloadRule(
+                        "${info2.key.serviceName}.${info2.key.serviceType}",
                         listOf(
-                                MdnsOffloadRule.Matcher(encodedServiceTypeWithWildCard, TYPE_PTR),
-                                MdnsOffloadRule.Matcher(encodedFullServiceName2, TYPE_SRV),
-                                MdnsOffloadRule.Matcher(encodedFullServiceName2, TYPE_TXT),
+                                MdnsOffloadRule.Matcher(
+                                    encodedServiceTypeWithWildCard,
+                                    intArrayOf(TYPE_PTR)
+                                ),
+                                MdnsOffloadRule.Matcher(
+                                    encodedFullServiceName2,
+                                    intArrayOf(TYPE_SRV, TYPE_TXT)
+                                ),
 
                         ),
                         null,
diff --git a/tests/unit/src/android/net/apf/ApfStandaloneTest.kt b/tests/unit/src/android/net/apf/ApfStandaloneTest.kt
index 2a918f8..21dc8fb 100644
--- a/tests/unit/src/android/net/apf/ApfStandaloneTest.kt
+++ b/tests/unit/src/android/net/apf/ApfStandaloneTest.kt
@@ -38,8 +38,10 @@
 import com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_SOLICITATION
 import com.android.testutils.DevSdkIgnoreRunner
 import kotlin.test.assertEquals
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
 
 /**
  * This class generate ApfStandaloneTest programs for side-loading into firmware without needing the
@@ -53,9 +55,20 @@
 @SmallTest
 class ApfStandaloneTest {
 
+    // Indicates which apfInterpreter to load.
+    @Parameterized.Parameter(0)
+    @JvmField
+    var apfInterpreterVersion: Int = ApfJniUtils.APF_INTERPRETER_VERSION_NEXT
+
     private val etherTypeDenyList = listOf(0x88A2, 0x88A4, 0x88B8, 0x88CD, 0x88E1, 0x88E3)
     private val ramSize = 1024
     private val clampSize = 1024
+    private lateinit var apfTestHelpers: ApfTestHelpers
+
+    @Before
+    fun setUp() {
+        apfTestHelpers = ApfTestHelpers(apfInterpreterVersion)
+    }
 
     fun runApfTest(isSuspendMode: Boolean) {
         val program = generateApfV4Program(isSuspendMode)
@@ -78,7 +91,7 @@
         val packetBadEtherType =
                 HexDump.hexStringToByteArray("ffffffffffff047bcb463fb588a201")
         val dataRegion = ByteArray(Counter.totalSize()) { 0 }
-        ApfTestHelpers.assertVerdict(
+        apfTestHelpers.assertVerdict(
             APF_VERSION_4,
             ApfTestHelpers.DROP,
             program,
@@ -154,7 +167,7 @@
             c0a801013204c0a80164ff
         """.replace("\\s+".toRegex(), "").trim()
         val dhcpRequestPkt = HexDump.hexStringToByteArray(dhcpRequestPktRawBytes)
-        ApfTestHelpers.assertVerdict(
+        apfTestHelpers.assertVerdict(
             APF_VERSION_4,
             ApfTestHelpers.DROP,
             program,
@@ -195,7 +208,7 @@
             0000000000000000000000028500c81d00000000
         """.replace("\\s+".toRegex(), "").trim()
         val rsPkt = HexDump.hexStringToByteArray(rsPktRawBytes)
-        ApfTestHelpers.assertVerdict(APF_VERSION_4, ApfTestHelpers.DROP, program, rsPkt, dataRegion)
+        apfTestHelpers.assertVerdict(APF_VERSION_4, ApfTestHelpers.DROP, program, rsPkt, dataRegion)
         assertEquals(mapOf<Counter, Long>(
                 Counter.TOTAL_PACKETS to 3,
                 Counter.DROPPED_RS to 1,
@@ -238,7 +251,7 @@
                 00000000
             """.replace("\\s+".toRegex(), "").trim()
             val pingRequestPkt = HexDump.hexStringToByteArray(pingRequestPktRawBytes)
-            ApfTestHelpers.assertVerdict(
+            apfTestHelpers.assertVerdict(
                 APF_VERSION_4,
                 ApfTestHelpers.DROP,
                 program,
@@ -266,12 +279,12 @@
     }
 
     private fun generateApfV4Program(isDeviceIdle: Boolean): ByteArray {
-        val countAndPassLabel = "countAndPass"
-        val countAndDropLabel = "countAndDrop"
-        val endOfDhcpFilter = "endOfDhcpFilter"
-        val endOfRsFilter = "endOfRsFiler"
-        val endOfPingFilter = "endOfPingFilter"
         val gen = ApfV4Generator(APF_VERSION_4, ramSize, clampSize)
+        val countAndPassLabel = gen.uniqueLabel
+        val countAndDropLabel = gen.uniqueLabel
+        val endOfDhcpFilter = gen.uniqueLabel
+        val endOfRsFilter = gen.uniqueLabel
+        val endOfPingFilter = gen.uniqueLabel
 
         maybeSetupCounter(gen, Counter.TOTAL_PACKETS)
         gen.addLoadData(R0, 0)
@@ -287,7 +300,7 @@
         gen.addStoreData(R0, 0)
 
         // ethtype filter
-        gen.addLoad16(R0, ETHER_TYPE_OFFSET)
+        gen.addLoad16intoR0(ETHER_TYPE_OFFSET)
         maybeSetupCounter(gen, Counter.DROPPED_ETHERTYPE_DENYLISTED)
         for (p in etherTypeDenyList) {
             gen.addJumpIfR0Equals(p.toLong(), countAndDropLabel)
@@ -296,22 +309,22 @@
         // dhcp request filters
 
         // Check IPv4
-        gen.addLoad16(R0, ETHER_TYPE_OFFSET)
+        gen.addLoad16intoR0(ETHER_TYPE_OFFSET)
         gen.addJumpIfR0NotEquals(ETH_P_IP.toLong(), endOfDhcpFilter)
 
         // Pass DHCP addressed to us.
         // Check src is IP is 0.0.0.0
-        gen.addLoad32(R0, IPV4_SRC_ADDR_OFFSET)
+        gen.addLoad32intoR0(IPV4_SRC_ADDR_OFFSET)
         gen.addJumpIfR0NotEquals(0, endOfDhcpFilter)
         // Check dst ip is 255.255.255.255
-        gen.addLoad32(R0, IPV4_DEST_ADDR_OFFSET)
+        gen.addLoad32intoR0(IPV4_DEST_ADDR_OFFSET)
         gen.addJumpIfR0NotEquals(IPV4_BROADCAST_ADDRESS.toLong(), endOfDhcpFilter)
         // Check it's UDP.
-        gen.addLoad8(R0, IPV4_PROTOCOL_OFFSET)
+        gen.addLoad8intoR0(IPV4_PROTOCOL_OFFSET)
         gen.addJumpIfR0NotEquals(OsConstants.IPPROTO_UDP.toLong(), endOfDhcpFilter)
         // Check it's addressed to DHCP client port.
         gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE)
-        gen.addLoad16Indexed(R0, TCP_UDP_DESTINATION_PORT_OFFSET)
+        gen.addLoad16R1IndexedIntoR0(TCP_UDP_DESTINATION_PORT_OFFSET)
         gen.addJumpIfR0NotEquals(DHCP_SERVER_PORT.toLong(), endOfDhcpFilter)
         // drop dhcp the discovery and request
         maybeSetupCounter(gen, Counter.DROPPED_DHCP_REQUEST_DISCOVERY)
@@ -322,13 +335,13 @@
         // rs filters
 
         // check IPv6
-        gen.addLoad16(R0, ETHER_TYPE_OFFSET)
+        gen.addLoad16intoR0(ETHER_TYPE_OFFSET)
         gen.addJumpIfR0NotEquals(OsConstants.ETH_P_IPV6.toLong(), endOfRsFilter)
         // check ICMP6 packet
-        gen.addLoad8(R0, IPV6_NEXT_HEADER_OFFSET)
+        gen.addLoad8intoR0(IPV6_NEXT_HEADER_OFFSET)
         gen.addJumpIfR0NotEquals(IPPROTO_ICMPV6.toLong(), endOfRsFilter)
         // check type it is RS
-        gen.addLoad8(R0, ICMP6_TYPE_OFFSET)
+        gen.addLoad8intoR0(ICMP6_TYPE_OFFSET)
         gen.addJumpIfR0NotEquals(ICMPV6_ROUTER_SOLICITATION.toLong(), endOfRsFilter)
         // drop rs packet
         maybeSetupCounter(gen, Counter.DROPPED_RS)
@@ -340,14 +353,14 @@
             // ping filter
 
             // Check IPv4
-            gen.addLoad16(R0, ETHER_TYPE_OFFSET)
+            gen.addLoad16intoR0(ETHER_TYPE_OFFSET)
             gen.addJumpIfR0NotEquals(ETH_P_IP.toLong(), endOfPingFilter)
             // Check it's ICMP.
-            gen.addLoad8(R0, IPV4_PROTOCOL_OFFSET)
+            gen.addLoad8intoR0(IPV4_PROTOCOL_OFFSET)
             gen.addJumpIfR0NotEquals(OsConstants.IPPROTO_ICMP.toLong(), endOfPingFilter)
             // Check if it is echo request
             gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE)
-            gen.addLoad8Indexed(R0, ETH_HEADER_LEN)
+            gen.addLoad8R1IndexedIntoR0(ETH_HEADER_LEN)
             gen.addJumpIfR0NotEquals(8, endOfPingFilter)
             // drop ping request
             maybeSetupCounter(gen, Counter.DROPPED_ICMP4_ECHO_REQUEST)
@@ -438,5 +451,13 @@
 
     companion object {
         const val TAG = "ApfStandaloneTest"
+        @Parameterized.Parameters
+        @JvmStatic
+        fun data(): Iterable<Any?> {
+            return mutableListOf<Int?>(
+                ApfJniUtils.APF_INTERPRETER_VERSION_V6,
+                ApfJniUtils.APF_INTERPRETER_VERSION_NEXT
+            )
+        }
     }
 }
diff --git a/tests/unit/src/android/net/apf/ApfTest.java b/tests/unit/src/android/net/apf/ApfTest.java
index 9a4a224..64f3b11 100644
--- a/tests/unit/src/android/net/apf/ApfTest.java
+++ b/tests/unit/src/android/net/apf/ApfTest.java
@@ -18,7 +18,6 @@
 
 import static android.net.apf.ApfCounterTracker.Counter.getCounterEnumFromOffset;
 import static android.net.apf.ApfTestHelpers.TIMEOUT_MS;
-import static android.net.apf.ApfTestHelpers.consumeInstalledProgram;
 import static android.net.apf.ApfTestHelpers.DROP;
 import static android.net.apf.ApfTestHelpers.MIN_PKT_SIZE;
 import static android.net.apf.ApfTestHelpers.PASS;
@@ -31,9 +30,6 @@
 import static android.net.apf.BaseApfGenerator.PASS_LABEL;
 import static android.net.apf.BaseApfGenerator.Register.R0;
 import static android.net.apf.BaseApfGenerator.Register.R1;
-import static android.net.apf.ApfJniUtils.compareBpfApf;
-import static android.net.apf.ApfJniUtils.compileToBpf;
-import static android.net.apf.ApfJniUtils.dropsAllPackets;
 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_UNIX;
@@ -57,7 +53,6 @@
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.times;
@@ -74,7 +69,6 @@
 import android.net.apf.ApfCounterTracker.Counter;
 import android.net.apf.ApfFilter.ApfConfiguration;
 import android.net.apf.BaseApfGenerator.IllegalInstructionException;
-import android.net.ip.IpClient;
 import android.net.metrics.IpConnectivityLog;
 import android.os.Build;
 import android.os.Handler;
@@ -177,13 +171,14 @@
     @Mock private NetworkQuirkMetrics mNetworkQuirkMetrics;
     @Mock private ApfSessionInfoMetrics mApfSessionInfoMetrics;
     @Mock private IpClientRaInfoMetrics mIpClientRaInfoMetrics;
-    @Mock private IpClient.IpClientCallbacksWrapper mIpClientCb;
+    @Mock private ApfFilter.IApfController mApfController;
     @GuardedBy("mApfFilterCreated")
-    private final ArrayList<AndroidPacketFilter> mApfFilterCreated = new ArrayList<>();
+    private final ArrayList<ApfFilter> mApfFilterCreated = new ArrayList<>();
     private FileDescriptor mWriteSocket;
     private HandlerThread mHandlerThread;
     private Handler mHandler;
     private long mCurrentTimeMs;
+    private ApfTestHelpers mApfTestHelpers;
 
     @Before
     public void setUp() throws Exception {
@@ -197,7 +192,7 @@
         doReturn(readSocket).when(mDependencies).createPacketReaderSocket(anyInt());
         mCurrentTimeMs = SystemClock.elapsedRealtime();
         doReturn(mCurrentTimeMs).when(mDependencies).elapsedRealtime();
-        doReturn(true).when(mIpClientCb).installPacketFilter(any());
+        doReturn(true).when(mApfController).installPacketFilter(any(), any());
         doAnswer((invocation) -> {
             synchronized (mApfFilterCreated) {
                 mApfFilterCreated.add(invocation.getArgument(0));
@@ -207,12 +202,13 @@
         mHandlerThread = new HandlerThread("ApfTestThread");
         mHandlerThread.start();
         mHandler = new Handler(mHandlerThread.getLooper());
+        mApfTestHelpers = new ApfTestHelpers(ApfJniUtils.APF_INTERPRETER_VERSION_V6);
     }
 
     private void shutdownApfFilters() throws Exception {
         ConcurrentUtils.quitResources(THREAD_QUIT_MAX_RETRY_COUNT, () -> {
             synchronized (mApfFilterCreated) {
-                final ArrayList<AndroidPacketFilter> ret =
+                final ArrayList<ApfFilter> ret =
                         new ArrayList<>(mApfFilterCreated);
                 mApfFilterCreated.clear();
                 return ret;
@@ -279,58 +275,58 @@
     }
 
     private void assertPass(ApfV4Generator gen) throws ApfV4Generator.IllegalInstructionException {
-        ApfTestHelpers.assertPass(mApfVersion, gen);
+        mApfTestHelpers.assertPass(mApfVersion, gen);
     }
 
     private void assertDrop(ApfV4Generator gen) throws ApfV4Generator.IllegalInstructionException {
-        ApfTestHelpers.assertDrop(mApfVersion, gen);
+        mApfTestHelpers.assertDrop(mApfVersion, gen);
     }
 
     private void assertPass(byte[] program, byte[] packet) {
-        ApfTestHelpers.assertPass(mApfVersion, program, packet);
+        mApfTestHelpers.assertPass(mApfVersion, program, packet);
     }
 
     private void assertDrop(byte[] program, byte[] packet) {
-        ApfTestHelpers.assertDrop(mApfVersion, program, packet);
+        mApfTestHelpers.assertDrop(mApfVersion, program, packet);
     }
 
     private void assertPass(byte[] program, byte[] packet, int filterAge) {
-        ApfTestHelpers.assertPass(mApfVersion, program, packet, filterAge);
+        mApfTestHelpers.assertPass(mApfVersion, program, packet, filterAge);
     }
 
     private void assertDrop(byte[] program, byte[] packet, int filterAge) {
-        ApfTestHelpers.assertDrop(mApfVersion, program, packet, filterAge);
+        mApfTestHelpers.assertDrop(mApfVersion, program, packet, filterAge);
     }
 
     private void assertPass(ApfV4Generator gen, byte[] packet, int filterAge)
             throws ApfV4Generator.IllegalInstructionException {
-        ApfTestHelpers.assertPass(mApfVersion, gen, packet, filterAge);
+        mApfTestHelpers.assertPass(mApfVersion, gen, packet, filterAge);
     }
 
     private void assertDrop(ApfV4Generator gen, byte[] packet, int filterAge)
             throws ApfV4Generator.IllegalInstructionException {
-        ApfTestHelpers.assertDrop(mApfVersion, gen, packet, filterAge);
+        mApfTestHelpers.assertDrop(mApfVersion, gen, packet, filterAge);
     }
 
     private void assertDataMemoryContents(int expected, byte[] program, byte[] packet,
             byte[] data, byte[] expectedData) throws Exception {
-        ApfTestHelpers.assertDataMemoryContents(mApfVersion, expected, program, packet, data,
+        mApfTestHelpers.assertDataMemoryContents(mApfVersion, expected, program, packet, data,
                 expectedData, false /* ignoreInterpreterVersion */);
     }
 
     private void assertDataMemoryContentsIgnoreVersion(int expected, byte[] program,
             byte[] packet, byte[] data, byte[] expectedData) throws Exception {
-        ApfTestHelpers.assertDataMemoryContents(mApfVersion, expected, program, packet, data,
+        mApfTestHelpers.assertDataMemoryContents(mApfVersion, expected, program, packet, data,
                 expectedData, true /* ignoreInterpreterVersion */);
     }
 
     private void assertVerdict(String msg, int expected, byte[] program,
             byte[] packet, int filterAge) {
-        ApfTestHelpers.assertVerdict(mApfVersion, msg, expected, program, packet, filterAge);
+        mApfTestHelpers.assertVerdict(mApfVersion, msg, expected, program, packet, filterAge);
     }
 
     private void assertVerdict(int expected, byte[] program, byte[] packet) {
-        ApfTestHelpers.assertVerdict(mApfVersion, expected, program, packet);
+        mApfTestHelpers.assertVerdict(mApfVersion, expected, program, packet);
     }
 
     /**
@@ -537,53 +533,53 @@
 
         // Test byte load.
         gen = new ApfV4Generator(APF_VERSION_2, mRamSize, mClampSize);
-        gen.addLoad8(R0, 1);
+        gen.addLoad8intoR0(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, mRamSize, mClampSize);
-        gen.addLoad8(R0, 16);
+        gen.addLoad8intoR0(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, mRamSize, mClampSize);
-        gen.addLoad16(R0, 1);
+        gen.addLoad16intoR0(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, mRamSize, mClampSize);
-        gen.addLoad32(R0, 1);
+        gen.addLoad32intoR0(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, mRamSize, mClampSize);
         gen.addLoadImmediate(R1, 1);
-        gen.addLoad8Indexed(R0, 0);
+        gen.addLoad8R1IndexedIntoR0(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, mRamSize, mClampSize);
         gen.addLoadImmediate(R1, 8);
-        gen.addLoad8Indexed(R0, 8);
+        gen.addLoad8R1IndexedIntoR0(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, mRamSize, mClampSize);
         gen.addLoadImmediate(R1, 1);
-        gen.addLoad16Indexed(R0, 0);
+        gen.addLoad16R1IndexedIntoR0(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, mRamSize, mClampSize);
         gen.addLoadImmediate(R1, 1);
-        gen.addLoad32Indexed(R0, 0);
+        gen.addLoad32R1IndexedIntoR0(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);
 
@@ -995,24 +991,6 @@
         assertDataMemoryContents(PASS, gen.generate(), packet, data, expected_data);
     }
 
-    /**
-     * Generate some BPF programs, translate them to APF, then run APF and BPF programs
-     * over packet traces and verify both programs filter out the same packets.
-     */
-    @Test
-    public void testApfAgainstBpf() throws Exception {
-        String[] tcpdump_filters = new String[]{ "udp", "tcp", "icmp", "icmp6", "udp port 53",
-                "arp", "dst 239.255.255.250", "arp or tcp or udp port 53", "net 192.168.1.0/24",
-                "arp or icmp6 or portrange 53-54", "portrange 53-54 or portrange 100-50000",
-                "tcp[tcpflags] & (tcp-ack|tcp-fin) != 0 and (ip[2:2] > 57 or icmp)" };
-        String pcap_filename = stageFile(R.raw.apf);
-        for (String tcpdump_filter : tcpdump_filters) {
-            byte[] apf_program = Bpf2Apf.convert(compileToBpf(tcpdump_filter));
-            assertTrue("Failed to match for filter: " + tcpdump_filter,
-                    compareBpfApf(mApfVersion, tcpdump_filter, pcap_filename, apf_program));
-        }
-    }
-
     private void pretendPacketReceived(byte[] packet)
             throws InterruptedIOException, ErrnoException {
         Os.write(mWriteSocket, packet, 0, packet.length);
@@ -1022,7 +1000,7 @@
         AtomicReference<ApfFilter> apfFilter = new AtomicReference<>();
         mHandler.post(() ->
                 apfFilter.set(new ApfFilter(mHandler, mContext, config, TEST_PARAMS,
-                        mIpClientCb, mNetworkQuirkMetrics, mDependencies)));
+                        mApfController, mNetworkQuirkMetrics, mDependencies)));
         HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS);
         return apfFilter.get();
     }
@@ -1045,13 +1023,15 @@
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
         final ApfFilter apfFilter = getApfFilter(config);
-        consumeInstalledProgram(mIpClientCb, 2 /* installCnt */);
+        mApfTestHelpers.consumeInstalledProgram(mApfController, 2 /* installCnt */);
         apfFilter.setLinkProperties(lp);
-        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        byte[] program =
+            mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         byte[] data = new byte[Counter.totalSize()];
         final boolean result;
 
-        result = dropsAllPackets(mApfVersion, program, data, pcapFilename);
+        result = mApfTestHelpers.dropsAllPackets(
+            mApfVersion, program, data, pcapFilename);
         Log.i(TAG, "testApfFilterPcapFile(): Data counters: " + HexDump.toHexString(data, false));
 
         assertTrue("Failed to drop all packets by filter. \nAPF counters:" +
@@ -1193,10 +1173,11 @@
         ApfConfiguration config = getDefaultConfig();
         config.multicastFilter = DROP_MULTICAST;
         final ApfFilter apfFilter = getApfFilter(config);
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         apfFilter.setLinkProperties(lp);
 
-        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        byte[] program =
+            mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
         if (SdkLevel.isAtLeastV()) {
@@ -1248,7 +1229,8 @@
     public void testApfFilterIPv6() throws Exception {
         ApfConfiguration config = getDefaultConfig();
         ApfFilter apfFilter = getApfFilter(config);
-        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        byte[] program =
+            mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // Verify empty IPv6 packet is passed
         ByteBuffer packet = makeIpv6Packet(IPPROTO_UDP);
@@ -1475,178 +1457,6 @@
         assertEquals(count, gen.generate().length);
     }
 
-    private ApfV4Generator generateDnsFilter(boolean ipv6, String... labels) throws Exception {
-        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;
-    }
-
-    private void doTestDnsParsing(boolean expectPass, boolean ipv6, String filterName,
-            byte[] pkt) throws Exception {
-        final String[] labels = filterName.split(/*regex=*/ "[.]");
-        ApfV4Generator gen = generateDnsFilter(ipv6, labels);
-
-        // Hack to prevent the APF instruction limit triggering.
-        for (int i = 0; i < 500; i++) {
-            gen.addNop();
-        }
-
-        byte[] program = gen.generate();
-        Log.d(TAG, "prog_len=" + program.length);
-        if (expectPass) {
-            assertPass(program, pkt, 0);
-        } else {
-            assertDrop(program, pkt, 0);
-        }
-    }
-
-    private void doTestDnsParsing(boolean expectPass, boolean ipv6, String filterName,
-            String... packetNames) throws Exception {
-        final byte[] pkt = ipv6 ? makeMdnsV6Packet(packetNames) : makeMdnsV4Packet(packetNames);
-        doTestDnsParsing(expectPass, ipv6, filterName, pkt);
-    }
-
-    @Test
-    public void testDnsParsing() throws Exception {
-        final boolean ipv4 = false, ipv6 = true;
-
-        // Packets with one question.
-        // Names don't start with _ because DnsPacket thinks such names are invalid.
-        doTestDnsParsing(true, ipv6, "googlecast.tcp.local", "googlecast.tcp.local");
-        doTestDnsParsing(true, ipv4, "googlecast.tcp.local", "googlecast.tcp.local");
-        doTestDnsParsing(false, ipv6, "googlecast.tcp.lozal", "googlecast.tcp.local");
-        doTestDnsParsing(false, ipv4, "googlecast.tcp.lozal", "googlecast.tcp.local");
-        doTestDnsParsing(false, ipv6, "googlecast.udp.local", "googlecast.tcp.local");
-        doTestDnsParsing(false, ipv4, "googlecast.udp.local", "googlecast.tcp.local");
-
-        // Packets with multiple questions that can't be compressed. Not realistic for MDNS since
-        // everything ends in .local, but useful to ensure only the non-compression code is tested.
-        doTestDnsParsing(true, ipv6, "googlecast.tcp.local",
-                "googlecast.tcp.local", "developer.android.com");
-        doTestDnsParsing(true, ipv4, "googlecast.tcp.local",
-                "developer.android.com", "googlecast.tcp.local");
-        doTestDnsParsing(false, ipv4, "googlecast.tcp.local",
-                "developer.android.com", "googlecast.tcp.invalid");
-        doTestDnsParsing(true, ipv6, "googlecast.tcp.local",
-                "developer.android.com", "www.google.co.jp", "googlecast.tcp.local");
-        doTestDnsParsing(false, ipv4, "veryverylongservicename.tcp.local",
-                "www.google.co.jp", "veryverylongservicename.tcp.invalid");
-        doTestDnsParsing(true, ipv6, "googlecast.tcp.local",
-                "www.google.co.jp", "googlecast.tcp.local", "developer.android.com");
-
-        // Name with duplicate labels.
-        doTestDnsParsing(true, ipv6, "local.tcp.local", "local.tcp.local");
-
-        final byte[] pkt = makeMdnsCompressedV6Packet();
-        doTestDnsParsing(true, ipv6, "googlecast.tcp.local", pkt);
-        doTestDnsParsing(true, ipv6, "matter.tcp.local", pkt);
-        doTestDnsParsing(true, ipv6, "myservice.tcp.local", pkt);
-        doTestDnsParsing(false, ipv6, "otherservice.tcp.local", pkt);
-    }
-
-    private void doTestDnsParsingProgramLength(int expectedLength,
-            String filterName) throws Exception {
-        final String[] labels = filterName.split(/*regex=*/ "[.]");
-
-        ApfV4Generator gen = generateDnsFilter(/*ipv6=*/ true, labels);
-        assertEquals("Program for " + filterName + " had unexpected length:",
-                expectedLength, gen.generate().length);
-    }
-
-    /**
-     * Rough metric of code size. Checks how large the generated filter is in various scenarios.
-     * Helps ensure any changes to the code do not substantially increase APF code size.
-     */
-    @Test
-    public void testDnsParsingProgramLength() throws Exception {
-        doTestDnsParsingProgramLength(237, "MyDevice.local");
-        doTestDnsParsingProgramLength(285, "_googlecast.tcp.local");
-        doTestDnsParsingProgramLength(291, "_googlecast12345.tcp.local");
-        doTestDnsParsingProgramLength(244, "_googlecastZtcp.local");
-        doTestDnsParsingProgramLength(249, "_googlecastZtcp12345.local");
-    }
-
-    private void doTestDnsParsingNecessaryOverhead(int expectedNecessaryOverhead,
-            String filterName, byte[] pkt, String description) throws Exception {
-        final String[] labels = filterName.split(/*regex=*/ "[.]");
-
-        // Check that the generated code, when the program contains the specified number of extra
-        // bytes, is capable of dropping the packet.
-        ApfV4Generator gen = generateDnsFilter(/*ipv6=*/ true, labels);
-        for (int i = 0; i < expectedNecessaryOverhead; i++) {
-            gen.addNop();
-        }
-        final byte[] programWithJustEnoughOverhead = gen.generate();
-        assertVerdict(
-                "Overhead too low: filter for " + filterName + " with " + expectedNecessaryOverhead
-                        + " extra instructions unexpectedly passed " + description,
-                DROP, programWithJustEnoughOverhead, pkt, 0);
-
-        if (expectedNecessaryOverhead == 0) return;
-
-        // Check that the generated code, without the specified number of extra program bytes,
-        // cannot correctly drop the packet because it hits the interpreter instruction limit.
-        gen = generateDnsFilter(/*ipv6=*/ true, labels);
-        for (int i = 0; i < expectedNecessaryOverhead - 1; i++) {
-            gen.addNop();
-        }
-        final byte[] programWithNotEnoughOverhead = gen.generate();
-
-        assertVerdict(
-                "Overhead too high: filter for " + filterName + " with " + expectedNecessaryOverhead
-                        + " extra instructions unexpectedly dropped " + description,
-                PASS, programWithNotEnoughOverhead, pkt, 0);
-    }
-
-    private void doTestDnsParsingNecessaryOverhead(int expectedNecessaryOverhead,
-            String filterName, String... packetNames) throws Exception {
-        doTestDnsParsingNecessaryOverhead(expectedNecessaryOverhead, filterName,
-                makeMdnsV6Packet(packetNames),
-                "IPv6 MDNS packet containing: " + Arrays.toString(packetNames));
-    }
-
-    /**
-     * Rough metric of filter efficiency. Because the filter uses backwards jumps, on complex
-     * packets it will not finish running before the interpreter hits the maximum number of allowed
-     * instructions (== number of bytes in the program) and unconditionally accepts the packet.
-     * This test checks much extra code the program must contain in order for the generated filter
-     * to successfully drop the packet. It helps ensure any changes to the code do not reduce the
-     * complexity of packets that the APF code can drop.
-     */
-    @Test
-    public void testDnsParsingNecessaryOverhead() throws Exception {
-        // Simple packets can be parsed with zero extra code.
-        doTestDnsParsingNecessaryOverhead(0, "googlecast.tcp.local",
-                "matter.tcp.local", "developer.android.com");
-
-        doTestDnsParsingNecessaryOverhead(0, "googlecast.tcp.local",
-                "developer.android.com", "matter.tcp.local");
-
-        doTestDnsParsingNecessaryOverhead(0, "googlecast.tcp.local",
-                "developer.android.com", "matter.tcp.local", "www.google.co.jp");
-
-        doTestDnsParsingNecessaryOverhead(0, "googlecast.tcp.local",
-                "developer.android.com", "matter.tcp.local", "www.google.co.jp",
-                "example.org");
-
-        // More complicated packets cause more instructions to be run and can only be dropped if
-        // the program contains lots of extra code.
-        doTestDnsParsingNecessaryOverhead(57, "googlecast.tcp.local",
-                "developer.android.com", "matter.tcp.local", "www.google.co.jp",
-                "example.org", "otherexample.net");
-
-        doTestDnsParsingNecessaryOverhead(115, "googlecast.tcp.local",
-                "developer.android.com", "matter.tcp.local", "www.google.co.jp",
-                "example.org", "otherexample.net", "docs.new");
-
-        doTestDnsParsingNecessaryOverhead(0, "foo.tcp.local",
-                makeMdnsCompressedV6Packet(), "compressed packet");
-
-        doTestDnsParsingNecessaryOverhead(235, "foo.tcp.local",
-                makeMdnsCompressedV6PacketWithManyNames(), "compressed packet with many names");
-    }
-
     @Test
     public void testApfFilterMulticast() throws Exception {
         final byte[] unicastIpv4Addr   = {(byte)192,0,2,63};
@@ -1661,10 +1471,11 @@
         ApfConfiguration config = getDefaultConfig();
         config.ieee802_3Filter = DROP_802_3_FRAMES;
         final ApfFilter apfFilter = getApfFilter(config);
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         apfFilter.setLinkProperties(lp);
 
-        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        byte[] program =
+            mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // Construct IPv4 and IPv6 multicast packets.
         ByteBuffer mcastv4packet = makeIpv4Packet(IPPROTO_UDP);
@@ -1699,7 +1510,7 @@
 
         // Turn on multicast filter and verify it works
         apfFilter.setMulticastFilter(true);
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        program = mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         assertDrop(program, mcastv4packet.array());
         assertDrop(program, mcastv6packet.array());
         assertDrop(program, bcastv4packet1.array());
@@ -1708,7 +1519,7 @@
 
         // Turn off multicast filter and verify it's off
         apfFilter.setMulticastFilter(false);
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        program = mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         assertPass(program, mcastv4packet.array());
         assertPass(program, mcastv6packet.array());
         assertPass(program, bcastv4packet1.array());
@@ -1718,11 +1529,11 @@
         // Verify it can be initialized to on
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
-        clearInvocations(mIpClientCb);
+        clearInvocations(mApfController);
         final ApfFilter apfFilter2 = getApfFilter(config);
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         apfFilter2.setLinkProperties(lp);
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        program = mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         assertDrop(program, mcastv4packet.array());
         assertDrop(program, mcastv6packet.array());
         assertDrop(program, bcastv4packet1.array());
@@ -1747,7 +1558,8 @@
     private void doTestApfFilterMulticastPingWhileDozing(boolean isLightDozing) throws Exception {
         final ApfConfiguration configuration = getDefaultConfig();
         final ApfFilter apfFilter = getApfFilter(configuration);
-        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        byte[] program =
+            mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
                 ArgumentCaptor.forClass(BroadcastReceiver.class);
         verify(mDependencies).addDeviceIdleReceiver(receiverCaptor.capture());
@@ -1769,13 +1581,13 @@
             doReturn(true).when(mPowerManager).isDeviceIdleMode();
             receiver.onReceive(mContext, new Intent(ACTION_DEVICE_IDLE_MODE_CHANGED));
         }
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        program = mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         // ...and even while dozing...
         assertPass(program, packet.array());
 
         // ...but when the multicast filter is also enabled, drop the multicast pings to save power.
         apfFilter.setMulticastFilter(true);
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        program = mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         assertDrop(program, packet.array());
 
         // However, we should still let through all other ICMPv6 types.
@@ -1794,7 +1606,7 @@
             doReturn(false).when(mPowerManager).isDeviceIdleMode();
             receiver.onReceive(mContext, new Intent(ACTION_DEVICE_IDLE_MODE_CHANGED));
         }
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        program = mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         assertPass(program, packet.array());
     }
 
@@ -1803,7 +1615,8 @@
     public void testApfFilter802_3() throws Exception {
         ApfConfiguration config = getDefaultConfig();
         ApfFilter apfFilter = getApfFilter(config);
-        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        byte[] program =
+            mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // Verify empty packet of 100 zero bytes is passed
         // Note that eth-type = 0 makes it an IEEE802.3 frame
@@ -1821,7 +1634,7 @@
         // Now turn on the filter
         config.ieee802_3Filter = DROP_802_3_FRAMES;
         apfFilter = getApfFilter(config);
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        program = mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // Verify that IEEE802.3 frame is dropped
         // In this case ethtype is used for payload length
@@ -1846,7 +1659,8 @@
 
         ApfConfiguration config = getDefaultConfig();
         ApfFilter apfFilter = getApfFilter(config);
-        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        byte[] program =
+            mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // Verify empty packet of 100 zero bytes is passed
         // Note that eth-type = 0 makes it an IEEE802.3 frame
@@ -1864,7 +1678,7 @@
         // Now add IPv4 to the black list
         config.ethTypeBlackList = ipv4BlackList;
         apfFilter = getApfFilter(config);
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        program = mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // Verify that IPv4 frame will be dropped
         setIpv4VersionFields(packet);
@@ -1877,7 +1691,7 @@
         // Now let us have both IPv4 and IPv6 in the black list
         config.ethTypeBlackList = ipv4Ipv6BlackList;
         apfFilter = getApfFilter(config);
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        program = mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // Verify that IPv4 frame will be dropped
         setIpv4VersionFields(packet);
@@ -1915,7 +1729,8 @@
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
         ApfFilter apfFilter = getApfFilter(config);
-        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        byte[] program =
+            mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // Verify initially ARP request filter is off, and GARP filter is on.
         verifyArpFilter(program, PASS);
@@ -1925,11 +1740,11 @@
         LinkProperties lp = new LinkProperties();
         assertTrue(lp.addLinkAddress(linkAddress));
         apfFilter.setLinkProperties(lp);
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        program = mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         verifyArpFilter(program, DROP);
 
         apfFilter.setLinkProperties(new LinkProperties());
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        program = mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         // Inform ApfFilter of loss of IP and verify ARP filtering is off
         verifyArpFilter(program, PASS);
     }
@@ -2197,19 +2012,20 @@
     private byte[] verifyRaLifetime(ByteBuffer packet, int lifetime)
             throws IOException, ErrnoException {
         // Verify new program generated if ApfFilter witnesses RA
-        clearInvocations(mIpClientCb);
+        clearInvocations(mApfController);
         pretendPacketReceived(packet.array());
-        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        byte[] program =
+            mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         verifyRaLifetime(program, packet, lifetime);
         return program;
     }
 
     private void assertInvalidRa(ByteBuffer packet)
             throws IOException, ErrnoException, InterruptedException {
-        clearInvocations(mIpClientCb);
+        clearInvocations(mApfController);
         pretendPacketReceived(packet.array());
         Thread.sleep(NO_CALLBACK_TIMEOUT_MS);
-        verify(mIpClientCb, never()).installPacketFilter(any());
+        verify(mApfController, never()).installPacketFilter(any(), any());
     }
 
     @Test
@@ -2218,7 +2034,8 @@
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
         ApfFilter apfFilter = getApfFilter(config);
-        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        byte[] program =
+            mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         final int ROUTER_LIFETIME = 1000;
         final int PREFIX_VALID_LIFETIME = 200;
@@ -2302,7 +2119,8 @@
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
         final ApfFilter apfFilter = getApfFilter(config);
-        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        byte[] program =
+            mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         final int RA_REACHABLE_TIME = 1800;
         final int RA_RETRANSMISSION_TIMER = 1234;
 
@@ -2317,7 +2135,7 @@
 
         // Assume apf is shown the given RA, it generates program to filter it.
         pretendPacketReceived(raPacket);
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        program = mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         assertDrop(program, raPacket);
 
         // A packet with different reachable time should be passed.
@@ -2342,7 +2160,7 @@
         config.multicastFilter = DROP_MULTICAST;
         config.ieee802_3Filter = DROP_802_3_FRAMES;
         final ApfFilter apfFilter = getApfFilter(config);
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         final int routerLifetime = 1000;
         final int timePassedSeconds = 12;
@@ -2357,7 +2175,8 @@
         synchronized (apfFilter) {
             apfFilter.installNewProgram();
         }
-        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        byte[] program =
+            mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         verifyRaLifetime(program, basePacket, routerLifetime, timePassedSeconds);
 
         // Packet should be passed if the program is installed after 1/6 * lifetime from last seen
@@ -2367,7 +2186,7 @@
         synchronized (apfFilter) {
             apfFilter.installNewProgram();
         }
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        program = mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         assertDrop(program, basePacket.array());
 
         mCurrentTimeMs += DateUtils.SECOND_IN_MILLIS;
@@ -2375,7 +2194,7 @@
         synchronized (apfFilter) {
             apfFilter.installNewProgram();
         }
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        program = mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         assertPass(program, basePacket.array());
     }
 
@@ -2448,12 +2267,13 @@
     @Test
     public void testMatchedRaUpdatesLifetime() throws Exception {
         final ApfFilter apfFilter = getApfFilter(getDefaultConfig());
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // Create an RA and build an APF program
         byte[] ra = new RaPacketBuilder(1800 /* router lifetime */).build();
         pretendPacketReceived(ra);
-        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        byte[] program =
+            mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // lifetime dropped significantly, assert pass
         ra = new RaPacketBuilder(200 /* router lifetime */).build();
@@ -2461,7 +2281,7 @@
 
         // update program with the new RA
         pretendPacketReceived(ra);
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        program = mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // assert program was updated and new lifetimes were taken into account.
         assertDrop(program, ra);
@@ -2472,7 +2292,7 @@
         final ApfConfiguration config = getDefaultConfig();
         config.acceptRaMinLft = 180;
         ApfFilter apfFilter = getApfFilter(config);
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        mApfTestHelpers.consumeInstalledProgram(mApfController, 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)
@@ -2524,7 +2344,7 @@
                     String.format(packetStringFmt, lifetime + lifetime));
             // feed the RA into APF and generate the filter, the filter shouldn't crash.
             pretendPacketReceived(ra);
-            consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+            mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         }
     }
 
@@ -2536,7 +2356,7 @@
         final ApfConfiguration config = getDefaultConfig();
         config.acceptRaMinLft = 180;
         final ApfFilter apfFilter = getApfFilter(config);
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // Create an initial RA and build an APF program
         byte[] ra = new RaPacketBuilder(1800 /* router lifetime */)
@@ -2544,7 +2364,8 @@
                 .build();
 
         pretendPacketReceived(ra);
-        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        byte[] program =
+            mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // repeated RA is dropped
         assertDrop(program, ra);
@@ -2564,7 +2385,7 @@
         final ApfConfiguration config = getDefaultConfig();
         config.acceptRaMinLft = 180;
         final ApfFilter apfFilter = getApfFilter(config);
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // Create an initial RA and build an APF program
         byte[] ra = new RaPacketBuilder(1800 /* router lifetime */)
@@ -2572,7 +2393,8 @@
                 .build();
 
         pretendPacketReceived(ra);
-        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        byte[] program =
+            mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // repeated RA is dropped
         assertDrop(program, ra);
@@ -2599,13 +2421,14 @@
         final ApfConfiguration config = getDefaultConfig();
         config.acceptRaMinLft = 180;
         final ApfFilter apfFilter = getApfFilter(config);
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // Create an initial RA and build an APF program
         byte[] ra = new RaPacketBuilder(0 /* router lifetime */).build();
 
         pretendPacketReceived(ra);
-        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        byte[] program =
+            mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // repeated RA is dropped
         assertDrop(program, ra);
@@ -2627,13 +2450,14 @@
         final ApfConfiguration config = getDefaultConfig();
         config.acceptRaMinLft = 180;
         final ApfFilter apfFilter = getApfFilter(config);
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // Create an initial RA and build an APF program
         byte[] ra = new RaPacketBuilder(100 /* router lifetime */).build();
 
         pretendPacketReceived(ra);
-        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        byte[] program =
+            mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // repeated RA is dropped
         assertDrop(program, ra);
@@ -2663,13 +2487,14 @@
         final ApfConfiguration config = getDefaultConfig();
         config.acceptRaMinLft = 180;
         final ApfFilter apfFilter = getApfFilter(config);
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // Create an initial RA and build an APF program
         byte[] ra = new RaPacketBuilder(200 /* router lifetime */).build();
 
         pretendPacketReceived(ra);
-        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        byte[] program =
+            mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // repeated RA is dropped
         assertDrop(program, ra);
@@ -2695,13 +2520,14 @@
         final ApfConfiguration config = getDefaultConfig();
         config.acceptRaMinLft = 180;
         final ApfFilter apfFilter = getApfFilter(config);
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // Create an initial RA and build an APF program
         byte[] ra = new RaPacketBuilder(1800 /* router lifetime */).build();
 
         pretendPacketReceived(ra);
-        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        byte[] program =
+            mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // repeated RA is dropped
         assertDrop(program, ra);
@@ -2733,12 +2559,13 @@
         final ApfConfiguration config = getDefaultConfig();
         config.acceptRaMinLft = 180;
         final ApfFilter apfFilter = getApfFilter(config);
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // Create an initial RA and build an APF program
         byte[] ra = new RaPacketBuilder(1800 /* router lifetime */).build();
         pretendPacketReceived(ra);
-        byte[] program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        byte[] program =
+            mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // repeated RA is dropped.
         assertDrop(program, ra);
@@ -2747,37 +2574,37 @@
         ra = new RaPacketBuilder(599 /* router lifetime */).build();
         assertPass(program, ra);
         pretendPacketReceived(ra);
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        program = mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         assertDrop(program, ra);
 
         ra = new RaPacketBuilder(180 /* router lifetime */).build();
         assertPass(program, ra);
         pretendPacketReceived(ra);
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        program = mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         assertDrop(program, ra);
 
         ra = new RaPacketBuilder(0 /* router lifetime */).build();
         assertPass(program, ra);
         pretendPacketReceived(ra);
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        program = mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         assertDrop(program, ra);
 
         ra = new RaPacketBuilder(180 /* router lifetime */).build();
         assertPass(program, ra);
         pretendPacketReceived(ra);
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        program = mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         assertDrop(program, ra);
 
         ra = new RaPacketBuilder(599 /* router lifetime */).build();
         assertPass(program, ra);
         pretendPacketReceived(ra);
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        program = mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         assertDrop(program, ra);
 
         ra = new RaPacketBuilder(1800 /* router lifetime */).build();
         assertPass(program, ra);
         pretendPacketReceived(ra);
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        program = mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         assertDrop(program, ra);
     }
 
@@ -2802,7 +2629,7 @@
 
     @Test
     public void testInstallPacketFilterFailure() throws Exception {
-        doReturn(false).when(mIpClientCb).installPacketFilter(any());
+        doReturn(false).when(mApfController).installPacketFilter(any(), any());
         final ApfConfiguration config = getDefaultConfig();
         final ApfFilter apfFilter = getApfFilter(config);
 
@@ -2821,33 +2648,14 @@
     public void testApfProgramOverSize() throws Exception {
         final ApfConfiguration config = getDefaultConfig();
         config.apfVersionSupported = 2;
-        config.apfRamSize = 512;
+        config.apfRamSize = 256;
         final ApfFilter apfFilter = getApfFilter(config);
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
-        final byte[] ra = buildLargeRa();
-        pretendPacketReceived(ra);
-        // The generated program size will be 529, which is larger than 512
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         verify(mNetworkQuirkMetrics).setEvent(NetworkQuirkEvent.QE_APF_OVER_SIZE_FAILURE);
         verify(mNetworkQuirkMetrics).statsWrite();
     }
 
     @Test
-    public void testGenerateApfProgramException() {
-        final ApfConfiguration config = getDefaultConfig();
-        ApfFilter apfFilter = getApfFilter(config);
-        // Simulate exception during installNewProgram() by mocking
-        // mDependencies.elapsedRealtime() to throw an exception (this method doesn't throw in
-        // real-world scenarios).
-        doThrow(new IllegalStateException("test exception")).when(mDependencies).elapsedRealtime();
-        synchronized (apfFilter) {
-            apfFilter.installNewProgram();
-        }
-        verify(mNetworkQuirkMetrics).setEvent(NetworkQuirkEvent.QE_APF_GENERATE_FILTER_EXCEPTION);
-        verify(mNetworkQuirkMetrics).statsWrite();
-    }
-
-    @Test
     public void testApfSessionInfoMetrics() throws Exception {
         final ApfConfiguration config = getDefaultConfig();
         config.apfVersionSupported = 4;
@@ -2856,7 +2664,8 @@
         final long durationTimeMs = config.minMetricsSessionDurationMs;
         doReturn(startTimeMs).when(mDependencies).elapsedRealtime();
         final ApfFilter apfFilter = getApfFilter(config);
-        byte[] program = consumeInstalledProgram(mIpClientCb, 2 /* installCnt */);
+        byte[] program =
+            mApfTestHelpers.consumeInstalledProgram(mApfController, 2 /* installCnt */);
         int maxProgramSize = 0;
         int numProgramUpdated = 0;
         maxProgramSize = Math.max(maxProgramSize, program.length);
@@ -2876,13 +2685,13 @@
         expectedData[passedIpv6IcmpCounterIdx + 3] += 1;
         assertDataMemoryContentsIgnoreVersion(PASS, program, ra, data, expectedData);
         pretendPacketReceived(ra);
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        program = mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         maxProgramSize = Math.max(maxProgramSize, program.length);
         numProgramUpdated++;
 
         apfFilter.setMulticastFilter(true);
         // setMulticastFilter will trigger program installation.
-        program = consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        program = mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         maxProgramSize = Math.max(maxProgramSize, program.length);
         numProgramUpdated++;
 
@@ -2935,7 +2744,7 @@
         final long durationTimeMs = config.minMetricsSessionDurationMs;
         doReturn(startTimeMs).when(mDependencies).elapsedRealtime();
         final ApfFilter apfFilter = getApfFilter(config);
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         final int routerLifetime = 1000;
         final int prefixValidLifetime = 200;
@@ -2973,20 +2782,20 @@
         // Inject RA packets. Calling assertProgramUpdateAndGet()/assertNoProgramUpdate() is to make
         // sure that the RA packet has been processed.
         pretendPacketReceived(ra1.build());
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         pretendPacketReceived(ra2.build());
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         pretendPacketReceived(raInvalid.build());
         Thread.sleep(NO_CALLBACK_TIMEOUT_MS);
-        verify(mIpClientCb, never()).installPacketFilter(any());
+        verify(mApfController, never()).installPacketFilter(any(), any());
         pretendPacketReceived(raZeroRouterLifetime.build());
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         pretendPacketReceived(raZeroPioValidLifetime.build());
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         pretendPacketReceived(raZeroRdnssLifetime.build());
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
         pretendPacketReceived(raZeroRioRouteLifetime.build());
-        consumeInstalledProgram(mIpClientCb, 1 /* installCnt */);
+        mApfTestHelpers.consumeInstalledProgram(mApfController, 1 /* installCnt */);
 
         // Write metrics data to statsd pipeline when shutdown.
         doReturn(startTimeMs + durationTimeMs).when(mDependencies).elapsedRealtime();
@@ -3058,141 +2867,141 @@
         gen.addLoadData(R0, 0);
         gen.addAdd(1);
         gen.addStoreData(R0, 0);
-        gen.addLoad16(R0, 12);
+        gen.addLoad16intoR0(12);
         gen.addLoadImmediate(R1, -108);
-        gen.addJumpIfR0LessThan(0x600, "LABEL_504");
+        gen.addJumpIfR0LessThan(0x600, (short) -504);
         gen.addLoadImmediate(R1, -112);
-        gen.addJumpIfR0Equals(0x88a2, "LABEL_504");
-        gen.addJumpIfR0Equals(0x88a4, "LABEL_504");
-        gen.addJumpIfR0Equals(0x88b8, "LABEL_504");
-        gen.addJumpIfR0Equals(0x88cd, "LABEL_504");
-        gen.addJumpIfR0Equals(0x88e1, "LABEL_504");
-        gen.addJumpIfR0Equals(0x88e3, "LABEL_504");
-        gen.addJumpIfR0NotEquals(0x806, "LABEL_116");
+        gen.addJumpIfR0Equals(0x88a2, (short) -504);
+        gen.addJumpIfR0Equals(0x88a4, (short) -504);
+        gen.addJumpIfR0Equals(0x88b8, (short) -504);
+        gen.addJumpIfR0Equals(0x88cd, (short) -504);
+        gen.addJumpIfR0Equals(0x88e1, (short) -504);
+        gen.addJumpIfR0Equals(0x88e3, (short) -504);
+        gen.addJumpIfR0NotEquals(0x806, (short) -116);
         gen.addLoadImmediate(R0, 14);
         gen.addLoadImmediate(R1, -36);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("000108000604"), "LABEL_498");
-        gen.addLoad16(R0, 20);
-        gen.addJumpIfR0Equals(0x1, "LABEL_102");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("000108000604"), (short) -498);
+        gen.addLoad16intoR0(20);
+        gen.addJumpIfR0Equals(0x1, (short) -102);
         gen.addLoadImmediate(R1, -40);
-        gen.addJumpIfR0NotEquals(0x2, "LABEL_498");
-        gen.addLoad32(R0, 28);
+        gen.addJumpIfR0NotEquals(0x2, (short) -498);
+        gen.addLoad32intoR0(28);
         gen.addLoadImmediate(R1, -116);
-        gen.addJumpIfR0Equals(0x0, "LABEL_504");
+        gen.addJumpIfR0Equals(0x0, (short) -504);
         gen.addLoadImmediate(R0, 0);
         gen.addLoadImmediate(R1, -44);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ffffffffffff"), "LABEL_498");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ffffffffffff"), (short) -498);
 
-        gen.defineLabel("LABEL_102");
-        gen.addLoad32(R0, 38);
+        gen.defineLabel((short) -102);
+        gen.addLoad32intoR0(38);
         gen.addLoadImmediate(R1, -64);
-        gen.addJumpIfR0Equals(0x0, "LABEL_504");
+        gen.addJumpIfR0Equals(0x0, (short) -504);
         gen.addLoadImmediate(R1, -8);
-        gen.addJump("LABEL_498");
+        gen.addJump((short) -498);
 
-        gen.defineLabel("LABEL_116");
-        gen.addLoad16(R0, 12);
-        gen.addJumpIfR0NotEquals(0x800, "LABEL_207");
-        gen.addLoad8(R0, 23);
-        gen.addJumpIfR0NotEquals(0x11, "LABEL_159");
-        gen.addLoad16(R0, 20);
-        gen.addJumpIfR0AnyBitsSet(0x1fff, "LABEL_159");
+        gen.defineLabel((short) -116);
+        gen.addLoad16intoR0(12);
+        gen.addJumpIfR0NotEquals(0x800, (short) -207);
+        gen.addLoad8intoR0(23);
+        gen.addJumpIfR0NotEquals(0x11, (short) -159);
+        gen.addLoad16intoR0(20);
+        gen.addJumpIfR0AnyBitsSet(0x1fff, (short) -159);
         gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE);
-        gen.addLoad16Indexed(R0, 16);
-        gen.addJumpIfR0NotEquals(0x44, "LABEL_159");
+        gen.addLoad16R1IndexedIntoR0(16);
+        gen.addJumpIfR0NotEquals(0x44, (short) -159);
         gen.addLoadImmediate(R0, 50);
         gen.addAddR1ToR0();
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("e212507c6345"), "LABEL_159");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("e212507c6345"), (short) -159);
         gen.addLoadImmediate(R1, -12);
-        gen.addJump("LABEL_498");
+        gen.addJump((short) -498);
 
-        gen.defineLabel("LABEL_159");
-        gen.addLoad8(R0, 30);
+        gen.defineLabel((short) -159);
+        gen.addLoad8intoR0(30);
         gen.addAnd(240);
         gen.addLoadImmediate(R1, -84);
-        gen.addJumpIfR0Equals(0xe0, "LABEL_504");
+        gen.addJumpIfR0Equals(0xe0, (short) -504);
         gen.addLoadImmediate(R1, -76);
-        gen.addLoad32(R0, 30);
-        gen.addJumpIfR0Equals(0xffffffff, "LABEL_504");
+        gen.addLoad32intoR0(30);
+        gen.addJumpIfR0Equals(0xffffffff, (short) -504);
         gen.addLoadImmediate(R1, -24);
         gen.addLoadImmediate(R0, 0);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ffffffffffff"), "LABEL_498");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ffffffffffff"), (short) -498);
         gen.addLoadImmediate(R1, -72);
-        gen.addJump("LABEL_504");
+        gen.addJump((short) -504);
         gen.addLoadImmediate(R1, -16);
-        gen.addJump("LABEL_498");
+        gen.addJump((short) -498);
 
-        gen.defineLabel("LABEL_207");
-        gen.addJumpIfR0Equals(0x86dd, "LABEL_231");
+        gen.defineLabel((short) -207);
+        gen.addJumpIfR0Equals(0x86dd, (short) -231);
         gen.addLoadImmediate(R0, 0);
         gen.addLoadImmediate(R1, -48);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ffffffffffff"), "LABEL_498");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ffffffffffff"), (short) -498);
         gen.addLoadImmediate(R1, -56);
-        gen.addJump("LABEL_504");
+        gen.addJump((short) -504);
 
-        gen.defineLabel("LABEL_231");
-        gen.addLoad8(R0, 20);
-        gen.addJumpIfR0Equals(0x3a, "LABEL_249");
+        gen.defineLabel((short) -231);
+        gen.addLoad8intoR0(20);
+        gen.addJumpIfR0Equals(0x3a, (short) -249);
         gen.addLoadImmediate(R1, -104);
-        gen.addLoad8(R0, 38);
-        gen.addJumpIfR0Equals(0xff, "LABEL_504");
+        gen.addLoad8intoR0(38);
+        gen.addJumpIfR0Equals(0xff, (short) -504);
         gen.addLoadImmediate(R1, -32);
-        gen.addJump("LABEL_498");
+        gen.addJump((short) -498);
 
-        gen.defineLabel("LABEL_249");
-        gen.addLoad8(R0, 54);
+        gen.defineLabel((short) -249);
+        gen.addLoad8intoR0(54);
         gen.addLoadImmediate(R1, -88);
-        gen.addJumpIfR0Equals(0x85, "LABEL_504");
-        gen.addJumpIfR0NotEquals(0x88, "LABEL_283");
+        gen.addJumpIfR0Equals(0x85, (short) -504);
+        gen.addJumpIfR0NotEquals(0x88, (short) -283);
         gen.addLoadImmediate(R0, 38);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ff0200000000000000000000000000"), "LABEL_283");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ff0200000000000000000000000000"), (short) -283);
         gen.addLoadImmediate(R1, -92);
-        gen.addJump("LABEL_504");
+        gen.addJump((short) -504);
 
-        gen.defineLabel("LABEL_283");
+        gen.defineLabel((short) -283);
         gen.addLoadFromMemory(R0, MemorySlot.PACKET_SIZE);
-        gen.addJumpIfR0NotEquals(0xa6, "LABEL_496");
+        gen.addJumpIfR0NotEquals(0xa6, (short) -496);
         gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_SECONDS);
-        gen.addJumpIfR0GreaterThan(0x254, "LABEL_496");
+        gen.addJumpIfR0GreaterThan(0x254, (short) -496);
         gen.addLoadImmediate(R0, 0);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("e212507c6345648788fd6df086dd68"), "LABEL_496");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("e212507c6345648788fd6df086dd68"), (short) -496);
         gen.addLoadImmediate(R0, 18);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("00703afffe800000000000002a0079e10abc1539fe80000000000000e01250fffe7c63458600"), "LABEL_496");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("00703afffe800000000000002a0079e10abc1539fe80000000000000e01250fffe7c63458600"), (short) -496);
         gen.addLoadImmediate(R0, 58);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("4000"), "LABEL_496");
-        gen.addLoad16(R0, 60);
-        gen.addJumpIfR0LessThan(0x254, "LABEL_496");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("4000"), (short) -496);
+        gen.addLoad16intoR0(60);
+        gen.addJumpIfR0LessThan(0x254, (short) -496);
         gen.addLoadImmediate(R0, 62);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("0000000000000000"), "LABEL_496");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("0000000000000000"), (short) -496);
         gen.addLoadImmediate(R0, 78);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("19050000"), "LABEL_496");
-        gen.addLoad32(R0, 82);
-        gen.addJumpIfR0LessThan(0x254, "LABEL_496");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("19050000"), (short) -496);
+        gen.addLoad32intoR0(82);
+        gen.addJumpIfR0LessThan(0x254, (short) -496);
         gen.addLoadImmediate(R0, 86);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("2001486048600000000000000000646420014860486000000000000000000064"), "LABEL_496");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("2001486048600000000000000000646420014860486000000000000000000064"), (short) -496);
         gen.addLoadImmediate(R0, 118);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("030440c0"), "LABEL_496");
-        gen.addLoad32(R0, 122);
-        gen.addJumpIfR0LessThan(0x254, "LABEL_496");
-        gen.addLoad32(R0, 126);
-        gen.addJumpIfR0LessThan(0x254, "LABEL_496");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("030440c0"), (short) -496);
+        gen.addLoad32intoR0(122);
+        gen.addJumpIfR0LessThan(0x254, (short) -496);
+        gen.addLoad32intoR0(126);
+        gen.addJumpIfR0LessThan(0x254, (short) -496);
         gen.addLoadImmediate(R0, 130);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("00000000"), "LABEL_496");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("00000000"), (short) -496);
         gen.addLoadImmediate(R0, 134);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("2a0079e10abc15390000000000000000"), "LABEL_496");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("2a0079e10abc15390000000000000000"), (short) -496);
         gen.addLoadImmediate(R1, -60);
-        gen.addJump("LABEL_504");
+        gen.addJump((short) -504);
 
-        gen.defineLabel("LABEL_496");
+        gen.defineLabel((short) -496);
         gen.addLoadImmediate(R1, -28);
 
-        gen.defineLabel("LABEL_498");
+        gen.defineLabel((short) -498);
         gen.addLoadData(R0, 0);
         gen.addAdd(1);
         gen.addStoreData(R0, 0);
         gen.addJump(PASS_LABEL);
 
-        gen.defineLabel("LABEL_504");
+        gen.defineLabel((short) -504);
         gen.addLoadData(R0, 0);
         gen.addAdd(1);
         gen.addStoreData(R0, 0);
@@ -3212,109 +3021,109 @@
         gen.addLoadData(R0, 0);
         gen.addAdd(1);
         gen.addStoreData(R0, 0);
-        gen.addLoad16(R0, 12);
+        gen.addLoad16intoR0(12);
         gen.addLoadImmediate(R1, -108);
-        gen.addJumpIfR0LessThan(0x600, "LABEL_283");
+        gen.addJumpIfR0LessThan(0x600, (short) -283);
         gen.addLoadImmediate(R1, -112);
-        gen.addJumpIfR0Equals(0x88a2, "LABEL_283");
-        gen.addJumpIfR0Equals(0x88a4, "LABEL_283");
-        gen.addJumpIfR0Equals(0x88b8, "LABEL_283");
-        gen.addJumpIfR0Equals(0x88cd, "LABEL_283");
-        gen.addJumpIfR0Equals(0x88e1, "LABEL_283");
-        gen.addJumpIfR0Equals(0x88e3, "LABEL_283");
-        gen.addJumpIfR0NotEquals(0x806, "LABEL_109");
+        gen.addJumpIfR0Equals(0x88a2, (short) -283);
+        gen.addJumpIfR0Equals(0x88a4, (short) -283);
+        gen.addJumpIfR0Equals(0x88b8, (short) -283);
+        gen.addJumpIfR0Equals(0x88cd, (short) -283);
+        gen.addJumpIfR0Equals(0x88e1, (short) -283);
+        gen.addJumpIfR0Equals(0x88e3, (short) -283);
+        gen.addJumpIfR0NotEquals(0x806, (short) -109);
         gen.addLoadImmediate(R0, 14);
         gen.addLoadImmediate(R1, -36);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("000108000604"), "LABEL_277");
-        gen.addLoad16(R0, 20);
-        gen.addJumpIfR0Equals(0x1, "LABEL_94");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("000108000604"), (short) -277);
+        gen.addLoad16intoR0(20);
+        gen.addJumpIfR0Equals(0x1, (short) -94);
         gen.addLoadImmediate(R1, -40);
-        gen.addJumpIfR0NotEquals(0x2, "LABEL_277");
-        gen.addLoad32(R0, 28);
+        gen.addJumpIfR0NotEquals(0x2, (short) -277);
+        gen.addLoad32intoR0(28);
         gen.addLoadImmediate(R1, -116);
-        gen.addJumpIfR0Equals(0x0, "LABEL_283");
+        gen.addJumpIfR0Equals(0x0, (short) -283);
         gen.addLoadImmediate(R0, 0);
         gen.addLoadImmediate(R1, -44);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ffffffffffff"), "LABEL_277");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ffffffffffff"), (short) -277);
 
-        gen.defineLabel("LABEL_94");
+        gen.defineLabel((short) -94);
         gen.addLoadImmediate(R0, 38);
         gen.addLoadImmediate(R1, -68);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("c0a801b3"), "LABEL_283");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("c0a801b3"), (short) -283);
         gen.addLoadImmediate(R1, -8);
-        gen.addJump("LABEL_277");
+        gen.addJump((short) -277);
 
-        gen.defineLabel("LABEL_109");
-        gen.addLoad16(R0, 12);
-        gen.addJumpIfR0NotEquals(0x800, "LABEL_204");
-        gen.addLoad8(R0, 23);
-        gen.addJumpIfR0NotEquals(0x11, "LABEL_151");
-        gen.addLoad16(R0, 20);
-        gen.addJumpIfR0AnyBitsSet(0x1fff, "LABEL_151");
+        gen.defineLabel((short) -109);
+        gen.addLoad16intoR0(12);
+        gen.addJumpIfR0NotEquals(0x800, (short) -204);
+        gen.addLoad8intoR0(23);
+        gen.addJumpIfR0NotEquals(0x11, (short) -151);
+        gen.addLoad16intoR0(20);
+        gen.addJumpIfR0AnyBitsSet(0x1fff, (short) -151);
         gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE);
-        gen.addLoad16Indexed(R0, 16);
-        gen.addJumpIfR0NotEquals(0x44, "LABEL_151");
+        gen.addLoad16R1IndexedIntoR0(16);
+        gen.addJumpIfR0NotEquals(0x44, (short) -151);
         gen.addLoadImmediate(R0, 50);
         gen.addAddR1ToR0();
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("f683d58f832b"), "LABEL_151");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("f683d58f832b"), (short) -151);
         gen.addLoadImmediate(R1, -12);
-        gen.addJump("LABEL_277");
+        gen.addJump((short) -277);
 
-        gen.defineLabel("LABEL_151");
-        gen.addLoad8(R0, 30);
+        gen.defineLabel((short) -151);
+        gen.addLoad8intoR0(30);
         gen.addAnd(240);
         gen.addLoadImmediate(R1, -84);
-        gen.addJumpIfR0Equals(0xe0, "LABEL_283");
+        gen.addJumpIfR0Equals(0xe0, (short) -283);
         gen.addLoadImmediate(R1, -76);
-        gen.addLoad32(R0, 30);
-        gen.addJumpIfR0Equals(0xffffffff, "LABEL_283");
+        gen.addLoad32intoR0(30);
+        gen.addJumpIfR0Equals(0xffffffff, (short) -283);
         gen.addLoadImmediate(R1, -80);
-        gen.addJumpIfR0Equals(0xc0a801ff, "LABEL_283");
+        gen.addJumpIfR0Equals(0xc0a801ff, (short) -283);
         gen.addLoadImmediate(R1, -24);
         gen.addLoadImmediate(R0, 0);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ffffffffffff"), "LABEL_277");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ffffffffffff"), (short) -277);
         gen.addLoadImmediate(R1, -72);
-        gen.addJump("LABEL_283");
+        gen.addJump((short) -283);
         gen.addLoadImmediate(R1, -16);
-        gen.addJump("LABEL_277");
+        gen.addJump((short) -277);
 
-        gen.defineLabel("LABEL_204");
-        gen.addJumpIfR0Equals(0x86dd, "LABEL_225");
+        gen.defineLabel((short) -204);
+        gen.addJumpIfR0Equals(0x86dd, (short) -225);
         gen.addLoadImmediate(R0, 0);
         gen.addLoadImmediate(R1, -48);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ffffffffffff"), "LABEL_277");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ffffffffffff"), (short) -277);
         gen.addLoadImmediate(R1, -56);
-        gen.addJump("LABEL_283");
+        gen.addJump((short) -283);
 
-        gen.defineLabel("LABEL_225");
-        gen.addLoad8(R0, 20);
-        gen.addJumpIfR0Equals(0x3a, "LABEL_241");
+        gen.defineLabel((short) -225);
+        gen.addLoad8intoR0(20);
+        gen.addJumpIfR0Equals(0x3a, (short) -241);
         gen.addLoadImmediate(R1, -104);
-        gen.addLoad8(R0, 38);
-        gen.addJumpIfR0Equals(0xff, "LABEL_283");
+        gen.addLoad8intoR0(38);
+        gen.addJumpIfR0Equals(0xff, (short) -283);
         gen.addLoadImmediate(R1, -32);
-        gen.addJump("LABEL_277");
+        gen.addJump((short) -277);
 
-        gen.defineLabel("LABEL_241");
-        gen.addLoad8(R0, 54);
+        gen.defineLabel((short) -241);
+        gen.addLoad8intoR0(54);
         gen.addLoadImmediate(R1, -88);
-        gen.addJumpIfR0Equals(0x85, "LABEL_283");
-        gen.addJumpIfR0NotEquals(0x88, "LABEL_275");
+        gen.addJumpIfR0Equals(0x85, (short) -283);
+        gen.addJumpIfR0NotEquals(0x88, (short) -275);
         gen.addLoadImmediate(R0, 38);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ff0200000000000000000000000000"), "LABEL_275");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ff0200000000000000000000000000"), (short) -275);
         gen.addLoadImmediate(R1, -92);
-        gen.addJump("LABEL_283");
+        gen.addJump((short) -283);
 
-        gen.defineLabel("LABEL_275");
+        gen.defineLabel((short) -275);
         gen.addLoadImmediate(R1, -28);
 
-        gen.defineLabel("LABEL_277");
+        gen.defineLabel((short) -277);
         gen.addLoadData(R0, 0);
         gen.addAdd(1);
         gen.addStoreData(R0, 0);
         gen.addJump(PASS_LABEL);
 
-        gen.defineLabel("LABEL_283");
+        gen.defineLabel((short) -283);
         gen.addLoadData(R0, 0);
         gen.addAdd(1);
         gen.addStoreData(R0, 0);
@@ -3333,7 +3142,7 @@
         gen.addLoadData(R0, 0);
         gen.addAdd(1);
         gen.addStoreData(R0, 0);
-        gen.addLoad16(R0, 12);
+        gen.addLoad16intoR0(12);
         gen.addCountAndDropIfR0LessThan(0x600, getCounterEnumFromOffset(-108));
         gen.addLoadImmediate(R1, -112);
         gen.addJumpIfR0Equals(0x88a2, gen.mCountAndDropLabel);
@@ -3342,95 +3151,95 @@
         gen.addJumpIfR0Equals(0x88cd, gen.mCountAndDropLabel);
         gen.addJumpIfR0Equals(0x88e1, gen.mCountAndDropLabel);
         gen.addJumpIfR0Equals(0x88e3, gen.mCountAndDropLabel);
-        gen.addJumpIfR0NotEquals(0x806, "LABEL_115");
+        gen.addJumpIfR0NotEquals(0x806, (short) -115);
         gen.addLoadImmediate(R0, 14);
         gen.addCountAndPassIfBytesAtR0NotEqual(hexStringToByteArray("000108000604"), getCounterEnumFromOffset(-36));
-        gen.addLoad16(R0, 20);
-        gen.addJumpIfR0Equals(0x1, "LABEL_100");
+        gen.addLoad16intoR0(20);
+        gen.addJumpIfR0Equals(0x1, (short) -100);
         gen.addCountAndPassIfR0NotEquals(0x2, getCounterEnumFromOffset(-40));
-        gen.addLoad32(R0, 28);
+        gen.addLoad32intoR0(28);
         gen.addCountAndDropIfR0Equals(0x0, getCounterEnumFromOffset(-116));
         gen.addLoadImmediate(R0, 0);
         gen.addCountAndPassIfBytesAtR0NotEqual(hexStringToByteArray("ffffffffffff"), getCounterEnumFromOffset(-44));
 
-        gen.defineLabel("LABEL_100");
+        gen.defineLabel((short) -100);
         gen.addLoadImmediate(R0, 38);
         gen.addCountAndDropIfBytesAtR0NotEqual(hexStringToByteArray("c0a801be"), getCounterEnumFromOffset(-68));
         gen.addCountAndPass(getCounterEnumFromOffset(-8));
 
-        gen.defineLabel("LABEL_115");
-        gen.addLoad16(R0, 12);
-        gen.addJumpIfR0NotEquals(0x800, "LABEL_263");
-        gen.addLoad8(R0, 23);
-        gen.addJumpIfR0NotEquals(0x11, "LABEL_157");
-        gen.addLoad16(R0, 20);
-        gen.addJumpIfR0AnyBitsSet(0x1fff, "LABEL_157");
+        gen.defineLabel((short) -115);
+        gen.addLoad16intoR0(12);
+        gen.addJumpIfR0NotEquals(0x800, (short) -263);
+        gen.addLoad8intoR0(23);
+        gen.addJumpIfR0NotEquals(0x11, (short) -157);
+        gen.addLoad16intoR0(20);
+        gen.addJumpIfR0AnyBitsSet(0x1fff, (short) -157);
         gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE);
-        gen.addLoad16Indexed(R0, 16);
-        gen.addJumpIfR0NotEquals(0x44, "LABEL_157");
+        gen.addLoad16R1IndexedIntoR0(16);
+        gen.addJumpIfR0NotEquals(0x44, (short) -157);
         gen.addLoadImmediate(R0, 50);
         gen.addAddR1ToR0();
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ea42226789c0"), "LABEL_157");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ea42226789c0"), (short) -157);
         gen.addCountAndPass(getCounterEnumFromOffset(-12));
 
-        gen.defineLabel("LABEL_157");
-        gen.addLoad8(R0, 30);
+        gen.defineLabel((short) -157);
+        gen.addLoad8intoR0(30);
         gen.addAnd(240);
         gen.addCountAndDropIfR0Equals(0xe0, getCounterEnumFromOffset(-84));
         gen.addLoadImmediate(R1, -76);
-        gen.addLoad32(R0, 30);
+        gen.addLoad32intoR0(30);
         gen.addJumpIfR0Equals(0xffffffff, gen.mCountAndDropLabel);
         gen.addCountAndDropIfR0Equals(0xc0a801ff, getCounterEnumFromOffset(-80));
-        gen.addLoad8(R0, 23);
-        gen.addJumpIfR0NotEquals(0x11, "LABEL_243");
+        gen.addLoad8intoR0(23);
+        gen.addJumpIfR0NotEquals(0x11, (short) -243);
         gen.addLoadImmediate(R0, 26);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("6b7a1f1fc0a801be"), "LABEL_243");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("6b7a1f1fc0a801be"), (short) -243);
         gen.addLoadFromMemory(R0, MemorySlot.IPV4_HEADER_SIZE);
         gen.addAdd(8);
         gen.addSwap();
-        gen.addLoad16(R0, 16);
+        gen.addLoad16intoR0(16);
         gen.addNeg(R1);
         gen.addAddR1ToR0();
-        gen.addJumpIfR0NotEquals(0x1, "LABEL_243");
+        gen.addJumpIfR0NotEquals(0x1, (short) -243);
         gen.addLoadFromMemory(R0, MemorySlot.IPV4_HEADER_SIZE);
         gen.addAdd(14);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("1194ceca"), "LABEL_243");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("1194ceca"), (short) -243);
         gen.addAdd(8);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ff"), "LABEL_243");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ff"), (short) -243);
         gen.addCountAndDrop(getCounterEnumFromOffset(-128));
 
-        gen.defineLabel("LABEL_243");
+        gen.defineLabel((short) -243);
         gen.addLoadImmediate(R1, -24);
         gen.addLoadImmediate(R0, 0);
         gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ffffffffffff"), gen.mCountAndPassLabel);
         gen.addCountAndDrop(getCounterEnumFromOffset(-72));
         gen.addCountAndPass(getCounterEnumFromOffset(-16));
 
-        gen.defineLabel("LABEL_263");
-        gen.addJumpIfR0Equals(0x86dd, "LABEL_284");
+        gen.defineLabel((short) -263);
+        gen.addJumpIfR0Equals(0x86dd, (short) -284);
         gen.addLoadImmediate(R0, 0);
         gen.addCountAndPassIfBytesAtR0NotEqual(hexStringToByteArray("ffffffffffff"), getCounterEnumFromOffset(-48));
         gen.addCountAndDrop(getCounterEnumFromOffset(-56));
 
-        gen.defineLabel("LABEL_284");
-        gen.addLoad8(R0, 20);
+        gen.defineLabel((short) -284);
+        gen.addLoad8intoR0(20);
         gen.addJumpIfR0Equals(0x0, gen.mCountAndPassLabel);
-        gen.addJumpIfR0Equals(0x3a, "LABEL_303");
+        gen.addJumpIfR0Equals(0x3a, (short) -303);
         gen.addLoadImmediate(R1, -104);
-        gen.addLoad8(R0, 38);
+        gen.addLoad8intoR0(38);
         gen.addJumpIfR0Equals(0xff, gen.mCountAndDropLabel);
         gen.addCountAndPass(getCounterEnumFromOffset(-32));
 
-        gen.defineLabel("LABEL_303");
-        gen.addLoad8(R0, 54);
+        gen.defineLabel((short) -303);
+        gen.addLoad8intoR0(54);
         gen.addLoadImmediate(R1, -88);
         gen.addJumpIfR0Equals(0x85, gen.mCountAndDropLabel);
-        gen.addJumpIfR0NotEquals(0x88, "LABEL_337");
+        gen.addJumpIfR0NotEquals(0x88, (short) -337);
         gen.addLoadImmediate(R0, 38);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ff0200000000000000000000000000"), "LABEL_337");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ff0200000000000000000000000000"), (short) -337);
         gen.addCountAndDrop(getCounterEnumFromOffset(-92));
 
-        gen.defineLabel("LABEL_337");
+        gen.defineLabel((short) -337);
         gen.addLoadImmediate(R1, -28);
 
         gen.addCountTrampoline();
@@ -3448,7 +3257,7 @@
         gen.addLoadCounter(R0, getCounterEnumFromOffset(-8));
         gen.addAdd(1);
         gen.addStoreData(R0, 0);
-        gen.addLoad16(R0, 12);
+        gen.addLoad16intoR0(12);
         gen.addCountAndDropIfR0LessThan(0x600, getCounterEnumFromOffset(-120));
         gen.addLoadImmediate(R1, -124);
         gen.addJumpIfR0Equals(0x88a2, gen.mCountAndDropLabel);
@@ -3457,130 +3266,130 @@
         gen.addJumpIfR0Equals(0x88cd, gen.mCountAndDropLabel);
         gen.addJumpIfR0Equals(0x88e1, gen.mCountAndDropLabel);
         gen.addJumpIfR0Equals(0x88e3, gen.mCountAndDropLabel);
-        gen.addJumpIfR0NotEquals(0x806, "LABEL_122");
+        gen.addJumpIfR0NotEquals(0x806, (short) -122);
         gen.addLoadImmediate(R0, 14);
         gen.addCountAndDropIfBytesAtR0NotEqual(hexStringToByteArray("000108000604"), getCounterEnumFromOffset(-152));
-        gen.addLoad16(R0, 20);
-        gen.addJumpIfR0Equals(0x1, "LABEL_104");
+        gen.addLoad16intoR0(20);
+        gen.addJumpIfR0Equals(0x1, (short) -104);
         gen.addCountAndDropIfR0NotEquals(0x2, getCounterEnumFromOffset(-156));
-        gen.addLoad32(R0, 28);
+        gen.addLoad32intoR0(28);
         gen.addCountAndDropIfR0Equals(0x0, getCounterEnumFromOffset(-128));
         gen.addLoadImmediate(R0, 0);
         gen.addCountAndPassIfBytesAtR0NotEqual(hexStringToByteArray("ffffffffffff"), getCounterEnumFromOffset(-56));
 
-        gen.defineLabel("LABEL_104");
+        gen.defineLabel((short) -104);
         gen.addLoadImmediate(R0, 38);
         gen.addCountAndDropIfBytesAtR0NotEqual(hexStringToByteArray("c0a801ec"), getCounterEnumFromOffset(-80));
         gen.addCountAndPass(getCounterEnumFromOffset(-20));
 
-        gen.defineLabel("LABEL_122");
-        gen.addLoad16(R0, 12);
-        gen.addJumpIfR0NotEquals(0x800, "LABEL_249");
-        gen.addLoad8(R0, 23);
-        gen.addJumpIfR0NotEquals(0x11, "LABEL_165");
-        gen.addLoad16(R0, 20);
-        gen.addJumpIfR0AnyBitsSet(0x1fff, "LABEL_165");
+        gen.defineLabel((short) -122);
+        gen.addLoad16intoR0(12);
+        gen.addJumpIfR0NotEquals(0x800, (short) -249);
+        gen.addLoad8intoR0(23);
+        gen.addJumpIfR0NotEquals(0x11, (short) -165);
+        gen.addLoad16intoR0(20);
+        gen.addJumpIfR0AnyBitsSet(0x1fff, (short) -165);
         gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE);
-        gen.addLoad16Indexed(R0, 16);
-        gen.addJumpIfR0NotEquals(0x44, "LABEL_165");
+        gen.addLoad16R1IndexedIntoR0(16);
+        gen.addJumpIfR0NotEquals(0x44, (short) -165);
         gen.addLoadImmediate(R0, 50);
         gen.addAddR1ToR0();
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("7e9046bc7008"), "LABEL_165");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("7e9046bc7008"), (short) -165);
         gen.addCountAndPass(getCounterEnumFromOffset(-24));
 
-        gen.defineLabel("LABEL_165");
-        gen.addLoad8(R0, 30);
+        gen.defineLabel((short) -165);
+        gen.addLoad8intoR0(30);
         gen.addAnd(240);
         gen.addCountAndDropIfR0Equals(0xe0, getCounterEnumFromOffset(-96));
         gen.addLoadImmediate(R1, -88);
-        gen.addLoad32(R0, 30);
+        gen.addLoad32intoR0(30);
         gen.addJumpIfR0Equals(0xffffffff, gen.mCountAndDropLabel);
         gen.addCountAndDropIfR0Equals(0xc0a801ff, getCounterEnumFromOffset(-92));
-        gen.addLoad8(R0, 23);
-        gen.addJumpIfR0NotEquals(0x6, "LABEL_225");
-        gen.addLoad16(R0, 20);
-        gen.addJumpIfR0AnyBitsSet(0x1fff, "LABEL_225");
+        gen.addLoad8intoR0(23);
+        gen.addJumpIfR0NotEquals(0x6, (short) -225);
+        gen.addLoad16intoR0(20);
+        gen.addJumpIfR0AnyBitsSet(0x1fff, (short) -225);
         gen.addLoadFromMemory(R1, MemorySlot.IPV4_HEADER_SIZE);
-        gen.addLoad16Indexed(R0, 16);
-        gen.addJumpIfR0NotEquals(0x7, "LABEL_225");
+        gen.addLoad16R1IndexedIntoR0(16);
+        gen.addJumpIfR0NotEquals(0x7, (short) -225);
         gen.addCountAndDrop(getCounterEnumFromOffset(-148));
 
-        gen.defineLabel("LABEL_225");
+        gen.defineLabel((short) -225);
         gen.addLoadImmediate(R1, -36);
         gen.addLoadImmediate(R0, 0);
         gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ffffffffffff"), gen.mCountAndPassLabel);
         gen.addCountAndDrop(getCounterEnumFromOffset(-84));
         gen.addCountAndPass(getCounterEnumFromOffset(-28));
 
-        gen.defineLabel("LABEL_249");
-        gen.addJumpIfR0Equals(0x86dd, "LABEL_273");
+        gen.defineLabel((short) -249);
+        gen.addJumpIfR0Equals(0x86dd, (short) -273);
         gen.addLoadImmediate(R0, 0);
         gen.addCountAndPassIfBytesAtR0NotEqual(hexStringToByteArray("ffffffffffff"), getCounterEnumFromOffset(-60));
         gen.addCountAndDrop(getCounterEnumFromOffset(-68));
 
-        gen.defineLabel("LABEL_273");
-        gen.addLoad8(R0, 20);
+        gen.defineLabel((short) -273);
+        gen.addLoad8intoR0(20);
         gen.addJumpIfR0Equals(0x0, gen.mCountAndPassLabel);
-        gen.addJumpIfR0Equals(0x3a, "LABEL_297");
+        gen.addJumpIfR0Equals(0x3a, (short) -297);
         gen.addLoadImmediate(R1, -116);
-        gen.addLoad8(R0, 38);
+        gen.addLoad8intoR0(38);
         gen.addJumpIfR0Equals(0xff, gen.mCountAndDropLabel);
         gen.addCountAndPass(getCounterEnumFromOffset(-44));
 
-        gen.defineLabel("LABEL_297");
-        gen.addLoad8(R0, 54);
+        gen.defineLabel((short) -297);
+        gen.addLoad8intoR0(54);
         gen.addCountAndDropIfR0Equals(0x85, getCounterEnumFromOffset(-100));
-        gen.addJumpIfR0NotEquals(0x88, "LABEL_333");
+        gen.addJumpIfR0NotEquals(0x88, (short) -333);
         gen.addLoadImmediate(R0, 38);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ff0200000000000000000000000000"), "LABEL_333");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("ff0200000000000000000000000000"), (short) -333);
         gen.addCountAndDrop(getCounterEnumFromOffset(-104));
 
-        gen.defineLabel("LABEL_333");
+        gen.defineLabel((short) -333);
         gen.addLoadFromMemory(R0, MemorySlot.PACKET_SIZE);
-        gen.addJumpIfR0NotEquals(0x96, "LABEL_574");
+        gen.addJumpIfR0NotEquals(0x96, (short) -574);
         gen.addLoadFromMemory(R0, MemorySlot.FILTER_AGE_SECONDS);
-        gen.addJumpIfR0GreaterThan(0x48e, "LABEL_574");
+        gen.addJumpIfR0GreaterThan(0x48e, (short) -574);
         gen.addLoadImmediate(R0, 0);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("7e9046bc700828c68e23672c86dd60"), "LABEL_574");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("7e9046bc700828c68e23672c86dd60"), (short) -574);
         gen.addLoadImmediate(R0, 18);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("00603afffe800000000000002ac68efffe23672c"), "LABEL_574");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("00603afffe800000000000002ac68efffe23672c"), (short) -574);
         gen.addLoadImmediate(R0, 54);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("8600"), "LABEL_574");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("8600"), (short) -574);
         gen.addLoadImmediate(R0, 58);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("40c0"), "LABEL_574");
-        gen.addLoad16(R0, 60);
-        gen.addJumpIfR0Equals(0x0, "LABEL_574");
-        gen.addJumpIfR0LessThan(0xb4, "LABEL_421");
-        gen.addJumpIfR0LessThan(0x91e, "LABEL_574");
-        gen.addJumpIfR0GreaterThan(0x1b58, "LABEL_574");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("40c0"), (short) -574);
+        gen.addLoad16intoR0(60);
+        gen.addJumpIfR0Equals(0x0, (short) -574);
+        gen.addJumpIfR0LessThan(0xb4, (short) -421);
+        gen.addJumpIfR0LessThan(0x91e, (short) -574);
+        gen.addJumpIfR0GreaterThan(0x1b58, (short) -574);
 
-        gen.defineLabel("LABEL_421");
+        gen.defineLabel((short) -421);
         gen.addLoadImmediate(R0, 62);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("0000000000000000010128c68e23672c05010000000005dc030440c0"), "LABEL_574");
-        gen.addLoad32(R0, 90);
-        gen.addJumpIfR0Equals(0x0, "LABEL_574");
-        gen.addJumpIfR0LessThan(0xb4, "LABEL_480");
-        gen.addJumpIfR0LessThan(0x55555555, "LABEL_574");
-        gen.addJumpIfR0GreaterThan(0xffffffffL, "LABEL_574");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("0000000000000000010128c68e23672c05010000000005dc030440c0"), (short) -574);
+        gen.addLoad32intoR0(90);
+        gen.addJumpIfR0Equals(0x0, (short) -574);
+        gen.addJumpIfR0LessThan(0xb4, (short) -480);
+        gen.addJumpIfR0LessThan(0x55555555, (short) -574);
+        gen.addJumpIfR0GreaterThan(0xffffffffL, (short) -574);
 
-        gen.defineLabel("LABEL_480");
-        gen.addLoad32(R0, 94);
-        gen.addJumpIfR0LessThan(0x55555555, "LABEL_574");
-        gen.addJumpIfR0GreaterThan(0xffffffffL, "LABEL_574");
+        gen.defineLabel((short) -480);
+        gen.addLoad32intoR0(94);
+        gen.addJumpIfR0LessThan(0x55555555, (short) -574);
+        gen.addJumpIfR0GreaterThan(0xffffffffL, (short) -574);
         gen.addLoadImmediate(R0, 98);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("000000002401fa000480f000000000000000000019030000"), "LABEL_574");
-        gen.addLoad32(R0, 122);
-        gen.addJumpIfR0Equals(0x0, "LABEL_574");
-        gen.addJumpIfR0LessThan(0x78, "LABEL_547");
-        gen.addJumpIfR0LessThan(0x91e, "LABEL_574");
-        gen.addJumpIfR0GreaterThan(0x1b58, "LABEL_574");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("000000002401fa000480f000000000000000000019030000"), (short) -574);
+        gen.addLoad32intoR0(122);
+        gen.addJumpIfR0Equals(0x0, (short) -574);
+        gen.addJumpIfR0LessThan(0x78, (short) -547);
+        gen.addJumpIfR0LessThan(0x91e, (short) -574);
+        gen.addJumpIfR0GreaterThan(0x1b58, (short) -574);
 
-        gen.defineLabel("LABEL_547");
+        gen.defineLabel((short) -547);
         gen.addLoadImmediate(R0, 126);
-        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("2401fa000480f00000000000000000010701"), "LABEL_574");
+        gen.addJumpIfBytesAtR0NotEqual(hexStringToByteArray("2401fa000480f00000000000000000010701"), (short) -574);
         gen.addCountAndDrop(getCounterEnumFromOffset(-72));
 
-        gen.defineLabel("LABEL_574");
+        gen.defineLabel((short) -574);
         gen.addLoadImmediate(R1, -40);
 
         gen.addCountTrampoline();
diff --git a/tests/unit/src/android/net/apf/ApfTestHelpers.kt b/tests/unit/src/android/net/apf/ApfTestHelpers.kt
index 6a5688e..ae225eb 100644
--- a/tests/unit/src/android/net/apf/ApfTestHelpers.kt
+++ b/tests/unit/src/android/net/apf/ApfTestHelpers.kt
@@ -20,15 +20,16 @@
 import android.net.apf.ApfCounterTracker.Counter.APF_VERSION
 import android.net.apf.ApfCounterTracker.Counter.TOTAL_PACKETS
 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.ArgumentMatchers.any
 import org.mockito.Mockito.clearInvocations
 import org.mockito.Mockito.timeout
 import org.mockito.Mockito.verify
 
-class ApfTestHelpers private constructor() {
+class ApfTestHelpers(apfInterpreterVersion: Int){
+    private val apfJniUtils = ApfJniUtils(apfInterpreterVersion)
     companion object {
         const val TIMEOUT_MS: Long = 1000
         const val PASS: Int = 1
@@ -53,172 +54,6 @@
             assertEquals(label(expected), label(got))
         }
 
-        private fun assertVerdict(
-            apfVersion: Int,
-            expected: Int,
-            program: ByteArray,
-            packet: ByteArray,
-            filterAge: Int
-        ) {
-            val msg = """Unexpected APF verdict. To debug:
-                apf_run
-                    --program ${HexDump.toHexString(program)}
-                    --packet ${HexDump.toHexString(packet)}
-                    --age $filterAge
-                    ${if (apfVersion > 4) " --v6" else ""}
-                    --trace " + " | less\n
-            """
-            assertReturnCodesEqual(
-                msg,
-                expected,
-                ApfJniUtils.apfSimulate(apfVersion, program, packet, null, filterAge)
-            )
-        }
-
-        @Throws(BaseApfGenerator.IllegalInstructionException::class)
-        private fun assertVerdict(
-            apfVersion: Int,
-            expected: Int,
-            gen: ApfV4Generator,
-            packet: ByteArray,
-            filterAge: Int
-        ) {
-            assertVerdict(apfVersion, expected, gen.generate(), packet, null, filterAge)
-        }
-
-        private fun assertVerdict(
-            apfVersion: Int,
-            expected: Int,
-            program: ByteArray,
-            packet: ByteArray,
-            data: ByteArray?,
-            filterAge: Int
-        ) {
-            val msg = """Unexpected APF verdict. To debug:
-                apf_run
-                    --program ${HexDump.toHexString(program)}
-                    --packet ${HexDump.toHexString(packet)}
-                    ${if (data != null) "--data ${HexDump.toHexString(data)}" else ""}
-                    --age $filterAge
-                    ${if (apfVersion > 4) "--v6" else ""}
-                    --trace | less
-            """
-            assertReturnCodesEqual(
-                msg,
-                expected,
-                ApfJniUtils.apfSimulate(apfVersion, program, packet, data, filterAge)
-            )
-        }
-
-        /**
-         * Runs the APF program with customized data region and checks the return code.
-         */
-        fun assertVerdict(
-            apfVersion: Int,
-            expected: Int,
-            program: ByteArray,
-            packet: ByteArray,
-            data: ByteArray?
-        ) {
-            assertVerdict(apfVersion, expected, program, packet, data, filterAge = 0)
-        }
-
-        /**
-         * Runs the APF program and checks the return code is equals to expected value. If not, the
-         * customized message is printed.
-         */
-        @JvmStatic
-        fun assertVerdict(
-            apfVersion: Int,
-            msg: String,
-            expected: Int,
-            program: ByteArray?,
-            packet: ByteArray?,
-            filterAge: Int
-        ) {
-            assertReturnCodesEqual(
-                msg,
-                expected,
-                ApfJniUtils.apfSimulate(apfVersion, program, packet, null, filterAge)
-            )
-        }
-
-        /**
-         * Runs the APF program and checks the return code is equals to expected value.
-         */
-        @JvmStatic
-        fun assertVerdict(apfVersion: Int, expected: Int, program: ByteArray, packet: ByteArray) {
-            assertVerdict(apfVersion, expected, program, packet, 0)
-        }
-
-        /**
-         * Runs the APF program and checks the return code is PASS.
-         */
-        @JvmStatic
-        fun assertPass(apfVersion: Int, program: ByteArray, packet: ByteArray, filterAge: Int) {
-            assertVerdict(apfVersion, PASS, program, packet, filterAge)
-        }
-
-        /**
-         * Runs the APF program and checks the return code is PASS.
-         */
-        @JvmStatic
-        fun assertPass(apfVersion: Int, program: ByteArray, packet: ByteArray) {
-            assertVerdict(apfVersion, PASS, program, packet)
-        }
-
-        /**
-         * Runs the APF program and checks the return code is DROP.
-         */
-        @JvmStatic
-        fun assertDrop(apfVersion: Int, program: ByteArray, packet: ByteArray, filterAge: Int) {
-            assertVerdict(apfVersion, DROP, program, packet, filterAge)
-        }
-
-        /**
-         * Runs the APF program and checks the return code is DROP.
-         */
-        @JvmStatic
-        fun assertDrop(apfVersion: Int, program: ByteArray, packet: ByteArray) {
-            assertVerdict(apfVersion, DROP, program, packet)
-        }
-
-        /**
-         * Runs the APF program and checks the return code is PASS.
-         */
-        @Throws(BaseApfGenerator.IllegalInstructionException::class)
-        @JvmStatic
-        fun assertPass(apfVersion: Int, gen: ApfV4Generator, packet: ByteArray, filterAge: Int) {
-            assertVerdict(apfVersion, PASS, gen, packet, filterAge)
-        }
-
-        /**
-         * Runs the APF program and checks the return code is DROP.
-         */
-        @Throws(BaseApfGenerator.IllegalInstructionException::class)
-        @JvmStatic
-        fun assertDrop(apfVersion: Int, gen: ApfV4Generator, packet: ByteArray, filterAge: Int) {
-            assertVerdict(apfVersion, DROP, gen, packet, filterAge)
-        }
-
-        /**
-         * Runs the APF program and checks the return code is PASS.
-         */
-        @Throws(BaseApfGenerator.IllegalInstructionException::class)
-        @JvmStatic
-        fun assertPass(apfVersion: Int, gen: ApfV4Generator) {
-            assertVerdict(apfVersion, PASS, gen, ByteArray(MIN_PKT_SIZE), 0)
-        }
-
-        /**
-         * Runs the APF program and checks the return code is DROP.
-         */
-        @Throws(BaseApfGenerator.IllegalInstructionException::class)
-        @JvmStatic
-        fun assertDrop(apfVersion: Int, gen: ApfV4Generator) {
-            assertVerdict(apfVersion, DROP, gen, ByteArray(MIN_PKT_SIZE), 0)
-        }
-
         /**
          * Checks the generated APF program equals to the expected value.
          */
@@ -234,69 +69,6 @@
             }
         }
 
-        /**
-         * Runs the APF program and checks the return code and data regions
-         * equals to expected value.
-         */
-        @Throws(BaseApfGenerator.IllegalInstructionException::class, Exception::class)
-        @JvmStatic
-        fun assertDataMemoryContents(
-            apfVersion: Int,
-            expected: Int,
-            program: ByteArray?,
-            packet: ByteArray?,
-            data: ByteArray,
-            expectedData: ByteArray,
-            ignoreInterpreterVersion: Boolean
-        ) {
-            assertReturnCodesEqual(
-                expected,
-                ApfJniUtils.apfSimulate(apfVersion, program, packet, data, 0)
-            )
-
-            if (ignoreInterpreterVersion) {
-                val apfVersionIdx = (Counter.totalSize() +
-                        APF_VERSION.offset())
-                val apfProgramIdIdx = (Counter.totalSize() +
-                        APF_PROGRAM_ID.offset())
-                for (i in 0..3) {
-                    data[apfVersionIdx + i] = 0
-                    data[apfProgramIdIdx + i] = 0
-                }
-            }
-            // assertArrayEquals() would only print one byte, making debugging difficult.
-            if (!expectedData.contentEquals(data)) {
-                throw Exception(
-                    ("\nprogram:     " + HexDump.toHexString(program) +
-                     "\ndata memory: " + HexDump.toHexString(data) +
-                     "\nexpected:    " + HexDump.toHexString(expectedData))
-                )
-            }
-        }
-
-        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>()
@@ -313,22 +85,283 @@
             }
             return ret
         }
+    }
 
-        @JvmStatic
-        fun consumeInstalledProgram(
-            ipClientCb: IpClient.IpClientCallbacksWrapper,
-            installCnt: Int
-        ): ByteArray {
-            val programCaptor = ArgumentCaptor.forClass(
-                ByteArray::class.java
-            )
+    private fun assertVerdict(
+        apfVersion: Int,
+        expected: Int,
+        program: ByteArray,
+        packet: ByteArray,
+        filterAge: Int
+    ) {
+        val msg = """Unexpected APF verdict. To debug:
+                apf_run
+                    --program ${HexDump.toHexString(program)}
+                    --packet ${HexDump.toHexString(packet)}
+                    --age $filterAge
+                    ${if (apfVersion > 4) " --v6" else ""}
+                    --trace " + " | less\n
+            """
+        assertReturnCodesEqual(
+            msg,
+            expected,
+            apfJniUtils.apfSimulate(apfVersion, program, packet, null, filterAge)
+        )
+    }
 
-            verify(ipClientCb, timeout(TIMEOUT_MS).times(installCnt)).installPacketFilter(
-                programCaptor.capture()
-            )
+    @Throws(BaseApfGenerator.IllegalInstructionException::class)
+    private fun assertVerdict(
+        apfVersion: Int,
+        expected: Int,
+        gen: ApfV4Generator,
+        packet: ByteArray,
+        filterAge: Int
+    ) {
+        assertVerdict(apfVersion, expected, gen.generate(), packet, null, filterAge)
+    }
 
-            clearInvocations<Any>(ipClientCb)
-            return programCaptor.value
+    private fun assertVerdict(
+        apfVersion: Int,
+        expected: Int,
+        program: ByteArray,
+        packet: ByteArray,
+        data: ByteArray?,
+        filterAge: Int
+    ) {
+        val msg = "Unexpected APF verdict. To debug: \n" + """
+                apf_run
+                    --program ${HexDump.toHexString(program)}
+                    --packet ${HexDump.toHexString(packet)}
+                    ${if (data != null) "--data ${HexDump.toHexString(data)}" else ""}
+                    --age $filterAge
+                    ${if (apfVersion > 4) "--v6" else ""}
+                    --trace | less
+            """.replace("\n", " ").replace("\\s+".toRegex(), " ") + "\n"
+        assertReturnCodesEqual(
+            msg,
+            expected,
+            apfJniUtils.apfSimulate(apfVersion, program, packet, data, filterAge)
+        )
+    }
+
+    /**
+     * Runs the APF program with customized data region and checks the return code.
+     */
+    fun assertVerdict(
+        apfVersion: Int,
+        expected: Int,
+        program: ByteArray,
+        packet: ByteArray,
+        data: ByteArray?
+    ) {
+        assertVerdict(apfVersion, expected, program, packet, data, filterAge = 0)
+    }
+
+    /**
+     * Runs the APF program and checks the return code is equals to expected value. If not, the
+     * customized message is printed.
+     */
+    fun assertVerdict(
+        apfVersion: Int,
+        msg: String,
+        expected: Int,
+        program: ByteArray?,
+        packet: ByteArray?,
+        filterAge: Int
+    ) {
+        assertReturnCodesEqual(
+            msg,
+            expected,
+            apfJniUtils.apfSimulate(apfVersion, program, packet, null, filterAge)
+        )
+    }
+
+    /**
+     * Runs the APF program and checks the return code is equals to expected value.
+     */
+    fun assertVerdict(apfVersion: Int, expected: Int, program: ByteArray, packet: ByteArray) {
+        assertVerdict(apfVersion, expected, program, packet, 0)
+    }
+
+    /**
+     * Runs the APF program and checks the return code is PASS.
+     */
+    fun assertPass(apfVersion: Int, program: ByteArray, packet: ByteArray, filterAge: Int) {
+        assertVerdict(apfVersion, PASS, program, packet, filterAge)
+    }
+
+    /**
+     * Runs the APF program and checks the return code is PASS.
+     */
+    fun assertPass(apfVersion: Int, program: ByteArray, packet: ByteArray) {
+        assertVerdict(apfVersion, PASS, program, packet)
+    }
+
+    /**
+     * Runs the APF program and checks the return code is DROP.
+     */
+    fun assertDrop(apfVersion: Int, program: ByteArray, packet: ByteArray, filterAge: Int) {
+        assertVerdict(apfVersion, DROP, program, packet, filterAge)
+    }
+
+    /**
+     * Runs the APF program and checks the return code is DROP.
+     */
+    fun assertDrop(apfVersion: Int, program: ByteArray, packet: ByteArray) {
+        assertVerdict(apfVersion, DROP, program, packet)
+    }
+
+    /**
+     * Runs the APF program and checks the return code is PASS.
+     */
+    @Throws(BaseApfGenerator.IllegalInstructionException::class)
+    fun assertPass(apfVersion: Int, gen: ApfV4Generator, packet: ByteArray, filterAge: Int) {
+        assertVerdict(apfVersion, PASS, gen, packet, filterAge)
+    }
+
+    /**
+     * Runs the APF program and checks the return code is DROP.
+     */
+    @Throws(BaseApfGenerator.IllegalInstructionException::class)
+    fun assertDrop(apfVersion: Int, gen: ApfV4Generator, packet: ByteArray, filterAge: Int) {
+        assertVerdict(apfVersion, DROP, gen, packet, filterAge)
+    }
+
+    /**
+     * Runs the APF program and checks the return code is PASS.
+     */
+    @Throws(BaseApfGenerator.IllegalInstructionException::class)
+    fun assertPass(apfVersion: Int, gen: ApfV4Generator) {
+        assertVerdict(apfVersion, PASS, gen, ByteArray(MIN_PKT_SIZE), 0)
+    }
+
+    /**
+     * Runs the APF program and checks the return code is DROP.
+     */
+    @Throws(BaseApfGenerator.IllegalInstructionException::class)
+    fun assertDrop(apfVersion: Int, gen: ApfV4Generator) {
+        assertVerdict(apfVersion, DROP, gen, ByteArray(MIN_PKT_SIZE), 0)
+    }
+
+    /**
+     * Runs the APF program and checks the return code and data regions
+     * equals to expected value.
+     */
+    @Throws(BaseApfGenerator.IllegalInstructionException::class, Exception::class)
+    fun assertDataMemoryContents(
+        apfVersion: Int,
+        expected: Int,
+        program: ByteArray?,
+        packet: ByteArray?,
+        data: ByteArray,
+        expectedData: ByteArray,
+        ignoreInterpreterVersion: Boolean
+    ) {
+        assertReturnCodesEqual(
+            expected,
+            apfJniUtils.apfSimulate(apfVersion, program, packet, data, 0)
+        )
+
+        if (ignoreInterpreterVersion) {
+            val apfVersionIdx = (Counter.totalSize() +
+                    APF_VERSION.offset())
+            val apfProgramIdIdx = (Counter.totalSize() +
+                    APF_PROGRAM_ID.offset())
+            for (i in 0..3) {
+                data[apfVersionIdx + i] = 0
+                data[apfProgramIdIdx + i] = 0
+            }
         }
+        // assertArrayEquals() would only print one byte, making debugging difficult.
+        if (!expectedData.contentEquals(data)) {
+            throw Exception(
+                ("\nprogram:     " + HexDump.toHexString(program) +
+                        "\ndata memory: " + HexDump.toHexString(data) +
+                        "\nexpected:    " + HexDump.toHexString(expectedData))
+            )
+        }
+    }
+
+    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 consumeInstalledProgram(
+        apfController: ApfFilter.IApfController,
+        installCnt: Int
+    ): ByteArray {
+        val programCaptor = ArgumentCaptor.forClass(
+            ByteArray::class.java
+        )
+
+        verify(apfController, timeout(TIMEOUT_MS).times(installCnt)).installPacketFilter(
+            programCaptor.capture(),
+            any()
+        )
+
+        clearInvocations<Any>(apfController)
+        return programCaptor.value
+    }
+
+    fun consumeTransmittedPackets(
+        expectCnt: Int
+    ): List<ByteArray> {
+        val transmittedPackets = apfJniUtils.getAllTransmittedPackets()
+        assertEquals(expectCnt, transmittedPackets.size)
+        resetTransmittedPacketMemory()
+        return transmittedPackets
+    }
+
+    fun resetTransmittedPacketMemory() {
+        apfJniUtils.resetTransmittedPacketMemory()
+    }
+
+    fun disassembleApf(program: ByteArray): Array<String> {
+        return apfJniUtils.disassembleApf(program)
+    }
+
+    fun getAllTransmittedPackets(): List<ByteArray> {
+        return apfJniUtils.allTransmittedPackets
+    }
+
+    fun compareBpfApf(
+        apfVersion: Int,
+        filter: String,
+        pcapFilename: String,
+        apfProgram: ByteArray
+    ): Boolean {
+        return apfJniUtils.compareBpfApf(apfVersion, filter, pcapFilename, apfProgram)
+    }
+
+    fun compileToBpf(filter: String): String {
+        return apfJniUtils.compileToBpf(filter)
+    }
+
+    fun dropsAllPackets(
+        apfVersion: Int,
+        program: ByteArray,
+        data: ByteArray,
+        pcapFilename: String
+    ): Boolean {
+        return apfJniUtils.dropsAllPackets(apfVersion, program, data, pcapFilename)
     }
 }
diff --git a/tests/unit/src/android/net/apf/Bpf2Apf.java b/tests/unit/src/android/net/apf/Bpf2Apf.java
deleted file mode 100644
index 4dee2f6..0000000
--- a/tests/unit/src/android/net/apf/Bpf2Apf.java
+++ /dev/null
@@ -1,339 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net.apf;
-
-import static android.net.apf.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;
-
-import android.net.apf.BaseApfGenerator.IllegalInstructionException;
-import android.net.apf.BaseApfGenerator.Register;
-
-import java.io.BufferedReader;
-import java.io.InputStreamReader;
-
-/**
- * BPF to APF translator.
- *
- * Note: This is for testing purposes only and is not guaranteed to support
- *       translation of all BPF programs.
- *
- * Example usage:
- *   javac net/java/android/net/apf/ApfV4Generator.java \
- *         tests/servicestests/src/android/net/apf/Bpf2Apf.java
- *   sudo tcpdump -i em1 -d icmp | java -classpath tests/servicestests/src:net/java \
- *                                      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);
-        }
-        final long val_long = Long.parseLong(arg.substring(3), 16);
-        if (val_long < 0 || val_long > Long.parseLong("ffffffff", 16)) {
-            throw new IllegalArgumentException("Unhandled instruction: " + line);
-        }
-        return new Long((val_long << 32) >> 32).intValue();
-    }
-
-    private static MemorySlot byIndex(int value) {
-        switch (value) {
-            case 0: return MemorySlot.SLOT_0;
-            case 1: return MemorySlot.SLOT_1;
-            case 2: return MemorySlot.SLOT_2;
-            case 3: return MemorySlot.SLOT_3;
-            case 4: return MemorySlot.SLOT_4;
-            case 5: return MemorySlot.SLOT_5;
-            case 6: return MemorySlot.SLOT_6;
-            case 7: return MemorySlot.SLOT_7;
-        }
-        // Either < 0 or > 15 which aren't valid slot numbers,
-        // or >= Memory.FIRST_PREFILLED (ie. 8),
-        // but we don't need to check that,
-        // since we only handle valid SLOT_X numbers.
-        throw new IllegalArgumentException(
-                String.format("Memory slot %d not in range 0..7", value));
-    }
-
-    /**
-     * Convert a single line of "tcpdump -d" (human readable BPF program dump) {@code line} into
-     * APF instruction(s) and append them to {@code gen}. Here's an example line:
-     * (001) jeq      #0x86dd          jt 2    jf 7
-     */
-    private static void convertLine(String line, ApfV4Generator gen)
-            throws IllegalInstructionException {
-        if (line.indexOf("(") != 0 || line.indexOf(")") != 4 || line.indexOf(" ") != 5) {
-            throw new IllegalArgumentException("Unhandled instruction: " + line);
-        }
-        int label = Integer.parseInt(line.substring(1, 4));
-        gen.defineLabel(Integer.toString(label));
-        String opcode = line.substring(6, 10).trim();
-        String arg = line.substring(15, Math.min(32, line.length())).trim();
-        switch (opcode) {
-            case "ld":
-            case "ldh":
-            case "ldb":
-            case "ldx":
-            case "ldxb":
-            case "ldxh":
-                Register dest = opcode.contains("x") ? R1 : R0;
-                if (arg.equals("4*([14]&0xf)")) {
-                    if (!opcode.equals("ldxb")) {
-                        throw new IllegalArgumentException("Unhandled instruction: " + line);
-                    }
-                    gen.addLoadFromMemory(dest, MemorySlot.IPV4_HEADER_SIZE);
-                    break;
-                }
-                if (arg.equals("#pktlen")) {
-                    if (!opcode.equals("ld")) {
-                        throw new IllegalArgumentException("Unhandled instruction: " + line);
-                    }
-                    gen.addLoadFromMemory(dest, MemorySlot.PACKET_SIZE);
-                    break;
-                }
-                if (arg.startsWith("#0x")) {
-                    if (!opcode.equals("ld")) {
-                        throw new IllegalArgumentException("Unhandled instruction: " + line);
-                    }
-                    gen.addLoadImmediate(dest, parseImm(line, arg));
-                    break;
-                }
-                if (arg.startsWith("M[")) {
-                    if (!opcode.startsWith("ld")) {
-                        throw new IllegalArgumentException("Unhandled instruction: " + line);
-                    }
-                    int memory_slot = Integer.parseInt(arg.substring(2, arg.length() - 1));
-                    gen.addLoadFromMemory(dest, byIndex(memory_slot));
-                    break;
-                }
-                if (arg.startsWith("[x + ")) {
-                    int offset = Integer.parseInt(arg.substring(5, arg.length() - 1));
-                    switch (opcode) {
-                        case "ld":
-                        case "ldx":
-                            gen.addLoad32Indexed(dest, offset);
-                            break;
-                        case "ldh":
-                        case "ldxh":
-                            gen.addLoad16Indexed(dest, offset);
-                            break;
-                        case "ldb":
-                        case "ldxb":
-                            gen.addLoad8Indexed(dest, offset);
-                            break;
-                    }
-                } else {
-                    int offset = Integer.parseInt(arg.substring(1, arg.length() - 1));
-                    switch (opcode) {
-                        case "ld":
-                        case "ldx":
-                            gen.addLoad32(dest, offset);
-                            break;
-                        case "ldh":
-                        case "ldxh":
-                            gen.addLoad16(dest, offset);
-                            break;
-                        case "ldb":
-                        case "ldxb":
-                            gen.addLoad8(dest, offset);
-                            break;
-                    }
-                }
-                break;
-            case "st":
-            case "stx":
-                Register src = opcode.contains("x") ? R1 : R0;
-                if (!arg.startsWith("M[")) {
-                    throw new IllegalArgumentException("Unhandled instruction: " + line);
-                }
-                int memory_slot = Integer.parseInt(arg.substring(2, arg.length() - 1));
-                gen.addStoreToMemory(byIndex(memory_slot), src);
-                break;
-            case "add":
-            case "and":
-            case "or":
-            case "sub":
-                if (arg.equals("x")) {
-                    switch(opcode) {
-                        case "add":
-                            gen.addAddR1ToR0();
-                            break;
-                        case "and":
-                            gen.addAndR0WithR1();
-                            break;
-                        case "or":
-                            gen.addOrR0WithR1();
-                            break;
-                        case "sub":
-                            gen.addNeg(R1);
-                            gen.addAddR1ToR0();
-                            gen.addNeg(R1);
-                            break;
-                    }
-                } else {
-                    int imm = parseImm(line, arg);
-                    switch(opcode) {
-                        case "add":
-                            gen.addAdd(imm);
-                            break;
-                        case "and":
-                            gen.addAnd(imm);
-                            break;
-                        case "or":
-                            gen.addOr(imm);
-                            break;
-                        case "sub":
-                            gen.addAdd(-imm);
-                            break;
-                    }
-                }
-                break;
-            case "jeq":
-            case "jset":
-            case "jgt":
-            case "jge":
-                int val = 0;
-                boolean reg_compare;
-                if (arg.startsWith("x")) {
-                    reg_compare = true;
-                } else {
-                    reg_compare = false;
-                    val = parseImm(line, arg);
-                }
-                int jt_offset = line.indexOf("jt");
-                int jf_offset = line.indexOf("jf");
-                String true_label = line.substring(jt_offset + 2, jf_offset).trim();
-                String false_label = line.substring(jf_offset + 2).trim();
-                boolean true_label_is_fallthrough = Integer.parseInt(true_label) == label + 1;
-                boolean false_label_is_fallthrough = Integer.parseInt(false_label) == label + 1;
-                if (true_label_is_fallthrough && false_label_is_fallthrough)
-                    break;
-                switch (opcode) {
-                    case "jeq":
-                        if (!true_label_is_fallthrough) {
-                            if (reg_compare) {
-                                gen.addJumpIfR0EqualsR1(true_label);
-                            } else {
-                                gen.addJumpIfR0Equals(val, true_label);
-                            }
-                        }
-                        if (!false_label_is_fallthrough) {
-                            if (!true_label_is_fallthrough) {
-                                gen.addJump(false_label);
-                            } else if (reg_compare) {
-                                gen.addJumpIfR0NotEqualsR1(false_label);
-                            } else {
-                                gen.addJumpIfR0NotEquals(val, false_label);
-                            }
-                        }
-                        break;
-                    case "jset":
-                        if (reg_compare) {
-                            gen.addJumpIfR0AnyBitsSetR1(true_label);
-                        } else {
-                            gen.addJumpIfR0AnyBitsSet(val, true_label);
-                        }
-                        if (!false_label_is_fallthrough) {
-                            gen.addJump(false_label);
-                        }
-                        break;
-                    case "jgt":
-                        if (!true_label_is_fallthrough ||
-                                // We have no less-than-or-equal-to register to register
-                                // comparison instruction, so in this case we'll jump
-                                // around an unconditional jump.
-                                (!false_label_is_fallthrough && reg_compare)) {
-                            if (reg_compare) {
-                                gen.addJumpIfR0GreaterThanR1(true_label);
-                            } else {
-                                gen.addJumpIfR0GreaterThan(val, true_label);
-                            }
-                        }
-                        if (!false_label_is_fallthrough) {
-                            if (!true_label_is_fallthrough || reg_compare) {
-                                gen.addJump(false_label);
-                            } else {
-                                gen.addJumpIfR0LessThan(val + 1, false_label);
-                            }
-                        }
-                        break;
-                    case "jge":
-                        if (!false_label_is_fallthrough ||
-                                // We have no greater-than-or-equal-to register to register
-                                // comparison instruction, so in this case we'll jump
-                                // around an unconditional jump.
-                                (!true_label_is_fallthrough && reg_compare)) {
-                            if (reg_compare) {
-                                gen.addJumpIfR0LessThanR1(false_label);
-                            } else {
-                                gen.addJumpIfR0LessThan(val, false_label);
-                            }
-                        }
-                        if (!true_label_is_fallthrough) {
-                            if (!false_label_is_fallthrough || reg_compare) {
-                                gen.addJump(true_label);
-                            } else {
-                                gen.addJumpIfR0GreaterThan(val - 1, true_label);
-                            }
-                        }
-                        break;
-                }
-                break;
-            case "ret":
-                if (arg.equals("#0")) {
-                    gen.addJump(gen.DROP_LABEL);
-                } else {
-                    gen.addJump(gen.PASS_LABEL);
-                }
-                break;
-            case "tax":
-                gen.addMove(R1);
-                break;
-            case "txa":
-                gen.addMove(R0);
-                break;
-            default:
-                throw new IllegalArgumentException("Unhandled instruction: " + line);
-        }
-    }
-
-    /**
-     * Convert the output of "tcpdump -d" (human readable BPF program dump) {@code bpf} into an APF
-     * program and return it.
-     */
-    public static byte[] convert(String bpf) throws IllegalInstructionException {
-        ApfV4Generator gen = new ApfV4Generator(APF_VERSION_3, sRamSize, sClampSize);
-        for (String line : bpf.split("\\n")) convertLine(line, gen);
-        return gen.generate();
-    }
-
-    /**
-     * Convert the output of "tcpdump -d" (human readable BPF program dump) piped in stdin into an
-     * APF program and output it via stdout.
-     */
-    public static void main(String[] args) throws Exception {
-        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
-        String line = null;
-        StringBuilder responseData = new StringBuilder();
-        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/JumpTableTest.kt b/tests/unit/src/android/net/apf/JumpTableTest.kt
deleted file mode 100644
index 066c34a..0000000
--- a/tests/unit/src/android/net/apf/JumpTableTest.kt
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright (C) 2023 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.BaseApfGenerator.MemorySlot
-import android.net.apf.BaseApfGenerator.Register.R0
-import androidx.test.filters.SmallTest
-import androidx.test.runner.AndroidJUnit4
-import com.android.testutils.assertThrows
-import java.util.NoSuchElementException
-import java.util.concurrent.atomic.AtomicReference
-import kotlin.test.assertEquals
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.inOrder
-import org.mockito.MockitoAnnotations
-
-@RunWith(AndroidJUnit4::class)
-@SmallTest
-class JumpTableTest {
-
-    @Mock
-    lateinit var gen: ApfV4Generator
-
-    @Before
-    fun setUp() {
-        MockitoAnnotations.initMocks(this)
-    }
-
-    @Test(expected = NullPointerException::class)
-    fun testNullStartLabel() {
-        // Can't use "null" because the method is @NonNull.
-        JumpTable(AtomicReference<String>(null).get(), MemorySlot.SLOT_0)
-    }
-
-    @Test(expected = IllegalArgumentException::class)
-    fun testSlotTooLarge() {
-        JumpTable("my_jump_table", MemorySlot.IPV4_HEADER_SIZE)
-    }
-
-    @Test
-    fun testValidSlotNumbers() {
-        JumpTable("my_jump_table", MemorySlot.SLOT_1)
-        JumpTable("my_jump_table", MemorySlot.SLOT_4)
-        JumpTable("my_jump_table", MemorySlot.SLOT_6)
-    }
-
-    @Test
-    fun testGetStartLabel() {
-        assertEquals("xyz", JumpTable("xyz", MemorySlot.SLOT_3).startLabel)
-        assertEquals("abc", JumpTable("abc", MemorySlot.SLOT_5).startLabel)
-    }
-
-    @Test
-    fun testCodeGeneration() {
-        val name = "my_jump_table"
-        val slot = MemorySlot.SLOT_7
-
-        val j = JumpTable(name, slot)
-        j.addLabel("foo")
-        j.addLabel("bar")
-        j.addLabel("bar")
-        j.addLabel("baz")
-
-        assertEquals(0, j.getIndex("foo"))
-        assertEquals(1, j.getIndex("bar"))
-        assertEquals(2, j.getIndex("baz"))
-
-        assertThrows(NoSuchElementException::class.java) {
-            j.getIndex("nonexistent")
-        }
-
-        val inOrder = inOrder(gen)
-
-        j.generate(gen)
-
-        inOrder.verify(gen).defineLabel(name)
-        inOrder.verify(gen).addLoadFromMemory(R0, slot)
-        inOrder.verify(gen).addJumpIfR0Equals(0, "foo")
-        inOrder.verify(gen).addJumpIfR0Equals(1, "bar")
-        inOrder.verify(gen).addJumpIfR0Equals(2, "baz")
-        inOrder.verify(gen).addJump(ApfV4Generator.PASS_LABEL)
-        inOrder.verifyNoMoreInteractions()
-    }
-}
diff --git a/tests/unit/src/android/net/apf/LegacyApfTest.java b/tests/unit/src/android/net/apf/LegacyApfTest.java
deleted file mode 100644
index 319a997..0000000
--- a/tests/unit/src/android/net/apf/LegacyApfTest.java
+++ /dev/null
@@ -1,2045 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.net.apf;
-
-import static android.net.apf.ApfJniUtils.dropsAllPackets;
-import static android.net.apf.ApfTestHelpers.TIMEOUT_MS;
-import static android.system.OsConstants.AF_UNIX;
-import static android.net.apf.ApfTestHelpers.DROP;
-import static android.net.apf.ApfTestHelpers.PASS;
-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;
-
-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.any;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.content.Context;
-import android.net.IpPrefix;
-import android.net.LinkAddress;
-import android.net.LinkProperties;
-import android.net.NattKeepalivePacketDataParcelable;
-import android.net.TcpKeepalivePacketDataParcelable;
-import android.net.apf.ApfCounterTracker.Counter;
-import android.net.apf.ApfFilter.ApfConfiguration;
-import android.net.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;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SmallTest;
-
-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.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;
-import com.android.server.networkstack.tests.R;
-import com.android.testutils.ConcurrentUtils;
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
-
-import libcore.io.IoUtils;
-import libcore.io.Streams;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
-
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileDescriptor;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.InetAddress;
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.Random;
-
-/**
- * Tests for APF program generator and interpreter.
- *
- * The test cases will be executed by both APFv4 and APFv6 interpreter.
- */
-@DevSdkIgnoreRunner.MonitorThreadLeak
-@RunWith(DevSdkIgnoreRunner.class)
-@SmallTest
-public class LegacyApfTest {
-    private static final int APF_VERSION_2 = 2;
-
-    @Rule
-    public DevSdkIgnoreRule mDevSdkIgnoreRule = new DevSdkIgnoreRule();
-    // Indicates which apf interpreter to run.
-    @Parameterized.Parameter()
-    public int mApfVersion;
-
-    @Parameterized.Parameters
-    public static Iterable<? extends Object> data() {
-        return Arrays.asList(4, 6);
-    }
-
-    @Mock private Context mContext;
-    @Mock
-    private ApfFilter.Dependencies mDependencies;
-    @Mock private PowerManager mPowerManager;
-    @Mock private IpConnectivityLog mIpConnectivityLog;
-    @Mock private NetworkQuirkMetrics mNetworkQuirkMetrics;
-    @Mock private ApfSessionInfoMetrics mApfSessionInfoMetrics;
-    @Mock private IpClientRaInfoMetrics mIpClientRaInfoMetrics;
-    @Mock private LegacyApfFilter.Clock mClock;
-    @GuardedBy("mApfFilterCreated")
-    private final ArrayList<AndroidPacketFilter> mApfFilterCreated = new ArrayList<>();
-    @GuardedBy("mThreadsToBeCleared")
-    private final ArrayList<Thread> mThreadsToBeCleared = new ArrayList<>();
-
-    @Before
-    public void setUp() throws Exception {
-        MockitoAnnotations.initMocks(this);
-        doReturn(mPowerManager).when(mContext).getSystemService(PowerManager.class);
-        doReturn(mApfSessionInfoMetrics).when(mDependencies).getApfSessionInfoMetrics();
-        doReturn(mIpClientRaInfoMetrics).when(mDependencies).getIpClientRaInfoMetrics();
-        doAnswer((invocation) -> {
-            synchronized (mApfFilterCreated) {
-                mApfFilterCreated.add(invocation.getArgument(0));
-            }
-            return null;
-        }).when(mDependencies).onApfFilterCreated(any());
-        doAnswer((invocation) -> {
-            synchronized (mThreadsToBeCleared) {
-                mThreadsToBeCleared.add(invocation.getArgument(0));
-            }
-            return null;
-        }).when(mDependencies).onThreadCreated(any());
-    }
-
-    private void quitThreads() throws Exception {
-        ConcurrentUtils.quitThreads(
-                THREAD_QUIT_MAX_RETRY_COUNT,
-                false /* interrupt */,
-                HANDLER_TIMEOUT_MS,
-                () -> {
-                    synchronized (mThreadsToBeCleared) {
-                        final ArrayList<Thread> ret = new ArrayList<>(mThreadsToBeCleared);
-                        mThreadsToBeCleared.clear();
-                        return ret;
-                    }
-                });
-    }
-
-    private void shutdownApfFilters() throws Exception {
-        ConcurrentUtils.quitResources(THREAD_QUIT_MAX_RETRY_COUNT, () -> {
-            synchronized (mApfFilterCreated) {
-                final ArrayList<AndroidPacketFilter> ret =
-                        new ArrayList<>(mApfFilterCreated);
-                mApfFilterCreated.clear();
-                return ret;
-            }
-        }, (apf) -> {
-            apf.shutdown();
-        });
-        synchronized (mApfFilterCreated) {
-            assertEquals("ApfFilters did not fully shutdown.",
-                    0, mApfFilterCreated.size());
-        }
-        // It's necessary to wait until all ReceiveThreads have finished running because
-        // clearInlineMocks clears all Mock objects, including some privilege frameworks
-        // required by logStats, at the end of ReceiveThread#run.
-        quitThreads();
-    }
-
-    @After
-    public void tearDown() throws Exception {
-        shutdownApfFilters();
-        // Clear mocks to prevent from stubs holding instances and cause memory leaks.
-        Mockito.framework().clearInlineMocks();
-    }
-
-    private static final String TAG = "ApfTest";
-
-    private static final boolean DROP_MULTICAST = true;
-    private static final boolean ALLOW_MULTICAST = false;
-
-    private static final boolean DROP_802_3_FRAMES = true;
-    private static final boolean ALLOW_802_3_FRAMES = false;
-
-    private static final int MIN_RDNSS_LIFETIME_SEC = 0;
-    private static final int MIN_METRICS_SESSION_DURATIONS_MS = 300_000;
-
-    private static final int HANDLER_TIMEOUT_MS = 1000;
-    private static final int THREAD_QUIT_MAX_RETRY_COUNT = 3;
-
-    // Constants for opcode encoding
-    private static final byte LI_OP   = (byte)(13 << 3);
-    private static final byte LDDW_OP = (byte)(22 << 3);
-    private static final byte STDW_OP = (byte)(23 << 3);
-    private static final byte SIZE0   = (byte)(0 << 1);
-    private static final byte SIZE8   = (byte)(1 << 1);
-    private static final byte SIZE16  = (byte)(2 << 1);
-    private static final byte SIZE32  = (byte)(3 << 1);
-    private static final byte R1_REG = 1;
-
-    private static ApfConfiguration getDefaultConfig() {
-        ApfFilter.ApfConfiguration config = new ApfConfiguration();
-        config.apfVersionSupported = 2;
-        config.apfRamSize = 4096;
-        config.multicastFilter = ALLOW_MULTICAST;
-        config.ieee802_3Filter = ALLOW_802_3_FRAMES;
-        config.ethTypeBlackList = new int[0];
-        config.minRdnssLifetimeSec = MIN_RDNSS_LIFETIME_SEC;
-        config.minRdnssLifetimeSec = 67;
-        config.minMetricsSessionDurationMs = MIN_METRICS_SESSION_DURATIONS_MS;
-        return config;
-    }
-
-    private void assertPass(ApfV4Generator gen) throws ApfV4Generator.IllegalInstructionException {
-        ApfTestHelpers.assertPass(mApfVersion, gen);
-    }
-
-    private void assertDrop(ApfV4Generator gen) throws ApfV4Generator.IllegalInstructionException {
-        ApfTestHelpers.assertDrop(mApfVersion, gen);
-    }
-
-    private void assertPass(byte[] program, byte[] packet) {
-        ApfTestHelpers.assertPass(mApfVersion, program, packet);
-    }
-
-    private void assertDrop(byte[] program, byte[] packet) {
-        ApfTestHelpers.assertDrop(mApfVersion, program, packet);
-    }
-
-    private void assertPass(byte[] program, byte[] packet, int filterAge) {
-        ApfTestHelpers.assertPass(mApfVersion, program, packet, filterAge);
-    }
-
-    private void assertDrop(byte[] program, byte[] packet, int filterAge) {
-        ApfTestHelpers.assertDrop(mApfVersion, program, packet, filterAge);
-    }
-
-    private void assertPass(ApfV4Generator gen, byte[] packet, int filterAge)
-            throws ApfV4Generator.IllegalInstructionException {
-        ApfTestHelpers.assertPass(mApfVersion, gen, packet, filterAge);
-    }
-
-    private void assertDrop(ApfV4Generator gen, byte[] packet, int filterAge)
-            throws ApfV4Generator.IllegalInstructionException {
-        ApfTestHelpers.assertDrop(mApfVersion, gen, packet, filterAge);
-    }
-
-    private void assertDataMemoryContents(int expected, byte[] program, byte[] packet,
-            byte[] data, byte[] expectedData) throws Exception {
-        ApfTestHelpers.assertDataMemoryContents(mApfVersion, expected, program, packet, data,
-                expectedData, false /* ignoreInterpreterVersion */);
-    }
-
-    private void assertDataMemoryContentsIgnoreVersion(int expected, byte[] program,
-            byte[] packet, byte[] data, byte[] expectedData) throws Exception {
-        ApfTestHelpers.assertDataMemoryContents(mApfVersion, expected, program, packet, data,
-                expectedData, true /* ignoreInterpreterVersion */);
-    }
-
-    private void assertVerdict(String msg, int expected, byte[] program,
-            byte[] packet, int filterAge) {
-        ApfTestHelpers.assertVerdict(mApfVersion, msg, expected, program, packet, filterAge);
-    }
-
-    private void assertVerdict(int expected, byte[] program, byte[] packet) {
-        ApfTestHelpers.assertVerdict(mApfVersion, expected, program, packet);
-    }
-
-    /**
-     * Generate APF program, run pcap file though APF filter, then check all the packets in the file
-     * should be dropped.
-     */
-    @Test
-    public void testApfFilterPcapFile() throws Exception {
-        final byte[] MOCK_PCAP_IPV4_ADDR = {(byte) 172, 16, 7, (byte) 151};
-        String pcapFilename = stageFile(R.raw.apfPcap);
-        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
-        LinkAddress link = new LinkAddress(InetAddress.getByAddress(MOCK_PCAP_IPV4_ADDR), 16);
-        LinkProperties lp = new LinkProperties();
-        lp.addLinkAddress(link);
-
-        ApfConfiguration config = getDefaultConfig();
-        config.apfVersionSupported = 4;
-        config.apfRamSize = 1700;
-        config.multicastFilter = DROP_MULTICAST;
-        config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestLegacyApfFilter apfFilter = new TestLegacyApfFilter(mContext, config, ipClientCallback,
-                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
-        apfFilter.setLinkProperties(lp);
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
-        byte[] data = new byte[Counter.totalSize()];
-        final boolean result;
-
-        result = dropsAllPackets(mApfVersion, program, data, pcapFilename);
-        Log.i(TAG, "testApfFilterPcapFile(): Data counters: " + HexDump.toHexString(data, false));
-
-        assertTrue("Failed to drop all packets by filter. \nAPF counters:" +
-            HexDump.toHexString(data, false), result);
-    }
-
-    private static final int ETH_HEADER_LEN               = 14;
-    private static final int ETH_DEST_ADDR_OFFSET         = 0;
-    private static final int ETH_ETHERTYPE_OFFSET         = 12;
-    private static final byte[] ETH_BROADCAST_MAC_ADDRESS =
-            {(byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff };
-    private static final byte[] ETH_MULTICAST_MDNS_v4_MAC_ADDRESS =
-            {(byte) 0x01, (byte) 0x00, (byte) 0x5e, (byte) 0x00, (byte) 0x00, (byte) 0xfb};
-    private static final byte[] ETH_MULTICAST_MDNS_V6_MAC_ADDRESS =
-            {(byte) 0x33, (byte) 0x33, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xfb};
-
-    private static final int IP_HEADER_OFFSET = ETH_HEADER_LEN;
-
-    private static final int IPV4_HEADER_LEN          = 20;
-    private static final int IPV4_TOTAL_LENGTH_OFFSET = IP_HEADER_OFFSET + 2;
-    private static final int IPV4_PROTOCOL_OFFSET     = IP_HEADER_OFFSET + 9;
-    private static final int IPV4_SRC_ADDR_OFFSET     = IP_HEADER_OFFSET + 12;
-    private static final int IPV4_DEST_ADDR_OFFSET    = IP_HEADER_OFFSET + 16;
-
-    private static final int IPV4_TCP_HEADER_LEN           = 20;
-    private static final int IPV4_TCP_HEADER_OFFSET        = IP_HEADER_OFFSET + IPV4_HEADER_LEN;
-    private static final int IPV4_TCP_SRC_PORT_OFFSET      = IPV4_TCP_HEADER_OFFSET + 0;
-    private static final int IPV4_TCP_DEST_PORT_OFFSET     = IPV4_TCP_HEADER_OFFSET + 2;
-    private static final int IPV4_TCP_SEQ_NUM_OFFSET       = IPV4_TCP_HEADER_OFFSET + 4;
-    private static final int IPV4_TCP_ACK_NUM_OFFSET       = IPV4_TCP_HEADER_OFFSET + 8;
-    private static final int IPV4_TCP_HEADER_LENGTH_OFFSET = IPV4_TCP_HEADER_OFFSET + 12;
-    private static final int IPV4_TCP_HEADER_FLAG_OFFSET   = IPV4_TCP_HEADER_OFFSET + 13;
-
-    private static final int IPV4_UDP_HEADER_OFFSET    = IP_HEADER_OFFSET + IPV4_HEADER_LEN;
-    private static final int IPV4_UDP_SRC_PORT_OFFSET  = IPV4_UDP_HEADER_OFFSET + 0;
-    private static final int IPV4_UDP_DEST_PORT_OFFSET = IPV4_UDP_HEADER_OFFSET + 2;
-    private static final int IPV4_UDP_LENGTH_OFFSET    = IPV4_UDP_HEADER_OFFSET + 4;
-    private static final int IPV4_UDP_PAYLOAD_OFFSET   = IPV4_UDP_HEADER_OFFSET + 8;
-    private static final byte[] IPV4_BROADCAST_ADDRESS =
-            {(byte) 255, (byte) 255, (byte) 255, (byte) 255};
-
-    private static final int IPV6_HEADER_LEN             = 40;
-    private static final int IPV6_PAYLOAD_LENGTH_OFFSET  = IP_HEADER_OFFSET + 4;
-    private static final int IPV6_NEXT_HEADER_OFFSET     = IP_HEADER_OFFSET + 6;
-    private static final int IPV6_SRC_ADDR_OFFSET        = IP_HEADER_OFFSET + 8;
-    private static final int IPV6_DEST_ADDR_OFFSET       = IP_HEADER_OFFSET + 24;
-    private static final int IPV6_PAYLOAD_OFFSET = IP_HEADER_OFFSET + IPV6_HEADER_LEN;
-    private static final int IPV6_TCP_SRC_PORT_OFFSET    = IPV6_PAYLOAD_OFFSET + 0;
-    private static final int IPV6_TCP_DEST_PORT_OFFSET   = IPV6_PAYLOAD_OFFSET + 2;
-    private static final int IPV6_TCP_SEQ_NUM_OFFSET     = IPV6_PAYLOAD_OFFSET + 4;
-    private static final int IPV6_TCP_ACK_NUM_OFFSET     = IPV6_PAYLOAD_OFFSET + 8;
-    // The IPv6 all nodes address ff02::1
-    private static final byte[] IPV6_ALL_NODES_ADDRESS   =
-            { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
-    private static final byte[] IPV6_ALL_ROUTERS_ADDRESS =
-            { (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 };
-    private static final byte[] IPV6_SOLICITED_NODE_MULTICAST_ADDRESS = {
-            (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
-            (byte) 0xff, (byte) 0xab, (byte) 0xcd, (byte) 0xef,
-    };
-
-    private static final int ICMP6_TYPE_OFFSET           = IP_HEADER_OFFSET + IPV6_HEADER_LEN;
-    private static final int ICMP6_ROUTER_SOLICITATION   = 133;
-    private static final int ICMP6_ROUTER_ADVERTISEMENT  = 134;
-    private static final int ICMP6_NEIGHBOR_SOLICITATION = 135;
-    private static final int ICMP6_NEIGHBOR_ANNOUNCEMENT = 136;
-
-    private static final int ICMP6_RA_HEADER_LEN = 16;
-    private static final int ICMP6_RA_CHECKSUM_OFFSET =
-            IP_HEADER_OFFSET + IPV6_HEADER_LEN + 2;
-    private static final int ICMP6_RA_ROUTER_LIFETIME_OFFSET =
-            IP_HEADER_OFFSET + IPV6_HEADER_LEN + 6;
-    private static final int ICMP6_RA_REACHABLE_TIME_OFFSET =
-            IP_HEADER_OFFSET + IPV6_HEADER_LEN + 8;
-    private static final int ICMP6_RA_RETRANSMISSION_TIMER_OFFSET =
-            IP_HEADER_OFFSET + IPV6_HEADER_LEN + 12;
-    private static final int ICMP6_RA_OPTION_OFFSET =
-            IP_HEADER_OFFSET + IPV6_HEADER_LEN + ICMP6_RA_HEADER_LEN;
-
-    private static final int ICMP6_PREFIX_OPTION_TYPE                      = 3;
-    private static final int ICMP6_PREFIX_OPTION_LEN                       = 32;
-    private static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET     = 4;
-    private static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET = 8;
-
-    // From RFC6106: Recursive DNS Server option
-    private static final int ICMP6_RDNSS_OPTION_TYPE = 25;
-    // From RFC6106: DNS Search List option
-    private static final int ICMP6_DNSSL_OPTION_TYPE = 31;
-
-    // From RFC4191: Route Information option
-    private static final int ICMP6_ROUTE_INFO_OPTION_TYPE = 24;
-    // Above three options all have the same format:
-    private static final int ICMP6_4_BYTE_OPTION_LEN      = 8;
-    private static final int ICMP6_4_BYTE_LIFETIME_OFFSET = 4;
-    private static final int ICMP6_4_BYTE_LIFETIME_LEN    = 4;
-
-    private static final int UDP_HEADER_LEN              = 8;
-    private static final int UDP_DESTINATION_PORT_OFFSET = ETH_HEADER_LEN + 22;
-
-    private static final int DHCP_CLIENT_PORT       = 68;
-    private static final int DHCP_CLIENT_MAC_OFFSET = ETH_HEADER_LEN + UDP_HEADER_LEN + 48;
-
-    private static final int ARP_HEADER_OFFSET          = ETH_HEADER_LEN;
-    private static final byte[] ARP_IPV4_REQUEST_HEADER = {
-            0, 1, // Hardware type: Ethernet (1)
-            8, 0, // Protocol type: IP (0x0800)
-            6,    // Hardware size: 6
-            4,    // Protocol size: 4
-            0, 1  // Opcode: request (1)
-    };
-    private static final byte[] ARP_IPV4_REPLY_HEADER = {
-            0, 1, // Hardware type: Ethernet (1)
-            8, 0, // Protocol type: IP (0x0800)
-            6,    // Hardware size: 6
-            4,    // Protocol size: 4
-            0, 2  // Opcode: reply (2)
-    };
-    private static final int ARP_SOURCE_IP_ADDRESS_OFFSET = ARP_HEADER_OFFSET + 14;
-    private static final int ARP_TARGET_IP_ADDRESS_OFFSET = ARP_HEADER_OFFSET + 24;
-
-    private static final byte[] MOCK_IPV4_ADDR           = {10, 0, 0, 1};
-    private static final byte[] MOCK_BROADCAST_IPV4_ADDR = {10, 0, 31, (byte) 255}; // prefix = 19
-    private static final byte[] MOCK_MULTICAST_IPV4_ADDR = {(byte) 224, 0, 0, 1};
-    private static final byte[] ANOTHER_IPV4_ADDR        = {10, 0, 0, 2};
-    private static final byte[] IPV4_SOURCE_ADDR         = {10, 0, 0, 3};
-    private static final byte[] ANOTHER_IPV4_SOURCE_ADDR = {(byte) 192, 0, 2, 1};
-    private static final byte[] BUG_PROBE_SOURCE_ADDR1   = {0, 0, 1, 2};
-    private static final byte[] BUG_PROBE_SOURCE_ADDR2   = {3, 4, 0, 0};
-    private static final byte[] IPV4_ANY_HOST_ADDR       = {0, 0, 0, 0};
-    private static final byte[] IPV4_MDNS_MULTICAST_ADDR = {(byte) 224, 0, 0, (byte) 251};
-    private static final byte[] IPV6_MDNS_MULTICAST_ADDR =
-            {(byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (byte) 0xfb};
-    private static final int IPV6_UDP_DEST_PORT_OFFSET = IPV6_PAYLOAD_OFFSET + 2;
-    private static final int MDNS_UDP_PORT = 5353;
-
-    private static void setIpv4VersionFields(ByteBuffer packet) {
-        packet.putShort(ETH_ETHERTYPE_OFFSET, (short) ETH_P_IP);
-        packet.put(IP_HEADER_OFFSET, (byte) 0x45);
-    }
-
-    private static void setIpv6VersionFields(ByteBuffer packet) {
-        packet.putShort(ETH_ETHERTYPE_OFFSET, (short) ETH_P_IPV6);
-        packet.put(IP_HEADER_OFFSET, (byte) 0x60);
-    }
-
-    private static ByteBuffer makeIpv4Packet(int proto) {
-        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
-        setIpv4VersionFields(packet);
-        packet.put(IPV4_PROTOCOL_OFFSET, (byte) proto);
-        return packet;
-    }
-
-    private static ByteBuffer makeIpv6Packet(int nextHeader) {
-        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
-        setIpv6VersionFields(packet);
-        packet.put(IPV6_NEXT_HEADER_OFFSET, (byte) nextHeader);
-        return packet;
-    }
-
-    @Test
-    public void testApfFilterIPv4() throws Exception {
-        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
-        LinkAddress link = new LinkAddress(InetAddress.getByAddress(MOCK_IPV4_ADDR), 19);
-        LinkProperties lp = new LinkProperties();
-        lp.addLinkAddress(link);
-
-        ApfConfiguration config = getDefaultConfig();
-        config.multicastFilter = DROP_MULTICAST;
-        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]);
-        // 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, 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());
-
-        // Verify L2 unicast to IPv4 broadcast addresses is dropped (b/30231088)
-        put(packet, IPV4_DEST_ADDR_OFFSET, IPV4_BROADCAST_ADDRESS);
-        assertDrop(program, packet.array());
-        put(packet, IPV4_DEST_ADDR_OFFSET, MOCK_BROADCAST_IPV4_ADDR);
-        assertDrop(program, packet.array());
-
-        // Verify multicast/broadcast IPv4, not DHCP to us, is dropped
-        put(packet, ETH_DEST_ADDR_OFFSET, ETH_BROADCAST_MAC_ADDRESS);
-        assertDrop(program, packet.array());
-        packet.put(IP_HEADER_OFFSET, (byte) 0x45);
-        assertDrop(program, packet.array());
-        packet.put(IPV4_PROTOCOL_OFFSET, (byte)IPPROTO_UDP);
-        assertDrop(program, packet.array());
-        packet.putShort(UDP_DESTINATION_PORT_OFFSET, (short)DHCP_CLIENT_PORT);
-        assertDrop(program, packet.array());
-        put(packet, IPV4_DEST_ADDR_OFFSET, MOCK_MULTICAST_IPV4_ADDR);
-        assertDrop(program, packet.array());
-        put(packet, IPV4_DEST_ADDR_OFFSET, MOCK_BROADCAST_IPV4_ADDR);
-        assertDrop(program, packet.array());
-        put(packet, IPV4_DEST_ADDR_OFFSET, IPV4_BROADCAST_ADDRESS);
-        assertDrop(program, packet.array());
-
-        // Verify broadcast IPv4 DHCP to us is passed
-        put(packet, DHCP_CLIENT_MAC_OFFSET, TestLegacyApfFilter.MOCK_MAC_ADDR);
-        assertPass(program, packet.array());
-
-        // Verify unicast IPv4 DHCP to us is passed
-        put(packet, ETH_DEST_ADDR_OFFSET, TestLegacyApfFilter.MOCK_MAC_ADDR);
-        assertPass(program, packet.array());
-    }
-
-    @Test
-    public void testApfFilterIPv6() throws Exception {
-        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
-        ApfConfiguration config = getDefaultConfig();
-        TestLegacyApfFilter apfFilter = new TestLegacyApfFilter(mContext, config, ipClientCallback,
-                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
-
-        // Verify empty IPv6 packet is passed
-        ByteBuffer packet = makeIpv6Packet(IPPROTO_UDP);
-        assertPass(program, packet.array());
-
-        // Verify empty ICMPv6 packet is passed
-        packet.put(IPV6_NEXT_HEADER_OFFSET, (byte)IPPROTO_ICMPV6);
-        assertPass(program, packet.array());
-
-        // Verify empty ICMPv6 NA packet is passed
-        packet.put(ICMP6_TYPE_OFFSET, (byte)ICMP6_NEIGHBOR_ANNOUNCEMENT);
-        assertPass(program, packet.array());
-
-        // Verify ICMPv6 NA to ff02::1 is dropped
-        put(packet, IPV6_DEST_ADDR_OFFSET, IPV6_ALL_NODES_ADDRESS);
-        assertDrop(program, packet.array());
-
-        // Verify ICMPv6 NA to ff02::2 is dropped
-        put(packet, IPV6_DEST_ADDR_OFFSET, IPV6_ALL_ROUTERS_ADDRESS);
-        assertDrop(program, packet.array());
-
-        // Verify ICMPv6 NA to Solicited-Node Multicast is passed
-        put(packet, IPV6_DEST_ADDR_OFFSET, IPV6_SOLICITED_NODE_MULTICAST_ADDRESS);
-        assertPass(program, packet.array());
-
-        // Verify ICMPv6 RS to any is dropped
-        packet.put(ICMP6_TYPE_OFFSET, (byte)ICMP6_ROUTER_SOLICITATION);
-        assertDrop(program, packet.array());
-        put(packet, IPV6_DEST_ADDR_OFFSET, IPV6_ALL_ROUTERS_ADDRESS);
-        assertDrop(program, packet.array());
-    }
-
-    @Test
-    public void testApfFilterMulticast() throws Exception {
-        final byte[] unicastIpv4Addr   = {(byte)192,0,2,63};
-        final byte[] broadcastIpv4Addr = {(byte)192,0,2,(byte)255};
-        final byte[] multicastIpv4Addr = {(byte)224,0,0,1};
-        final byte[] multicastIpv6Addr = {(byte)0xff,2,0,0,0,0,0,0,0,0,0,0,0,0,0,(byte)0xfb};
-
-        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
-        LinkAddress link = new LinkAddress(InetAddress.getByAddress(unicastIpv4Addr), 24);
-        LinkProperties lp = new LinkProperties();
-        lp.addLinkAddress(link);
-
-        ApfConfiguration config = getDefaultConfig();
-        config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestLegacyApfFilter apfFilter = new TestLegacyApfFilter(mContext, config, ipClientCallback,
-                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
-        apfFilter.setLinkProperties(lp);
-
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
-
-        // Construct IPv4 and IPv6 multicast packets.
-        ByteBuffer mcastv4packet = makeIpv4Packet(IPPROTO_UDP);
-        put(mcastv4packet, IPV4_DEST_ADDR_OFFSET, multicastIpv4Addr);
-
-        ByteBuffer mcastv6packet = makeIpv6Packet(IPPROTO_UDP);
-        put(mcastv6packet, IPV6_DEST_ADDR_OFFSET, multicastIpv6Addr);
-
-        // Construct IPv4 broadcast packet.
-        ByteBuffer bcastv4packet1 = makeIpv4Packet(IPPROTO_UDP);
-        bcastv4packet1.put(ETH_BROADCAST_MAC_ADDRESS);
-        bcastv4packet1.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP);
-        put(bcastv4packet1, IPV4_DEST_ADDR_OFFSET, multicastIpv4Addr);
-
-        ByteBuffer bcastv4packet2 = makeIpv4Packet(IPPROTO_UDP);
-        bcastv4packet2.put(ETH_BROADCAST_MAC_ADDRESS);
-        bcastv4packet2.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP);
-        put(bcastv4packet2, IPV4_DEST_ADDR_OFFSET, IPV4_BROADCAST_ADDRESS);
-
-        // Construct IPv4 broadcast with L2 unicast address packet (b/30231088).
-        ByteBuffer bcastv4unicastl2packet = makeIpv4Packet(IPPROTO_UDP);
-        bcastv4unicastl2packet.put(TestLegacyApfFilter.MOCK_MAC_ADDR);
-        bcastv4unicastl2packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_IP);
-        put(bcastv4unicastl2packet, IPV4_DEST_ADDR_OFFSET, broadcastIpv4Addr);
-
-        // Verify initially disabled multicast filter is off
-        assertPass(program, mcastv4packet.array());
-        assertPass(program, mcastv6packet.array());
-        assertPass(program, bcastv4packet1.array());
-        assertPass(program, bcastv4packet2.array());
-        assertPass(program, bcastv4unicastl2packet.array());
-
-        // Turn on multicast filter and verify it works
-        ipClientCallback.resetApfProgramWait();
-        apfFilter.setMulticastFilter(true);
-        program = ipClientCallback.assertProgramUpdateAndGet();
-        assertDrop(program, mcastv4packet.array());
-        assertDrop(program, mcastv6packet.array());
-        assertDrop(program, bcastv4packet1.array());
-        assertDrop(program, bcastv4packet2.array());
-        assertDrop(program, bcastv4unicastl2packet.array());
-
-        // Turn off multicast filter and verify it's off
-        ipClientCallback.resetApfProgramWait();
-        apfFilter.setMulticastFilter(false);
-        program = ipClientCallback.assertProgramUpdateAndGet();
-        assertPass(program, mcastv4packet.array());
-        assertPass(program, mcastv6packet.array());
-        assertPass(program, bcastv4packet1.array());
-        assertPass(program, bcastv4packet2.array());
-        assertPass(program, bcastv4unicastl2packet.array());
-
-        // Verify it can be initialized to on
-        ipClientCallback.resetApfProgramWait();
-        config.multicastFilter = DROP_MULTICAST;
-        config.ieee802_3Filter = DROP_802_3_FRAMES;
-        apfFilter =  new TestLegacyApfFilter(mContext, config, ipClientCallback,
-                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
-        apfFilter.setLinkProperties(lp);
-        program = ipClientCallback.assertProgramUpdateAndGet();
-        assertDrop(program, mcastv4packet.array());
-        assertDrop(program, mcastv6packet.array());
-        assertDrop(program, bcastv4packet1.array());
-        assertDrop(program, bcastv4unicastl2packet.array());
-
-        // Verify that ICMPv6 multicast is not dropped.
-        mcastv6packet.put(IPV6_NEXT_HEADER_OFFSET, (byte)IPPROTO_ICMPV6);
-        assertPass(program, mcastv6packet.array());
-    }
-
-    @Test
-    public void testApfFilterMulticastPingWhileDozing() throws Exception {
-        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
-        final ApfConfiguration configuration = getDefaultConfig();
-        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};
-        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());
-
-        // ...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.
-        apfFilter.setMulticastFilter(true);
-        assertDrop(ipClientCallback.assertProgramUpdateAndGet(), packet.array());
-
-        // However, we should still let through all other ICMPv6 types.
-        ByteBuffer raPacket = ByteBuffer.wrap(packet.array().clone());
-        setIpv6VersionFields(packet);
-        packet.put(IPV6_NEXT_HEADER_OFFSET, (byte) IPPROTO_ICMPV6);
-        raPacket.put(ICMP6_TYPE_OFFSET, (byte) NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT);
-        assertPass(ipClientCallback.assertProgramUpdateAndGet(), raPacket.array());
-
-        // Now wake up from doze mode to ensure that we no longer drop the packets.
-        // (The multicast filter is still enabled at this point).
-        apfFilter.setDozeMode(false);
-        assertPass(ipClientCallback.assertProgramUpdateAndGet(), packet.array());
-
-        apfFilter.shutdown();
-    }
-
-    @Test
-    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-    public void testApfFilter802_3() throws Exception {
-        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
-        ApfConfiguration config = getDefaultConfig();
-        TestLegacyApfFilter apfFilter =  new TestLegacyApfFilter(mContext, config, ipClientCallback,
-                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
-
-        // Verify empty packet of 100 zero bytes is passed
-        // Note that eth-type = 0 makes it an IEEE802.3 frame
-        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
-        assertPass(program, packet.array());
-
-        // Verify empty packet with IPv4 is passed
-        setIpv4VersionFields(packet);
-        assertPass(program, packet.array());
-
-        // Verify empty IPv6 packet is passed
-        setIpv6VersionFields(packet);
-        assertPass(program, packet.array());
-
-        // Now turn on the filter
-        ipClientCallback.resetApfProgramWait();
-        config.ieee802_3Filter = DROP_802_3_FRAMES;
-        apfFilter =  new TestLegacyApfFilter(mContext, config, ipClientCallback,
-                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
-        program = ipClientCallback.assertProgramUpdateAndGet();
-
-        // Verify that IEEE802.3 frame is dropped
-        // In this case ethtype is used for payload length
-        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)(100 - 14));
-        assertDrop(program, packet.array());
-
-        // Verify that IPv4 (as example of Ethernet II) frame will pass
-        setIpv4VersionFields(packet);
-        assertPass(program, packet.array());
-
-        // Verify that IPv6 (as example of Ethernet II) frame will pass
-        setIpv6VersionFields(packet);
-        assertPass(program, packet.array());
-    }
-
-    @Test
-    @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
-    public void testApfFilterEthTypeBL() throws Exception {
-        final int[] emptyBlackList = {};
-        final int[] ipv4BlackList = {ETH_P_IP};
-        final int[] ipv4Ipv6BlackList = {ETH_P_IP, ETH_P_IPV6};
-
-        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
-        ApfConfiguration config = getDefaultConfig();
-        TestLegacyApfFilter apfFilter =  new TestLegacyApfFilter(mContext, config, ipClientCallback,
-                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
-
-        // Verify empty packet of 100 zero bytes is passed
-        // Note that eth-type = 0 makes it an IEEE802.3 frame
-        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
-        assertPass(program, packet.array());
-
-        // Verify empty packet with IPv4 is passed
-        setIpv4VersionFields(packet);
-        assertPass(program, packet.array());
-
-        // Verify empty IPv6 packet is passed
-        setIpv6VersionFields(packet);
-        assertPass(program, packet.array());
-
-        // Now add IPv4 to the black list
-        ipClientCallback.resetApfProgramWait();
-        config.ethTypeBlackList = ipv4BlackList;
-        apfFilter =  new TestLegacyApfFilter(mContext, config, ipClientCallback,
-                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
-        program = ipClientCallback.assertProgramUpdateAndGet();
-
-        // Verify that IPv4 frame will be dropped
-        setIpv4VersionFields(packet);
-        assertDrop(program, packet.array());
-
-        // Verify that IPv6 frame will pass
-        setIpv6VersionFields(packet);
-        assertPass(program, packet.array());
-
-        // Now let us have both IPv4 and IPv6 in the black list
-        ipClientCallback.resetApfProgramWait();
-        config.ethTypeBlackList = ipv4Ipv6BlackList;
-        apfFilter =  new TestLegacyApfFilter(mContext, config, ipClientCallback,
-                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
-        program = ipClientCallback.assertProgramUpdateAndGet();
-
-        // Verify that IPv4 frame will be dropped
-        setIpv4VersionFields(packet);
-        assertDrop(program, packet.array());
-
-        // Verify that IPv6 frame will be dropped
-        setIpv6VersionFields(packet);
-        assertDrop(program, packet.array());
-    }
-
-    private byte[] getProgram(MockIpClientCallback cb, TestLegacyApfFilter filter,
-            LinkProperties lp) {
-        cb.resetApfProgramWait();
-        filter.setLinkProperties(lp);
-        return cb.assertProgramUpdateAndGet();
-    }
-
-    private void verifyArpFilter(byte[] program, int filterResult) {
-        // Verify ARP request packet
-        assertPass(program, arpRequestBroadcast(MOCK_IPV4_ADDR));
-        assertVerdict(filterResult, program, arpRequestBroadcast(ANOTHER_IPV4_ADDR));
-        assertVerdict(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));
-        assertPass(program, arpReply(ANOTHER_IPV4_SOURCE_ADDR, IPV4_ANY_HOST_ADDR));
-        assertPass(program, arpReply(BUG_PROBE_SOURCE_ADDR1, IPV4_ANY_HOST_ADDR));
-        assertPass(program, arpReply(BUG_PROBE_SOURCE_ADDR2, IPV4_ANY_HOST_ADDR));
-
-        // Verify unicast ARP reply packet is always accepted.
-        assertPass(program, arpReply(IPV4_SOURCE_ADDR, MOCK_IPV4_ADDR));
-        assertPass(program, arpReply(IPV4_SOURCE_ADDR, ANOTHER_IPV4_ADDR));
-        assertPass(program, arpReply(IPV4_SOURCE_ADDR, IPV4_ANY_HOST_ADDR));
-
-        // Verify GARP reply packets are always filtered
-        assertDrop(program, garpReply());
-    }
-
-    @Test
-    public void testApfFilterArp() throws Exception {
-        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
-        ApfConfiguration config = getDefaultConfig();
-        config.multicastFilter = DROP_MULTICAST;
-        config.ieee802_3Filter = DROP_802_3_FRAMES;
-        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);
-
-        // Inform ApfFilter of our address and verify ARP filtering is on
-        LinkAddress linkAddress = new LinkAddress(InetAddress.getByAddress(MOCK_IPV4_ADDR), 24);
-        LinkProperties lp = new LinkProperties();
-        assertTrue(lp.addLinkAddress(linkAddress));
-        verifyArpFilter(getProgram(ipClientCallback, apfFilter, lp), DROP);
-
-        // Inform ApfFilter of loss of IP and verify ARP filtering is off
-        verifyArpFilter(getProgram(ipClientCallback, apfFilter, new LinkProperties()), PASS);
-    }
-
-    private static byte[] arpReply(byte[] sip, byte[] tip) {
-        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
-        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_ARP);
-        put(packet, ARP_HEADER_OFFSET, ARP_IPV4_REPLY_HEADER);
-        put(packet, ARP_SOURCE_IP_ADDRESS_OFFSET, sip);
-        put(packet, ARP_TARGET_IP_ADDRESS_OFFSET, tip);
-        return packet.array();
-    }
-
-    private static byte[] arpRequestBroadcast(byte[] tip) {
-        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
-        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_ARP);
-        put(packet, ETH_DEST_ADDR_OFFSET, ETH_BROADCAST_MAC_ADDRESS);
-        put(packet, ARP_HEADER_OFFSET, ARP_IPV4_REQUEST_HEADER);
-        put(packet, ARP_TARGET_IP_ADDRESS_OFFSET, tip);
-        return packet.array();
-    }
-
-    private static byte[] garpReply() {
-        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
-        packet.putShort(ETH_ETHERTYPE_OFFSET, (short)ETH_P_ARP);
-        put(packet, ETH_DEST_ADDR_OFFSET, ETH_BROADCAST_MAC_ADDRESS);
-        put(packet, ARP_HEADER_OFFSET, ARP_IPV4_REPLY_HEADER);
-        put(packet, ARP_TARGET_IP_ADDRESS_OFFSET, IPV4_ANY_HOST_ADDR);
-        return packet.array();
-    }
-
-    private static final byte[] IPV4_KEEPALIVE_SRC_ADDR = {10, 0, 0, 5};
-    private static final byte[] IPV4_KEEPALIVE_DST_ADDR = {10, 0, 0, 6};
-    private static final byte[] IPV4_ANOTHER_ADDR = {10, 0 , 0, 7};
-    private static final byte[] IPV6_KEEPALIVE_SRC_ADDR =
-            {(byte) 0x24, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (byte) 0xfa, (byte) 0xf1};
-    private static final byte[] IPV6_KEEPALIVE_DST_ADDR =
-            {(byte) 0x24, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (byte) 0xfa, (byte) 0xf2};
-    private static final byte[] IPV6_ANOTHER_ADDR =
-            {(byte) 0x24, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, (byte) 0xfa, (byte) 0xf5};
-
-    @Test
-    public void testApfFilterKeepaliveAck() throws Exception {
-        final MockIpClientCallback cb = new MockIpClientCallback();
-        final ApfConfiguration config = getDefaultConfig();
-        config.multicastFilter = DROP_MULTICAST;
-        config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestLegacyApfFilter apfFilter =  new TestLegacyApfFilter(mContext, config, cb,
-                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
-        byte[] program;
-        final int srcPort = 12345;
-        final int dstPort = 54321;
-        final int seqNum = 2123456789;
-        final int ackNum = 1234567890;
-        final int anotherSrcPort = 23456;
-        final int anotherDstPort = 65432;
-        final int anotherSeqNum = 2123456780;
-        final int anotherAckNum = 1123456789;
-        final int slot1 = 1;
-        final int slot2 = 2;
-        final int window = 14480;
-        final int windowScale = 4;
-
-        // src: 10.0.0.5, port: 12345
-        // dst: 10.0.0.6, port: 54321
-        InetAddress srcAddr = InetAddress.getByAddress(IPV4_KEEPALIVE_SRC_ADDR);
-        InetAddress dstAddr = InetAddress.getByAddress(IPV4_KEEPALIVE_DST_ADDR);
-
-        final TcpKeepalivePacketDataParcelable parcel = new TcpKeepalivePacketDataParcelable();
-        parcel.srcAddress = srcAddr.getAddress();
-        parcel.srcPort = srcPort;
-        parcel.dstAddress = dstAddr.getAddress();
-        parcel.dstPort = dstPort;
-        parcel.seq = seqNum;
-        parcel.ack = ackNum;
-
-        apfFilter.addTcpKeepalivePacketFilter(slot1, parcel);
-        program = cb.assertProgramUpdateAndGet();
-
-        // Verify IPv4 keepalive ack packet is dropped
-        // src: 10.0.0.6, port: 54321
-        // dst: 10.0.0.5, port: 12345
-        assertDrop(program,
-                ipv4TcpPacket(IPV4_KEEPALIVE_DST_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
-                        dstPort, srcPort, ackNum, seqNum + 1, 0 /* dataLength */));
-        // Verify IPv4 non-keepalive ack packet from the same source address is passed
-        assertPass(program,
-                ipv4TcpPacket(IPV4_KEEPALIVE_DST_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
-                        dstPort, srcPort, ackNum + 100, seqNum, 0 /* dataLength */));
-        assertPass(program,
-                ipv4TcpPacket(IPV4_KEEPALIVE_DST_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
-                        dstPort, srcPort, ackNum, seqNum + 1, 10 /* dataLength */));
-        // Verify IPv4 packet from another address is passed
-        assertPass(program,
-                ipv4TcpPacket(IPV4_ANOTHER_ADDR, IPV4_KEEPALIVE_SRC_ADDR, anotherSrcPort,
-                        anotherDstPort, anotherSeqNum, anotherAckNum, 0 /* dataLength */));
-
-        // Remove IPv4 keepalive filter
-        apfFilter.removeKeepalivePacketFilter(slot1);
-
-        try {
-            // src: 2404:0:0:0:0:0:faf1, port: 12345
-            // dst: 2404:0:0:0:0:0:faf2, port: 54321
-            srcAddr = InetAddress.getByAddress(IPV6_KEEPALIVE_SRC_ADDR);
-            dstAddr = InetAddress.getByAddress(IPV6_KEEPALIVE_DST_ADDR);
-
-            final TcpKeepalivePacketDataParcelable ipv6Parcel =
-                    new TcpKeepalivePacketDataParcelable();
-            ipv6Parcel.srcAddress = srcAddr.getAddress();
-            ipv6Parcel.srcPort = srcPort;
-            ipv6Parcel.dstAddress = dstAddr.getAddress();
-            ipv6Parcel.dstPort = dstPort;
-            ipv6Parcel.seq = seqNum;
-            ipv6Parcel.ack = ackNum;
-
-            apfFilter.addTcpKeepalivePacketFilter(slot1, ipv6Parcel);
-            program = cb.assertProgramUpdateAndGet();
-
-            // Verify IPv6 keepalive ack packet is dropped
-            // src: 2404:0:0:0:0:0:faf2, port: 54321
-            // dst: 2404:0:0:0:0:0:faf1, port: 12345
-            assertDrop(program,
-                    ipv6TcpPacket(IPV6_KEEPALIVE_DST_ADDR, IPV6_KEEPALIVE_SRC_ADDR,
-                            dstPort, srcPort, ackNum, seqNum + 1));
-            // Verify IPv6 non-keepalive ack packet from the same source address is passed
-            assertPass(program,
-                    ipv6TcpPacket(IPV6_KEEPALIVE_DST_ADDR, IPV6_KEEPALIVE_SRC_ADDR,
-                            dstPort, srcPort, ackNum + 100, seqNum));
-            // Verify IPv6 packet from another address is passed
-            assertPass(program,
-                    ipv6TcpPacket(IPV6_ANOTHER_ADDR, IPV6_KEEPALIVE_SRC_ADDR, anotherSrcPort,
-                            anotherDstPort, anotherSeqNum, anotherAckNum));
-
-            // Remove IPv6 keepalive filter
-            apfFilter.removeKeepalivePacketFilter(slot1);
-
-            // Verify multiple filters
-            apfFilter.addTcpKeepalivePacketFilter(slot1, parcel);
-            apfFilter.addTcpKeepalivePacketFilter(slot2, ipv6Parcel);
-            program = cb.assertProgramUpdateAndGet();
-
-            // Verify IPv4 keepalive ack packet is dropped
-            // src: 10.0.0.6, port: 54321
-            // dst: 10.0.0.5, port: 12345
-            assertDrop(program,
-                    ipv4TcpPacket(IPV4_KEEPALIVE_DST_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
-                            dstPort, srcPort, ackNum, seqNum + 1, 0 /* dataLength */));
-            // Verify IPv4 non-keepalive ack packet from the same source address is passed
-            assertPass(program,
-                    ipv4TcpPacket(IPV4_KEEPALIVE_DST_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
-                            dstPort, srcPort, ackNum + 100, seqNum, 0 /* dataLength */));
-            // Verify IPv4 packet from another address is passed
-            assertPass(program,
-                    ipv4TcpPacket(IPV4_ANOTHER_ADDR, IPV4_KEEPALIVE_SRC_ADDR, anotherSrcPort,
-                            anotherDstPort, anotherSeqNum, anotherAckNum, 0 /* dataLength */));
-
-            // Verify IPv6 keepalive ack packet is dropped
-            // src: 2404:0:0:0:0:0:faf2, port: 54321
-            // dst: 2404:0:0:0:0:0:faf1, port: 12345
-            assertDrop(program,
-                    ipv6TcpPacket(IPV6_KEEPALIVE_DST_ADDR, IPV6_KEEPALIVE_SRC_ADDR,
-                            dstPort, srcPort, ackNum, seqNum + 1));
-            // Verify IPv6 non-keepalive ack packet from the same source address is passed
-            assertPass(program,
-                    ipv6TcpPacket(IPV6_KEEPALIVE_DST_ADDR, IPV6_KEEPALIVE_SRC_ADDR,
-                            dstPort, srcPort, ackNum + 100, seqNum));
-            // Verify IPv6 packet from another address is passed
-            assertPass(program,
-                    ipv6TcpPacket(IPV6_ANOTHER_ADDR, IPV6_KEEPALIVE_SRC_ADDR, anotherSrcPort,
-                            anotherDstPort, anotherSeqNum, anotherAckNum));
-
-            // Remove keepalive filters
-            apfFilter.removeKeepalivePacketFilter(slot1);
-            apfFilter.removeKeepalivePacketFilter(slot2);
-        } catch (UnsupportedOperationException e) {
-            // TODO: support V6 packets
-        }
-
-        program = cb.assertProgramUpdateAndGet();
-
-        // Verify IPv4, IPv6 packets are passed
-        assertPass(program,
-                ipv4TcpPacket(IPV4_KEEPALIVE_DST_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
-                        dstPort, srcPort, ackNum, seqNum + 1, 0 /* dataLength */));
-        assertPass(program,
-                ipv6TcpPacket(IPV6_KEEPALIVE_DST_ADDR, IPV6_KEEPALIVE_SRC_ADDR,
-                        dstPort, srcPort, ackNum, seqNum + 1));
-        assertPass(program,
-                ipv4TcpPacket(IPV4_ANOTHER_ADDR, IPV4_KEEPALIVE_SRC_ADDR, srcPort,
-                        dstPort, anotherSeqNum, anotherAckNum, 0 /* dataLength */));
-        assertPass(program,
-                ipv6TcpPacket(IPV6_ANOTHER_ADDR, IPV6_KEEPALIVE_SRC_ADDR, srcPort,
-                        dstPort, anotherSeqNum, anotherAckNum));
-    }
-
-    private static byte[] ipv4TcpPacket(byte[] sip, byte[] dip, int sport,
-            int dport, int seq, int ack, int dataLength) {
-        final int totalLength = dataLength + IPV4_HEADER_LEN + IPV4_TCP_HEADER_LEN;
-
-        ByteBuffer packet = ByteBuffer.wrap(new byte[totalLength + ETH_HEADER_LEN]);
-
-        // Ethertype and IPv4 header
-        setIpv4VersionFields(packet);
-        packet.putShort(IPV4_TOTAL_LENGTH_OFFSET, (short) totalLength);
-        packet.put(IPV4_PROTOCOL_OFFSET, (byte) IPPROTO_TCP);
-        put(packet, IPV4_SRC_ADDR_OFFSET, sip);
-        put(packet, IPV4_DEST_ADDR_OFFSET, dip);
-        packet.putShort(IPV4_TCP_SRC_PORT_OFFSET, (short) sport);
-        packet.putShort(IPV4_TCP_DEST_PORT_OFFSET, (short) dport);
-        packet.putInt(IPV4_TCP_SEQ_NUM_OFFSET, seq);
-        packet.putInt(IPV4_TCP_ACK_NUM_OFFSET, ack);
-
-        // TCP header length 5(20 bytes), reserved 3 bits, NS=0
-        packet.put(IPV4_TCP_HEADER_LENGTH_OFFSET, (byte) 0x50);
-        // TCP flags: ACK set
-        packet.put(IPV4_TCP_HEADER_FLAG_OFFSET, (byte) 0x10);
-        return packet.array();
-    }
-
-    private static byte[] ipv6TcpPacket(byte[] sip, byte[] tip, int sport,
-            int dport, int seq, int ack) {
-        ByteBuffer packet = ByteBuffer.wrap(new byte[100]);
-        setIpv6VersionFields(packet);
-        packet.put(IPV6_NEXT_HEADER_OFFSET, (byte) IPPROTO_TCP);
-        put(packet, IPV6_SRC_ADDR_OFFSET, sip);
-        put(packet, IPV6_DEST_ADDR_OFFSET, tip);
-        packet.putShort(IPV6_TCP_SRC_PORT_OFFSET, (short) sport);
-        packet.putShort(IPV6_TCP_DEST_PORT_OFFSET, (short) dport);
-        packet.putInt(IPV6_TCP_SEQ_NUM_OFFSET, seq);
-        packet.putInt(IPV6_TCP_ACK_NUM_OFFSET, ack);
-        return packet.array();
-    }
-
-    @Test
-    public void testApfFilterNattKeepalivePacket() throws Exception {
-        final MockIpClientCallback cb = new MockIpClientCallback();
-        final ApfConfiguration config = getDefaultConfig();
-        config.multicastFilter = DROP_MULTICAST;
-        config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestLegacyApfFilter apfFilter =  new TestLegacyApfFilter(mContext, config, cb,
-                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
-        byte[] program;
-        final int srcPort = 1024;
-        final int dstPort = 4500;
-        final int slot1 = 1;
-        // NAT-T keepalive
-        final byte[] kaPayload = {(byte) 0xff};
-        final byte[] nonKaPayload = {(byte) 0xfe};
-
-        // src: 10.0.0.5, port: 1024
-        // dst: 10.0.0.6, port: 4500
-        InetAddress srcAddr = InetAddress.getByAddress(IPV4_KEEPALIVE_SRC_ADDR);
-        InetAddress dstAddr = InetAddress.getByAddress(IPV4_KEEPALIVE_DST_ADDR);
-
-        final NattKeepalivePacketDataParcelable parcel = new NattKeepalivePacketDataParcelable();
-        parcel.srcAddress = srcAddr.getAddress();
-        parcel.srcPort = srcPort;
-        parcel.dstAddress = dstAddr.getAddress();
-        parcel.dstPort = dstPort;
-
-        apfFilter.addNattKeepalivePacketFilter(slot1, parcel);
-        program = cb.assertProgramUpdateAndGet();
-
-        // Verify IPv4 keepalive packet is dropped
-        // src: 10.0.0.6, port: 4500
-        // dst: 10.0.0.5, port: 1024
-        byte[] pkt = ipv4UdpPacket(IPV4_KEEPALIVE_DST_ADDR,
-                    IPV4_KEEPALIVE_SRC_ADDR, dstPort, srcPort, 1 /* dataLength */);
-        System.arraycopy(kaPayload, 0, pkt, IPV4_UDP_PAYLOAD_OFFSET, kaPayload.length);
-        assertDrop(program, pkt);
-
-        // Verify a packet with payload length 1 byte but it is not 0xff will pass the filter.
-        System.arraycopy(nonKaPayload, 0, pkt, IPV4_UDP_PAYLOAD_OFFSET, nonKaPayload.length);
-        assertPass(program, pkt);
-
-        // Verify IPv4 non-keepalive response packet from the same source address is passed
-        assertPass(program,
-                ipv4UdpPacket(IPV4_KEEPALIVE_DST_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
-                        dstPort, srcPort, 10 /* dataLength */));
-
-        // Verify IPv4 non-keepalive response packet from other source address is passed
-        assertPass(program,
-                ipv4UdpPacket(IPV4_ANOTHER_ADDR, IPV4_KEEPALIVE_SRC_ADDR,
-                        dstPort, srcPort, 10 /* dataLength */));
-
-        apfFilter.removeKeepalivePacketFilter(slot1);
-    }
-
-    private static byte[] ipv4UdpPacket(byte[] sip, byte[] dip, int sport,
-            int dport, int dataLength) {
-        final int totalLength = dataLength + IPV4_HEADER_LEN + UDP_HEADER_LEN;
-        final int udpLength = UDP_HEADER_LEN + dataLength;
-        ByteBuffer packet = ByteBuffer.wrap(new byte[totalLength + ETH_HEADER_LEN]);
-
-        // Ethertype and IPv4 header
-        setIpv4VersionFields(packet);
-        packet.putShort(IPV4_TOTAL_LENGTH_OFFSET, (short) totalLength);
-        packet.put(IPV4_PROTOCOL_OFFSET, (byte) IPPROTO_UDP);
-        put(packet, IPV4_SRC_ADDR_OFFSET, sip);
-        put(packet, IPV4_DEST_ADDR_OFFSET, dip);
-        packet.putShort(IPV4_UDP_SRC_PORT_OFFSET, (short) sport);
-        packet.putShort(IPV4_UDP_DEST_PORT_OFFSET, (short) dport);
-        packet.putShort(IPV4_UDP_LENGTH_OFFSET, (short) udpLength);
-
-        return packet.array();
-    }
-
-    private static class RaPacketBuilder {
-        final ByteArrayOutputStream mPacket = new ByteArrayOutputStream();
-        int mFlowLabel = 0x12345;
-        int mReachableTime = 30_000;
-        int mRetransmissionTimer = 1000;
-
-        public RaPacketBuilder(int routerLft) throws Exception {
-            InetAddress src = InetAddress.getByName("fe80::1234:abcd");
-            ByteBuffer buffer = ByteBuffer.allocate(ICMP6_RA_OPTION_OFFSET);
-
-            buffer.putShort(ETH_ETHERTYPE_OFFSET, (short) ETH_P_IPV6);
-            buffer.position(ETH_HEADER_LEN);
-
-            // skip version, tclass, flowlabel; set in build()
-            buffer.position(buffer.position() + 4);
-
-            buffer.putShort((short) 0);                     // Payload length; updated later
-            buffer.put((byte) IPPROTO_ICMPV6);              // Next header
-            buffer.put((byte) 0xff);                        // Hop limit
-            buffer.put(src.getAddress());                   // Source address
-            buffer.put(IPV6_ALL_NODES_ADDRESS);             // Destination address
-
-            buffer.put((byte) ICMP6_ROUTER_ADVERTISEMENT);  // Type
-            buffer.put((byte) 0);                           // Code (0)
-            buffer.putShort((short) 0);                     // Checksum (ignored)
-            buffer.put((byte) 64);                          // Hop limit
-            buffer.put((byte) 0);                           // M/O, reserved
-            buffer.putShort((short) routerLft);             // Router lifetime
-            // skip reachable time; set in build()
-            // skip retransmission timer; set in build();
-
-            mPacket.write(buffer.array(), 0, buffer.capacity());
-        }
-
-        public RaPacketBuilder setFlowLabel(int flowLabel) {
-            mFlowLabel = flowLabel;
-            return this;
-        }
-
-        public RaPacketBuilder setReachableTime(int reachable) {
-            mReachableTime = reachable;
-            return this;
-        }
-
-        public RaPacketBuilder setRetransmissionTimer(int retrans) {
-            mRetransmissionTimer = retrans;
-            return this;
-        }
-
-        public RaPacketBuilder addPioOption(int valid, int preferred, String prefixString)
-                throws Exception {
-            ByteBuffer buffer = ByteBuffer.allocate(ICMP6_PREFIX_OPTION_LEN);
-
-            IpPrefix prefix = new IpPrefix(prefixString);
-            buffer.put((byte) ICMP6_PREFIX_OPTION_TYPE);  // Type
-            buffer.put((byte) 4);                         // Length in 8-byte units
-            buffer.put((byte) prefix.getPrefixLength());  // Prefix length
-            buffer.put((byte) 0b11000000);                // L = 1, A = 1
-            buffer.putInt(valid);
-            buffer.putInt(preferred);
-            buffer.putInt(0);                             // Reserved
-            buffer.put(prefix.getRawAddress());
-
-            mPacket.write(buffer.array(), 0, buffer.capacity());
-            return this;
-        }
-
-        public RaPacketBuilder addRioOption(int lifetime, String prefixString) throws Exception {
-            IpPrefix prefix = new IpPrefix(prefixString);
-
-            int optionLength;
-            if (prefix.getPrefixLength() == 0) {
-                optionLength = 1;
-            } else if (prefix.getPrefixLength() <= 64) {
-                optionLength = 2;
-            } else {
-                optionLength = 3;
-            }
-
-            ByteBuffer buffer = ByteBuffer.allocate(optionLength * 8);
-
-            buffer.put((byte) ICMP6_ROUTE_INFO_OPTION_TYPE);  // Type
-            buffer.put((byte) optionLength);                  // Length in 8-byte units
-            buffer.put((byte) prefix.getPrefixLength());      // Prefix length
-            buffer.put((byte) 0b00011000);                    // Pref = high
-            buffer.putInt(lifetime);                          // Lifetime
-
-            byte[] prefixBytes = prefix.getRawAddress();
-            buffer.put(prefixBytes, 0, (optionLength - 1) * 8);
-
-            mPacket.write(buffer.array(), 0, buffer.capacity());
-            return this;
-        }
-
-        public RaPacketBuilder addDnsslOption(int lifetime, String... domains) {
-            ByteArrayOutputStream dnssl = new ByteArrayOutputStream();
-            for (String domain : domains) {
-                for (String label : domain.split("\\.")) {
-                    final byte[] bytes = label.getBytes(StandardCharsets.UTF_8);
-                    dnssl.write((byte) bytes.length);
-                    dnssl.write(bytes, 0, bytes.length);
-                }
-                dnssl.write((byte) 0);
-            }
-
-            // Extend with 0s to make it 8-byte aligned.
-            while (dnssl.size() % 8 != 0) {
-                dnssl.write((byte) 0);
-            }
-
-            final int length = ICMP6_4_BYTE_OPTION_LEN + dnssl.size();
-            ByteBuffer buffer = ByteBuffer.allocate(length);
-
-            buffer.put((byte) ICMP6_DNSSL_OPTION_TYPE);  // Type
-            buffer.put((byte) (length / 8));             // Length
-            // skip past reserved bytes
-            buffer.position(buffer.position() + 2);
-            buffer.putInt(lifetime);                     // Lifetime
-            buffer.put(dnssl.toByteArray());             // Domain names
-
-            mPacket.write(buffer.array(), 0, buffer.capacity());
-            return this;
-        }
-
-        public RaPacketBuilder addRdnssOption(int lifetime, String... servers) throws Exception {
-            int optionLength = 1 + 2 * servers.length;   // In 8-byte units
-            ByteBuffer buffer = ByteBuffer.allocate(optionLength * 8);
-
-            buffer.put((byte) ICMP6_RDNSS_OPTION_TYPE);  // Type
-            buffer.put((byte) optionLength);             // Length
-            buffer.putShort((short) 0);                  // Reserved
-            buffer.putInt(lifetime);                     // Lifetime
-            for (String server : servers) {
-                buffer.put(InetAddress.getByName(server).getAddress());
-            }
-
-            mPacket.write(buffer.array(), 0, buffer.capacity());
-            return this;
-        }
-
-        public RaPacketBuilder addZeroLengthOption() throws Exception {
-            ByteBuffer buffer = ByteBuffer.allocate(ICMP6_4_BYTE_OPTION_LEN);
-            buffer.put((byte) ICMP6_PREFIX_OPTION_TYPE);
-            buffer.put((byte) 0);
-
-            mPacket.write(buffer.array(), 0, buffer.capacity());
-            return this;
-        }
-
-        public byte[] build() {
-            ByteBuffer buffer = ByteBuffer.wrap(mPacket.toByteArray());
-            // IPv6, traffic class = 0, flow label = mFlowLabel
-            buffer.putInt(IP_HEADER_OFFSET, 0x60000000 | (0xFFFFF & mFlowLabel));
-            buffer.putShort(IPV6_PAYLOAD_LENGTH_OFFSET, (short) buffer.capacity());
-
-            buffer.position(ICMP6_RA_REACHABLE_TIME_OFFSET);
-            buffer.putInt(mReachableTime);
-            buffer.putInt(mRetransmissionTimer);
-
-            return buffer.array();
-        }
-    }
-
-    private byte[] buildLargeRa() throws Exception {
-        RaPacketBuilder builder = new RaPacketBuilder(1800 /* router lft */);
-
-        builder.addRioOption(1200, "64:ff9b::/96");
-        builder.addRdnssOption(7200, "2001:db8:1::1", "2001:db8:1::2");
-        builder.addRioOption(2100, "2000::/3");
-        builder.addRioOption(2400, "::/0");
-        builder.addPioOption(600, 300, "2001:db8:a::/64");
-        builder.addRioOption(1500, "2001:db8:c:d::/64");
-        builder.addPioOption(86400, 43200, "fd95:d1e:12::/64");
-
-        return builder.build();
-    }
-
-    // Verify that the last program pushed to the IpClient.Callback properly filters the
-    // given packet for the given lifetime.
-    private void verifyRaLifetime(byte[] program, ByteBuffer packet, int lifetime) {
-        verifyRaLifetime(program, packet, lifetime, 0);
-    }
-
-    // Verify that the last program pushed to the IpClient.Callback properly filters the
-    // given packet for the given lifetime and programInstallTime. programInstallTime is
-    // the time difference between when RA is last seen and the program is installed.
-    private void verifyRaLifetime(byte[] program, ByteBuffer packet, int lifetime,
-            int programInstallTime) {
-        final int FRACTION_OF_LIFETIME = 6;
-        final int ageLimit = lifetime / FRACTION_OF_LIFETIME - programInstallTime;
-
-        // Verify new program should drop RA for 1/6th its lifetime and pass afterwards.
-        assertDrop(program, packet.array());
-        assertDrop(program, packet.array(), ageLimit);
-        assertPass(program, packet.array(), ageLimit + 1);
-        assertPass(program, packet.array(), lifetime);
-        // Verify RA checksum is ignored
-        final short originalChecksum = packet.getShort(ICMP6_RA_CHECKSUM_OFFSET);
-        packet.putShort(ICMP6_RA_CHECKSUM_OFFSET, (short)12345);
-        assertDrop(program, packet.array());
-        packet.putShort(ICMP6_RA_CHECKSUM_OFFSET, (short)-12345);
-        assertDrop(program, packet.array());
-        packet.putShort(ICMP6_RA_CHECKSUM_OFFSET, originalChecksum);
-
-        // Verify other changes to RA (e.g., a change in the source address) make it not match.
-        final int offset = IPV6_SRC_ADDR_OFFSET + 5;
-        final byte originalByte = packet.get(offset);
-        packet.put(offset, (byte) (~originalByte));
-        assertPass(program, packet.array());
-        packet.put(offset, originalByte);
-        assertDrop(program, packet.array());
-    }
-
-    // Test that when ApfFilter is shown the given packet, it generates a program to filter it
-    // for the given lifetime.
-    private void verifyRaLifetime(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(TestLegacyApfFilter apfFilter,
-            MockIpClientCallback ipClientCallback, ByteBuffer packet)
-            throws IOException, ErrnoException {
-        apfFilter.pretendPacketReceived(packet.array());
-        ipClientCallback.assertNoProgramUpdate();
-    }
-
-    @Test
-    public void testApfFilterRa() throws Exception {
-        MockIpClientCallback ipClientCallback = new MockIpClientCallback();
-        ApfConfiguration config = getDefaultConfig();
-        config.multicastFilter = DROP_MULTICAST;
-        config.ieee802_3Filter = DROP_802_3_FRAMES;
-        TestLegacyApfFilter apfFilter = new TestLegacyApfFilter(mContext, config, ipClientCallback,
-                mIpConnectivityLog, mNetworkQuirkMetrics, mDependencies, mClock);
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
-
-        final int ROUTER_LIFETIME = 1000;
-        final int PREFIX_VALID_LIFETIME = 200;
-        final int PREFIX_PREFERRED_LIFETIME = 100;
-        final int RDNSS_LIFETIME  = 300;
-        final int ROUTE_LIFETIME  = 400;
-        // Note that lifetime of 2000 will be ignored in favor of shorter route lifetime of 1000.
-        final int DNSSL_LIFETIME  = 2000;
-
-        // Verify RA is passed the first time
-        RaPacketBuilder ra = new RaPacketBuilder(ROUTER_LIFETIME);
-        ByteBuffer basePacket = ByteBuffer.wrap(ra.build());
-        assertPass(program, basePacket.array());
-
-        verifyRaLifetime(apfFilter, ipClientCallback, basePacket, ROUTER_LIFETIME);
-
-        ra = new RaPacketBuilder(ROUTER_LIFETIME);
-        // Check that changes are ignored in every byte of the flow label.
-        ra.setFlowLabel(0x56789);
-        ByteBuffer newFlowLabelPacket = ByteBuffer.wrap(ra.build());
-
-        // Ensure zero-length options cause the packet to be silently skipped.
-        // Do this before we test other packets. http://b/29586253
-        ra = new RaPacketBuilder(ROUTER_LIFETIME);
-        ra.addZeroLengthOption();
-        ByteBuffer zeroLengthOptionPacket = ByteBuffer.wrap(ra.build());
-        assertInvalidRa(apfFilter, ipClientCallback, zeroLengthOptionPacket);
-
-        // Generate several RAs with different options and lifetimes, and verify when
-        // ApfFilter is shown these packets, it generates programs to filter them for the
-        // appropriate lifetime.
-        ra = new RaPacketBuilder(ROUTER_LIFETIME);
-        ra.addPioOption(PREFIX_VALID_LIFETIME, PREFIX_PREFERRED_LIFETIME, "2001:db8::/64");
-        ByteBuffer prefixOptionPacket = ByteBuffer.wrap(ra.build());
-        verifyRaLifetime(
-                apfFilter, ipClientCallback, prefixOptionPacket, PREFIX_PREFERRED_LIFETIME);
-
-        ra = new RaPacketBuilder(ROUTER_LIFETIME);
-        ra.addRdnssOption(RDNSS_LIFETIME, "2001:4860:4860::8888", "2001:4860:4860::8844");
-        ByteBuffer rdnssOptionPacket = ByteBuffer.wrap(ra.build());
-        verifyRaLifetime(apfFilter, ipClientCallback, rdnssOptionPacket, RDNSS_LIFETIME);
-
-        final int lowLifetime = 60;
-        ra = new RaPacketBuilder(ROUTER_LIFETIME);
-        ra.addRdnssOption(lowLifetime, "2620:fe::9");
-        ByteBuffer lowLifetimeRdnssOptionPacket = ByteBuffer.wrap(ra.build());
-        verifyRaLifetime(apfFilter, ipClientCallback, lowLifetimeRdnssOptionPacket,
-                ROUTER_LIFETIME);
-
-        ra = new RaPacketBuilder(ROUTER_LIFETIME);
-        ra.addRioOption(ROUTE_LIFETIME, "64:ff9b::/96");
-        ByteBuffer routeInfoOptionPacket = ByteBuffer.wrap(ra.build());
-        verifyRaLifetime(apfFilter, ipClientCallback, routeInfoOptionPacket, ROUTE_LIFETIME);
-
-        // Check that RIOs differing only in the first 4 bytes are different.
-        ra = new RaPacketBuilder(ROUTER_LIFETIME);
-        ra.addRioOption(ROUTE_LIFETIME, "64:ff9b::/64");
-        // Packet should be passed because it is different.
-        program = ipClientCallback.assertProgramUpdateAndGet();
-        assertPass(program, ra.build());
-
-        ra = new RaPacketBuilder(ROUTER_LIFETIME);
-        ra.addDnsslOption(DNSSL_LIFETIME, "test.example.com", "one.more.example.com");
-        ByteBuffer dnsslOptionPacket = ByteBuffer.wrap(ra.build());
-        verifyRaLifetime(apfFilter, ipClientCallback, dnsslOptionPacket, ROUTER_LIFETIME);
-
-        ByteBuffer largeRaPacket = ByteBuffer.wrap(buildLargeRa());
-        verifyRaLifetime(apfFilter, ipClientCallback, largeRaPacket, 300);
-
-        // Verify that current program filters all the RAs (note: ApfFilter.MAX_RAS == 10).
-        program = ipClientCallback.assertProgramUpdateAndGet();
-        verifyRaLifetime(program, basePacket, ROUTER_LIFETIME);
-        verifyRaLifetime(program, newFlowLabelPacket, ROUTER_LIFETIME);
-        verifyRaLifetime(program, prefixOptionPacket, PREFIX_PREFERRED_LIFETIME);
-        verifyRaLifetime(program, rdnssOptionPacket, RDNSS_LIFETIME);
-        verifyRaLifetime(program, lowLifetimeRdnssOptionPacket, ROUTER_LIFETIME);
-        verifyRaLifetime(program, routeInfoOptionPacket, ROUTE_LIFETIME);
-        verifyRaLifetime(program, dnsslOptionPacket, ROUTER_LIFETIME);
-        verifyRaLifetime(program, largeRaPacket, 300);
-    }
-
-    @Test
-    public void testRaWithDifferentReachableTimeAndRetransTimer() throws Exception {
-        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
-        final ApfConfiguration config = getDefaultConfig();
-        config.multicastFilter = DROP_MULTICAST;
-        config.ieee802_3Filter = DROP_802_3_FRAMES;
-        final 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;
-
-        // Create an Ra packet without options
-        // Reachable time = 1800, retransmission timer = 1234
-        RaPacketBuilder ra = new RaPacketBuilder(1800 /* router lft */);
-        ra.setReachableTime(RA_REACHABLE_TIME);
-        ra.setRetransmissionTimer(RA_RETRANSMISSION_TIMER);
-        byte[] raPacket = ra.build();
-        // First RA passes filter
-        assertPass(program, raPacket);
-
-        // Assume apf is shown the given RA, it generates program to filter it.
-        apfFilter.pretendPacketReceived(raPacket);
-        program = ipClientCallback.assertProgramUpdateAndGet();
-        assertDrop(program, raPacket);
-
-        // A packet with different reachable time should be passed.
-        // Reachable time = 2300, retransmission timer = 1234
-        ra.setReachableTime(RA_REACHABLE_TIME + 500);
-        raPacket = ra.build();
-        assertPass(program, raPacket);
-
-        // A packet with different retransmission timer should be passed.
-        // Reachable time = 1800, retransmission timer = 2234
-        ra.setReachableTime(RA_REACHABLE_TIME);
-        ra.setRetransmissionTimer(RA_RETRANSMISSION_TIMER + 1000);
-        raPacket = ra.build();
-        assertPass(program, raPacket);
-    }
-
-    /**
-     * Stage a file for testing, i.e. make it native accessible. Given a resource ID,
-     * copy that resource into the app's data directory and return the path to it.
-     */
-    private String stageFile(int rawId) throws Exception {
-        File file = new File(InstrumentationRegistry.getContext().getFilesDir(), "staged_file");
-        new File(file.getParent()).mkdirs();
-        InputStream in = null;
-        OutputStream out = null;
-        try {
-            in = InstrumentationRegistry.getContext().getResources().openRawResource(rawId);
-            out = new FileOutputStream(file);
-            Streams.copy(in, out);
-        } finally {
-            if (in != null) in.close();
-            if (out != null) out.close();
-        }
-        return file.getAbsolutePath();
-    }
-
-    private static void put(ByteBuffer buffer, int position, byte[] bytes) {
-        final int original = buffer.position();
-        buffer.position(position);
-        buffer.put(bytes);
-        buffer.position(original);
-    }
-
-    @Test
-    public void testRaParsing() throws Exception {
-        final int maxRandomPacketSize = 512;
-        final Random r = new Random();
-        MockIpClientCallback cb = new MockIpClientCallback();
-        ApfConfiguration config = getDefaultConfig();
-        config.multicastFilter = DROP_MULTICAST;
-        config.ieee802_3Filter = DROP_802_3_FRAMES;
-        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 (LegacyApfFilter.InvalidRaException e) {
-            } catch (Exception e) {
-                throw new Exception("bad packet: " + HexDump.toHexString(packet), e);
-            }
-        }
-    }
-
-    @Test
-    public void testRaProcessing() throws Exception {
-        final int maxRandomPacketSize = 512;
-        final Random r = new Random();
-        MockIpClientCallback cb = new MockIpClientCallback();
-        ApfConfiguration config = getDefaultConfig();
-        config.multicastFilter = DROP_MULTICAST;
-        config.ieee802_3Filter = DROP_802_3_FRAMES;
-        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.processRa(packet, packet.length);
-            } catch (Exception e) {
-                throw new Exception("bad packet: " + HexDump.toHexString(packet), e);
-            }
-        }
-    }
-
-    @Test
-    public void testProcessRaWithInfiniteLifeTimeWithoutCrash() throws Exception {
-        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
-        // configure accept_ra_min_lft
-        final ApfConfiguration config = getDefaultConfig();
-        config.acceptRaMinLft = 180;
-        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)
-        // Internet Protocol Version 6, Src: fe80::2ac6:8eff:fe23:672c, Dst: ff02::1
-        // Internet Control Message Protocol v6
-        //   Type: Router Advertisement (134)
-        //   Code: 0
-        //   Checksum: 0x0acd [correct]
-        //   Checksum Status: Good
-        //   Cur hop limit: 64
-        //   Flags: 0xc0, Managed address configuration, Other configuration, Prf (Default Router Preference): Medium
-        //   Router lifetime (s): 7000
-        //   Reachable time (ms): 0
-        //   Retrans timer (ms): 0
-        //   ICMPv6 Option (Source link-layer address : 28:c6:8e:23:67:2c)
-        //     Type: Source link-layer address (1)
-        //     Length: 1 (8 bytes)
-        //     Link-layer address: Netgear_23:67:2c (28:c6:8e:23:67:2c)
-        //     Source Link-layer address: Netgear_23:67:2c (28:c6:8e:23:67:2c)
-        //   ICMPv6 Option (MTU : 1500)
-        //     Type: MTU (5)
-        //     Length: 1 (8 bytes)
-        //     Reserved
-        //     MTU: 1500
-        //   ICMPv6 Option (Prefix information : 2401:fa00:480:f000::/64)
-        //     Type: Prefix information (3)
-        //     Length: 4 (32 bytes)
-        //     Prefix Length: 64
-        //     Flag: 0xc0, On-link flag(L), Autonomous address-configuration flag(A)
-        //     Valid Lifetime: Infinity (4294967295)
-        //     Preferred Lifetime: Infinity (4294967295)
-        //     Reserved
-        //     Prefix: 2401:fa00:480:f000::
-        //   ICMPv6 Option (Recursive DNS Server 2401:fa00:480:f000::1)
-        //     Type: Recursive DNS Server (25)
-        //     Length: 3 (24 bytes)
-        //     Reserved
-        //     Lifetime: 7000
-        //     Recursive DNS Servers: 2401:fa00:480:f000::1
-        //   ICMPv6 Option (Advertisement Interval : 600000)
-        //     Type: Advertisement Interval (7)
-        //     Length: 1 (8 bytes)
-        //     Reserved
-        //     Advertisement Interval: 600000
-        final String packetStringFmt = "33330000000128C68E23672C86DD60054C6B00603AFFFE800000000000002AC68EFFFE23672CFF02000000000000000000000000000186000ACD40C01B580000000000000000010128C68E23672C05010000000005DC030440C0%s000000002401FA000480F00000000000000000001903000000001B582401FA000480F000000000000000000107010000000927C0";
-        final List<String> lifetimes = List.of("FFFFFFFF", "00000001", "00001B58");
-        for (String lifetime : lifetimes) {
-            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.
-            apfFilter.pretendPacketReceived(ra);
-            ipClientCallback.assertProgramUpdateAndGet();
-        }
-    }
-
-    private TestAndroidPacketFilter makeTestApfFilter(ApfConfiguration config,
-            MockIpClientCallback ipClientCallback) throws Exception {
-        return new TestLegacyApfFilter(mContext, config, ipClientCallback, mIpConnectivityLog,
-                mNetworkQuirkMetrics, mDependencies, mClock);
-    }
-
-
-    @Test
-    public void testInstallPacketFilterFailure_LegacyApfFilter() throws Exception {
-        final MockIpClientCallback ipClientCallback = new MockIpClientCallback(false);
-        final ApfConfiguration config = getDefaultConfig();
-        final TestAndroidPacketFilter apfFilter = makeTestApfFilter(config, ipClientCallback);
-        verify(mNetworkQuirkMetrics).setEvent(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
-        verify(mNetworkQuirkMetrics).statsWrite();
-        reset(mNetworkQuirkMetrics);
-        synchronized (apfFilter) {
-            apfFilter.installNewProgramLocked();
-        }
-        verify(mNetworkQuirkMetrics).setEvent(NetworkQuirkEvent.QE_APF_INSTALL_FAILURE);
-        verify(mNetworkQuirkMetrics).statsWrite();
-    }
-
-    @Test
-    public void testApfProgramOverSize_LegacyApfFilter() throws Exception {
-        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
-        final ApfConfiguration config = getDefaultConfig();
-        config.apfVersionSupported = 2;
-        config.apfRamSize = 512;
-        final TestAndroidPacketFilter apfFilter = makeTestApfFilter(config, ipClientCallback);
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
-        final byte[] ra = buildLargeRa();
-        apfFilter.pretendPacketReceived(ra);
-        // The generated program size will be 529, which is larger than 512
-        program = ipClientCallback.assertProgramUpdateAndGet();
-        verify(mNetworkQuirkMetrics).setEvent(NetworkQuirkEvent.QE_APF_OVER_SIZE_FAILURE);
-        verify(mNetworkQuirkMetrics).statsWrite();
-    }
-
-    @Test
-    public void testGenerateApfProgramException_LegacyApfFilter() throws Exception {
-        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
-        final ApfConfiguration config = getDefaultConfig();
-        final TestAndroidPacketFilter apfFilter;
-        apfFilter = new TestLegacyApfFilter(mContext, config, ipClientCallback, mIpConnectivityLog,
-                mNetworkQuirkMetrics, mDependencies,
-                true /* throwsExceptionWhenGeneratesProgram */);
-        synchronized (apfFilter) {
-            apfFilter.installNewProgramLocked();
-        }
-        verify(mNetworkQuirkMetrics).setEvent(NetworkQuirkEvent.QE_APF_GENERATE_FILTER_EXCEPTION);
-        verify(mNetworkQuirkMetrics).statsWrite();
-    }
-
-    @Test
-    public void testApfSessionInfoMetrics_LegacyApfFilter() throws Exception {
-        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
-        final ApfConfiguration config = getDefaultConfig();
-        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);
-        int maxProgramSize = 0;
-        int numProgramUpdated = 0;
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
-        maxProgramSize = Math.max(maxProgramSize, program.length);
-        numProgramUpdated++;
-
-        final byte[] data = new byte[Counter.totalSize()];
-        final byte[] expectedData = data.clone();
-        final int totalPacketsCounterIdx = Counter.totalSize() + Counter.TOTAL_PACKETS.offset();
-        final int passedIpv6IcmpCounterIdx =
-                Counter.totalSize() + Counter.PASSED_IPV6_ICMP.offset();
-        final int droppedIpv4MulticastIdx =
-                Counter.totalSize() + Counter.DROPPED_IPV4_MULTICAST.offset();
-
-        // Receive an RA packet (passed).
-        final byte[] ra = buildLargeRa();
-        expectedData[totalPacketsCounterIdx + 3] += 1;
-        expectedData[passedIpv6IcmpCounterIdx + 3] += 1;
-        assertDataMemoryContentsIgnoreVersion(PASS, program, ra, data, expectedData);
-        apfFilter.pretendPacketReceived(ra);
-        program = ipClientCallback.assertProgramUpdateAndGet();
-        maxProgramSize = Math.max(maxProgramSize, program.length);
-        numProgramUpdated++;
-
-        apfFilter.setMulticastFilter(true);
-        // setMulticastFilter will trigger program installation.
-        program = ipClientCallback.assertProgramUpdateAndGet();
-        maxProgramSize = Math.max(maxProgramSize, program.length);
-        numProgramUpdated++;
-
-        // Receive IPv4 multicast packet (dropped).
-        final byte[] multicastIpv4Addr = {(byte) 224, 0, 0, 1};
-        ByteBuffer mcastv4packet = makeIpv4Packet(IPPROTO_UDP);
-        put(mcastv4packet, IPV4_DEST_ADDR_OFFSET, multicastIpv4Addr);
-        expectedData[totalPacketsCounterIdx + 3] += 1;
-        expectedData[droppedIpv4MulticastIdx + 3] += 1;
-        assertDataMemoryContentsIgnoreVersion(DROP, program, mcastv4packet.array(), data,
-                expectedData);
-
-        // Set data snapshot and update counters.
-        apfFilter.setDataSnapshot(data);
-
-        // Write metrics data to statsd pipeline when shutdown.
-        doReturn(startTimeMs + durationTimeMs).when(mClock).elapsedRealtime();
-        apfFilter.shutdown();
-        verify(mApfSessionInfoMetrics).setVersion(4);
-        verify(mApfSessionInfoMetrics).setMemorySize(4096);
-
-        // Verify Counters
-        final Map<Counter, Long> expectedCounters = Map.of(Counter.TOTAL_PACKETS, 2L,
-                Counter.PASSED_IPV6_ICMP, 1L, Counter.DROPPED_IPV4_MULTICAST, 1L);
-        final ArgumentCaptor<Counter> counterCaptor = ArgumentCaptor.forClass(Counter.class);
-        final ArgumentCaptor<Long> valueCaptor = ArgumentCaptor.forClass(Long.class);
-        verify(mApfSessionInfoMetrics, times(expectedCounters.size())).addApfCounter(
-                counterCaptor.capture(), valueCaptor.capture());
-        final List<Counter> counters = counterCaptor.getAllValues();
-        final List<Long> values = valueCaptor.getAllValues();
-        final ArrayMap<Counter, Long> capturedCounters = new ArrayMap<>();
-        for (int i = 0; i < counters.size(); i++) {
-            capturedCounters.put(counters.get(i), values.get(i));
-        }
-        assertEquals(expectedCounters, capturedCounters);
-
-        verify(mApfSessionInfoMetrics).setApfSessionDurationSeconds(
-                (int) (durationTimeMs / DateUtils.SECOND_IN_MILLIS));
-        verify(mApfSessionInfoMetrics).setNumOfTimesApfProgramUpdated(numProgramUpdated);
-        verify(mApfSessionInfoMetrics).setMaxProgramSize(maxProgramSize);
-        verify(mApfSessionInfoMetrics).statsWrite();
-    }
-
-    @Test
-    public void testIpClientRaInfoMetrics_LegacyApfFilter() throws Exception {
-        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
-        final ApfConfiguration config = getDefaultConfig();
-        final long startTimeMs = 12345;
-        final long durationTimeMs = config.minMetricsSessionDurationMs;
-        doReturn(startTimeMs).when(mClock).elapsedRealtime();
-        final TestAndroidPacketFilter apfFilter = makeTestApfFilter(config, ipClientCallback);
-        byte[] program = ipClientCallback.assertProgramUpdateAndGet();
-
-        final int routerLifetime = 1000;
-        final int prefixValidLifetime = 200;
-        final int prefixPreferredLifetime = 100;
-        final int rdnssLifetime  = 300;
-        final int routeLifetime  = 400;
-
-        // Construct 2 RAs with partial lifetimes larger than predefined constants
-        final RaPacketBuilder ra1 = new RaPacketBuilder(routerLifetime);
-        ra1.addPioOption(prefixValidLifetime + 123, prefixPreferredLifetime, "2001:db8::/64");
-        ra1.addRdnssOption(rdnssLifetime, "2001:4860:4860::8888", "2001:4860:4860::8844");
-        ra1.addRioOption(routeLifetime + 456, "64:ff9b::/96");
-        final RaPacketBuilder ra2 = new RaPacketBuilder(routerLifetime + 123);
-        ra2.addPioOption(prefixValidLifetime, prefixPreferredLifetime, "2001:db9::/64");
-        ra2.addRdnssOption(rdnssLifetime + 456, "2001:4860:4860::8888", "2001:4860:4860::8844");
-        ra2.addRioOption(routeLifetime, "64:ff9b::/96");
-
-        // Construct an invalid RA packet
-        final RaPacketBuilder raInvalid = new RaPacketBuilder(routerLifetime);
-        raInvalid.addZeroLengthOption();
-
-        // Construct 4 different kinds of zero lifetime RAs
-        final RaPacketBuilder raZeroRouterLifetime = new RaPacketBuilder(0 /* routerLft */);
-        final RaPacketBuilder raZeroPioValidLifetime = new RaPacketBuilder(routerLifetime);
-        raZeroPioValidLifetime.addPioOption(0, prefixPreferredLifetime, "2001:db10::/64");
-        final RaPacketBuilder raZeroRdnssLifetime = new RaPacketBuilder(routerLifetime);
-        raZeroRdnssLifetime.addPioOption(
-                prefixValidLifetime, prefixPreferredLifetime, "2001:db11::/64");
-        raZeroRdnssLifetime.addRdnssOption(0, "2001:4860:4860::8888", "2001:4860:4860::8844");
-        final RaPacketBuilder raZeroRioRouteLifetime = new RaPacketBuilder(routerLifetime);
-        raZeroRioRouteLifetime.addPioOption(
-                prefixValidLifetime, prefixPreferredLifetime, "2001:db12::/64");
-        raZeroRioRouteLifetime.addRioOption(0, "64:ff9b::/96");
-
-        // Inject RA packets. Calling assertProgramUpdateAndGet()/assertNoProgramUpdate() is to make
-        // sure that the RA packet has been processed.
-        apfFilter.pretendPacketReceived(ra1.build());
-        program = ipClientCallback.assertProgramUpdateAndGet();
-        apfFilter.pretendPacketReceived(ra2.build());
-        program = ipClientCallback.assertProgramUpdateAndGet();
-        apfFilter.pretendPacketReceived(raInvalid.build());
-        ipClientCallback.assertNoProgramUpdate();
-        apfFilter.pretendPacketReceived(raZeroRouterLifetime.build());
-        ipClientCallback.assertNoProgramUpdate();
-        apfFilter.pretendPacketReceived(raZeroPioValidLifetime.build());
-        ipClientCallback.assertNoProgramUpdate();
-        apfFilter.pretendPacketReceived(raZeroRdnssLifetime.build());
-        ipClientCallback.assertNoProgramUpdate();
-        apfFilter.pretendPacketReceived(raZeroRioRouteLifetime.build());
-        ipClientCallback.assertNoProgramUpdate();
-
-        // Write metrics data to statsd pipeline when shutdown.
-        doReturn(startTimeMs + durationTimeMs).when(mClock).elapsedRealtime();
-        apfFilter.shutdown();
-
-        // Verify each metric fields in IpClientRaInfoMetrics.
-        // LegacyApfFilter will purge expired RAs before adding new RA. Every time a new zero
-        // lifetime RA is received, zero lifetime RAs except the newly added one will be
-        // cleared, so the number of distinct RAs is 3 (ra1, ra2 and the newly added RA).
-        verify(mIpClientRaInfoMetrics).setMaxNumberOfDistinctRas(3);
-        verify(mIpClientRaInfoMetrics).setNumberOfZeroLifetimeRas(4);
-        verify(mIpClientRaInfoMetrics).setNumberOfParsingErrorRas(1);
-        verify(mIpClientRaInfoMetrics).setLowestRouterLifetimeSeconds(routerLifetime);
-        verify(mIpClientRaInfoMetrics).setLowestPioValidLifetimeSeconds(prefixValidLifetime);
-        verify(mIpClientRaInfoMetrics).setLowestRioRouteLifetimeSeconds(routeLifetime);
-        verify(mIpClientRaInfoMetrics).setLowestRdnssLifetimeSeconds(rdnssLifetime);
-        verify(mIpClientRaInfoMetrics).statsWrite();
-    }
-
-    @Test
-    public void testNoMetricsWrittenForShortDuration_LegacyApfFilter() throws Exception {
-        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
-        final ApfConfiguration config = getDefaultConfig();
-        final long startTimeMs = 12345;
-        final long durationTimeMs = config.minMetricsSessionDurationMs;
-
-        // Verify no metrics data written to statsd for duration less than durationTimeMs.
-        doReturn(startTimeMs).when(mClock).elapsedRealtime();
-        final TestAndroidPacketFilter apfFilter = makeTestApfFilter(config, ipClientCallback);
-        doReturn(startTimeMs + durationTimeMs - 1).when(mClock).elapsedRealtime();
-        apfFilter.shutdown();
-        verify(mApfSessionInfoMetrics, never()).statsWrite();
-        verify(mIpClientRaInfoMetrics, never()).statsWrite();
-
-        // Verify metrics data written to statsd for duration greater than or equal to
-        // durationTimeMs.
-        LegacyApfFilter.Clock clock = mock(LegacyApfFilter.Clock.class);
-        doReturn(startTimeMs).when(clock).elapsedRealtime();
-        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 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 Clock());
-        }
-
-        TestLegacyApfFilter(Context context, ApfFilter.ApfConfiguration config,
-                MockIpClientCallback ipClientCallback, IpConnectivityLog ipConnectivityLog,
-                NetworkQuirkMetrics networkQuirkMetrics, ApfFilter.Dependencies dependencies,
-                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, 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/apf/TestAndroidPacketFilter.java b/tests/unit/src/android/net/apf/TestAndroidPacketFilter.java
deleted file mode 100644
index 39386cd..0000000
--- a/tests/unit/src/android/net/apf/TestAndroidPacketFilter.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2023 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.system.ErrnoException;
-
-import java.io.IOException;
-
-/**
- * The interface for TestAndroidPacketFilter
- */
-public interface TestAndroidPacketFilter extends AndroidPacketFilter {
-    /**
-     * Pretend an RA packet has been received and show it to ApfFilter.
-     */
-    void pretendPacketReceived(byte[] packet) throws IOException, ErrnoException;
-
-    /**
-     * Generate and install a new filter program.
-     */
-    void installNewProgramLocked();
-}
diff --git a/tests/unit/src/android/net/ip/ConnectivityPacketTrackerTest.kt b/tests/unit/src/android/net/ip/ConnectivityPacketTrackerTest.kt
index 51a871d..675e6c8 100644
--- a/tests/unit/src/android/net/ip/ConnectivityPacketTrackerTest.kt
+++ b/tests/unit/src/android/net/ip/ConnectivityPacketTrackerTest.kt
@@ -39,6 +39,7 @@
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
+import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
 import org.mockito.Mockito
@@ -78,7 +79,7 @@
         MockitoAnnotations.initMocks(this)
         val readSocket = FileDescriptor()
         Os.socketpair(AF_UNIX, SOCK_STREAM or SOCK_NONBLOCK, 0, writeSocket, readSocket)
-        doReturn(readSocket).`when`(mDependencies).createPacketReaderSocket(anyInt())
+        doReturn(readSocket).`when`(mDependencies).createPacketReaderSocket(anyInt(), anyBoolean())
         doReturn(TEST_MAX_CAPTURE_PKT_SIZE).`when`(mDependencies).maxCapturePktSize
     }
 
@@ -123,6 +124,26 @@
     }
 
     @Test
+    fun testPacketMatchPattern() {
+        val packetTracker = getConnectivityPacketTracker()
+        // Using scapy to generate ARP request packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="01:02:03:04:05:06")
+        // arp = ARP()
+        // pkt = eth/arp
+        val arpPkt = """
+            010203040506000102030405080600010800060400015c857e3c74e1c0a8012200000000000000000000
+        """.replace("\\s+".toRegex(), "").trim().uppercase()
+        val arpPktByteArray = HexDump.hexStringToByteArray(arpPkt)
+
+        // start capture packet
+        setCapture(packetTracker, true)
+
+        pretendPacketReceive(arpPktByteArray)
+        assertEquals(1, getMatchedPacketCount(packetTracker, arpPkt))
+        assertEquals(1, getMatchedPacketCount(packetTracker, arpPkt.lowercase()))
+    }
+
+    @Test
     fun testMaxCapturePacketSize() {
         doReturn(3).`when`(mDependencies).maxCapturePktSize
         val packetTracker = getConnectivityPacketTracker(mDependencies)
@@ -171,7 +192,13 @@
         val result = CompletableFuture<ConnectivityPacketTracker>()
         handler.post {
             try {
-                val tracker = ConnectivityPacketTracker(handler, ifParams, localLog, dependencies)
+                val tracker = ConnectivityPacketTracker(
+                    handler,
+                    ifParams,
+                    localLog,
+                    dependencies,
+                    true
+                )
                 tracker.start(TAG)
                 result.complete(tracker)
             } catch (e: Exception) {
@@ -230,4 +257,4 @@
 
         return result.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
     }
-}
\ No newline at end of file
+}
diff --git a/tests/unit/src/android/net/ip/IpClientTest.java b/tests/unit/src/android/net/ip/IpClientTest.java
index f5fd22b..1527714 100644
--- a/tests/unit/src/android/net/ip/IpClientTest.java
+++ b/tests/unit/src/android/net/ip/IpClientTest.java
@@ -35,6 +35,7 @@
 import static com.android.net.module.util.netlink.NetlinkConstants.RTN_UNICAST;
 import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_ACK;
 import static com.android.net.module.util.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+import static com.android.networkstack.util.NetworkStackUtils.APF_ENABLE;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
@@ -76,8 +77,8 @@
 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;
 import android.net.apf.ApfFilter.ApfConfiguration;
 import android.net.ip.IpClientLinkObserver.IpClientNetlinkMonitor;
 import android.net.ip.IpClientLinkObserver.IpClientNetlinkMonitor.INetlinkMessageProcessor;
@@ -191,8 +192,8 @@
     @Mock private FileDescriptor mFd;
     @Mock private PrintWriter mWriter;
     @Mock private IpClientNetlinkMonitor mNetlinkMonitor;
-    @Mock private AndroidPacketFilter mApfFilter;
     @Mock private PackageManager mPackageManager;
+    @Mock private ApfFilter mApfFilter;
 
     private InterfaceParams mIfParams;
     private INetlinkMessageProcessor mNetlinkMessageProcessor;
@@ -216,10 +217,10 @@
         when(mDependencies.getDeviceConfigPropertyInt(eq(CONFIG_SOCKET_RECV_BUFSIZE), anyInt()))
                 .thenReturn(SOCKET_RECV_BUFSIZE);
         when(mDependencies.makeIpClientNetlinkMonitor(
-                any(), any(), any(), anyInt(), any())).thenReturn(mNetlinkMonitor);
+                any(), any(), any(), anyInt(), anyBoolean(), any())).thenReturn(mNetlinkMonitor);
         when(mNetlinkMonitor.start()).thenReturn(true);
-        when(mContext.getPackageManager()).thenReturn(mPackageManager);
-        when(mPackageManager.hasSystemFeature(eq(PackageManager.FEATURE_WATCH))).thenReturn(false);
+        doReturn(mPackageManager).when(mContext).getPackageManager();
+        doReturn(true).when(mDependencies).isFeatureNotChickenedOut(mContext, APF_ENABLE);
 
         mIfParams = null;
     }
@@ -240,7 +241,7 @@
         final ArgumentCaptor<INetlinkMessageProcessor> processorCaptor =
                 ArgumentCaptor.forClass(INetlinkMessageProcessor.class);
         verify(mDependencies).makeIpClientNetlinkMonitor(any(), any(), any(), anyInt(),
-                processorCaptor.capture());
+                anyBoolean(), processorCaptor.capture());
         mNetlinkMessageProcessor = processorCaptor.getValue();
         reset(mNetd);
         // Verify IpClient doesn't call onLinkPropertiesChange() when it starts.
@@ -356,7 +357,6 @@
         mNetlinkMessageProcessor.processNetlinkMessage(msg, TEST_UNUSED_REAL_TIME /* whenMs */);
     }
 
-
     @Test
     public void testNullInterfaceNameMostDefinitelyThrows() throws Exception {
         setTestInterfaceParams(null);
@@ -855,10 +855,10 @@
                 ApfConfiguration.class);
         if (isApfSupported) {
             verify(mDependencies, timeout(TEST_TIMEOUT_MS)).maybeCreateApfFilter(
-                    any(), any(), configCaptor.capture(), any(), any(), any(), anyBoolean());
+                    any(), any(), configCaptor.capture(), any(), any(), any());
         } else {
             verify(mDependencies, never()).maybeCreateApfFilter(
-                    any(), any(), configCaptor.capture(), any(), any(), any(), anyBoolean());
+                    any(), any(), configCaptor.capture(), any(), any(), any());
         }
 
         return isApfSupported ? configCaptor.getValue() : null;
@@ -927,7 +927,7 @@
         final ArgumentCaptor<ApfConfiguration> configCaptor = ArgumentCaptor.forClass(
                 ApfConfiguration.class);
         verify(mDependencies, timeout(TEST_TIMEOUT_MS)).maybeCreateApfFilter(
-                any(), any(), configCaptor.capture(), any(), any(), any(), anyBoolean());
+                any(), any(), configCaptor.capture(), any(), any(), any());
         final ApfConfiguration actual = configCaptor.getValue();
         assertNotNull(actual);
         assertEquals(SdkLevel.isAtLeastS() ? 4 : 3, actual.apfVersionSupported);
@@ -937,6 +937,28 @@
     }
 
     @Test
+    public void testForceApfV2OnLowRam() throws Exception {
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        ProvisioningConfiguration.Builder config = new ProvisioningConfiguration.Builder()
+                .withoutIPv4()
+                .withoutIpReachabilityMonitor()
+                .withInitialConfiguration(
+                        conf(links(TEST_LOCAL_ADDRESSES), prefixes(TEST_PREFIXES), ips()))
+                .withApfCapabilities(
+                        new ApfCapabilities(3 /* version */, 512 /* maxProgramSize */,
+                                ARPHRD_ETHER));
+        ipc.startProvisioning(config.build());
+        final ArgumentCaptor<ApfConfiguration> configCaptor = ArgumentCaptor.forClass(
+                ApfConfiguration.class);
+        verify(mDependencies, timeout(TEST_TIMEOUT_MS)).maybeCreateApfFilter(
+                any(), any(), configCaptor.capture(), any(), any(), any());
+
+        final ApfConfiguration apfConfig = configCaptor.getValue();
+        assertEquals(2, apfConfig.apfVersionSupported);
+        verifyShutdown(ipc);
+    }
+
+    @Test
     public void testDumpApfFilter_withNoException() throws Exception {
         final IpClient ipc = makeIpClient(TEST_IFNAME);
         final ApfConfiguration config = verifyApfFilterCreatedOnStart(ipc,
@@ -962,7 +984,7 @@
         ipc.updateApfCapabilities(newApfCapabilities);
         HandlerUtils.waitForIdle(ipc.getHandler(), TEST_TIMEOUT_MS);
         verify(mDependencies, never()).maybeCreateApfFilter(any(), any(), any(), any(), any(),
-                any(), anyBoolean());
+                any());
         verifyShutdown(ipc);
     }
 
@@ -978,16 +1000,54 @@
         ipc.updateApfCapabilities(null /* apfCapabilities */);
         HandlerUtils.waitForIdle(ipc.getHandler(), TEST_TIMEOUT_MS);
         verify(mDependencies, never()).maybeCreateApfFilter(any(), any(), any(), any(), any(),
-                any(), anyBoolean());
+                any());
         verifyShutdown(ipc);
     }
 
     @Test
+    public void testApfUpdateCapabilities_raceBetweenStopAndStartIpClient() throws Exception {
+        final IpClient ipc = makeIpClient(TEST_IFNAME);
+        ProvisioningConfiguration.Builder config = new ProvisioningConfiguration.Builder()
+                .withoutIPv4()
+                .withoutIpReachabilityMonitor()
+                .withInitialConfiguration(
+                        conf(links(TEST_LOCAL_ADDRESSES), prefixes(TEST_PREFIXES), ips()))
+                .withApfCapabilities(new ApfCapabilities(4 /* version */,
+                    4096 /* maxProgramSize */, ARPHRD_ETHER));
+        ipc.startProvisioning(config.build());
+
+        // Verify that APF filter can be created successfully.
+        ArgumentCaptor<ApfConfiguration> configCaptor = ArgumentCaptor.forClass(
+                ApfConfiguration.class);
+        verify(mDependencies, timeout(TEST_TIMEOUT_MS)).maybeCreateApfFilter(
+                any(), any(), configCaptor.capture(), any(), any(), any());
+        ApfConfiguration apfConfig = configCaptor.getValue();
+        assertEquals(SdkLevel.isAtLeastS() ? 4 : 3, apfConfig.apfVersionSupported);
+        assertEquals(4096, apfConfig.apfRamSize);
+
+        clearInvocations(mDependencies);
+
+        // Simulate stopping IpClient and restarting provisioning immediately, verify IpClient
+        // can still create APF filter successfully, make sure the race of mApfCapabilities
+        // initialization has been fixed.
+        ipc.stop();
+        // Update the maxProgramSize to differentiate with above APF config.
+        config.withApfCapabilities(new ApfCapabilities(4 /* version */,
+                2048 /* maxProgramSize */, ARPHRD_ETHER));
+        ipc.startProvisioning(config.build());
+        verify(mDependencies, timeout(TEST_TIMEOUT_MS)).maybeCreateApfFilter(
+                any(), any(), configCaptor.capture(), any(), any(), any());
+        apfConfig = configCaptor.getValue();
+        assertEquals(SdkLevel.isAtLeastS() ? 4 : 3, apfConfig.apfVersionSupported);
+        assertEquals(2048, apfConfig.apfRamSize);
+    }
+
+    @Test
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testVendorNdOffloadDisabledWhenApfV6Supported() throws Exception {
-        when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(), any(),
-                anyBoolean())).thenReturn(mApfFilter);
-        when(mApfFilter.supportNdOffload()).thenReturn(true);
+        when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(),
+                any())).thenReturn(mApfFilter);
+        when(mApfFilter.enableNdOffload()).thenReturn(true);
         final IpClient ipc = makeIpClient(TEST_IFNAME);
         ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                 .withoutIPv4()
@@ -1011,9 +1071,9 @@
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testVendorNdOffloadEnabledWhenApfV6NotSupported() throws Exception {
-        when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(), any(),
-                anyBoolean())).thenReturn(mApfFilter);
-        when(mApfFilter.supportNdOffload()).thenReturn(false);
+        when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(),
+                any())).thenReturn(mApfFilter);
+        when(mApfFilter.enableNdOffload()).thenReturn(false);
         final IpClient ipc = makeIpClient(TEST_IFNAME);
         ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                 .withoutIPv4()
@@ -1035,9 +1095,9 @@
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public void testVendorNdOffloadDisabledWhenApfCapabilitiesUpdated() throws Exception {
-        when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(), any(),
-                anyBoolean())).thenReturn(mApfFilter);
-        when(mApfFilter.supportNdOffload()).thenReturn(true);
+        when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(),
+                any())).thenReturn(mApfFilter);
+        when(mApfFilter.enableNdOffload()).thenReturn(true);
         final IpClient ipc = makeIpClient(TEST_IFNAME);
         ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
                 .withoutIPv4()
@@ -1058,8 +1118,8 @@
 
     @Test
     public void testLinkPropertiesUpdate_callSetLinkPropertiesOnApfFilter() throws Exception {
-        when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(), any(),
-                anyBoolean())).thenReturn(mApfFilter);
+        when(mDependencies.maybeCreateApfFilter(any(), any(), any(), any(), any(),
+                any())).thenReturn(mApfFilter);
         final IpClient ipc = makeIpClient(TEST_IFNAME);
         verifyApfFilterCreatedOnStart(ipc, true /* isApfSupported */);
         onInterfaceAddressUpdated(
diff --git a/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt b/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt
index 518cec7..343848b 100644
--- a/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt
+++ b/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt
@@ -59,7 +59,6 @@
 import com.android.net.module.util.netlink.StructNdMsg.NUD_REACHABLE
 import com.android.net.module.util.netlink.StructNdMsg.NUD_STALE
 import com.android.networkstack.metrics.IpReachabilityMonitorMetrics
-import com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION
 import com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION
 import com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION
 import com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_ROUTER_MAC_CHANGE_FAILURE_ONLY_AFTER_ROAM_VERSION
@@ -334,6 +333,10 @@
     fun testLoseProvisioning_FirstProbeIsFailed() {
         reachabilityMonitor.updateLinkProperties(TEST_LINK_PROPERTIES)
 
+        // Make the IPv4 DNS as reachable first.
+        neighborMonitor.enqueuePacket(makeNewNeighMessage(TEST_IPV4_DNS, NUD_REACHABLE))
+        handlerThread.waitForIdle(TEST_TIMEOUT_MS)
+
         neighborMonitor.enqueuePacket(makeNewNeighMessage(TEST_IPV4_DNS, NUD_FAILED))
         verify(callback, timeout(TEST_TIMEOUT_MS)).notifyLost(
             anyString(),
@@ -341,6 +344,10 @@
         )
     }
 
+    // Given the flag which ignores the NUD failure from the neighbor that is never reachable
+    // before has been enabled by default, we have to make the neighbor as reachable first and
+    // simulate a NUD failure by making a new NUD_FAILED neighbor message. So change the param
+    // "everReachable" to true always.
     private fun runLoseProvisioningTest(
         newLp: LinkProperties,
         lostNeighbor: InetAddress,
@@ -350,7 +357,7 @@
                 newLp,
                 lostNeighbor,
                 eventType,
-                false, /* everReachable */
+                true, /* everReachable */
                 true /* expectedNotifyLost */
         )
     }
@@ -368,11 +375,21 @@
         neighborMonitor.enqueuePacket(makeNewNeighMessage(TEST_IPV6_GATEWAY, NUD_STALE))
         neighborMonitor.enqueuePacket(makeNewNeighMessage(TEST_IPV4_DNS, NUD_STALE))
         neighborMonitor.enqueuePacket(makeNewNeighMessage(TEST_IPV6_DNS, NUD_STALE))
+        neighborMonitor.enqueuePacket(
+            makeNewNeighMessage(TEST_IPV6_LINKLOCAL_SCOPED_GATEWAY, NUD_STALE)
+        )
+        neighborMonitor.enqueuePacket(makeNewNeighMessage(TEST_IPV4_GATEWAY_DNS, NUD_STALE))
+
+        // Make all neighbors used in the test as reachable.
         if (everReachable) {
             neighborMonitor.enqueuePacket(makeNewNeighMessage(TEST_IPV4_DNS, NUD_REACHABLE))
-            neighborMonitor.enqueuePacket(makeNewNeighMessage(TEST_IPV4_GATEWAY, NUD_REACHABLE))
             neighborMonitor.enqueuePacket(makeNewNeighMessage(TEST_IPV6_DNS, NUD_REACHABLE))
             neighborMonitor.enqueuePacket(makeNewNeighMessage(TEST_IPV6_GATEWAY, NUD_REACHABLE))
+            neighborMonitor.enqueuePacket(makeNewNeighMessage(TEST_IPV4_GATEWAY, NUD_REACHABLE))
+            neighborMonitor.enqueuePacket(
+                makeNewNeighMessage(TEST_IPV6_LINKLOCAL_SCOPED_GATEWAY, NUD_REACHABLE)
+            )
+            neighborMonitor.enqueuePacket(makeNewNeighMessage(TEST_IPV4_GATEWAY_DNS, NUD_REACHABLE))
         }
 
         neighborMonitor.enqueuePacket(makeNewNeighMessage(lostNeighbor, NUD_PROBE))
@@ -517,7 +534,6 @@
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = true)
     fun testLoseProvisioning_ignoreNeverReachableIpv6GatewayLost() {
         runLoseProvisioningTest(
             TEST_LINK_PROPERTIES,
@@ -529,7 +545,6 @@
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = true)
     fun testLoseProvisioning_ignoreNeverReachableIpv6DnsLost() {
         runLoseProvisioningTest(
             TEST_LINK_PROPERTIES,
@@ -541,7 +556,6 @@
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = true)
     fun testLoseProvisioning_notIgnoreEverReachableIpv6GatewayLost() {
         runLoseProvisioningTest(
             TEST_LINK_PROPERTIES,
@@ -553,7 +567,6 @@
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = true)
     fun testLoseProvisioning_notIgnoreEverReachableIpv6DnsLost() {
         runLoseProvisioningTest(
             TEST_LINK_PROPERTIES,
@@ -565,7 +578,6 @@
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = true)
     fun testLoseProvisioning_ignoreNeverReachableIpv4DnsLost() {
         runLoseProvisioningTest(
             TEST_LINK_PROPERTIES,
@@ -577,7 +589,6 @@
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = true)
     fun testLoseProvisioning_notIgnoreEverReachableIpv4GatewayLost() {
         runLoseProvisioningTest(
             TEST_LINK_PROPERTIES,
@@ -589,7 +600,6 @@
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = true)
     fun testLoseProvisioning_notIgnoreEverReachableIpv4DnsLost() {
         runLoseProvisioningTest(
             TEST_LINK_PROPERTIES,
@@ -601,7 +611,6 @@
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = true)
     fun testLoseProvisioning_ignoreNeverReachableIpv6GatewayLost_withTwoIPv6DnsServers() {
         reachabilityMonitor.updateLinkProperties(TEST_DUAL_LINK_PROPERTIES)
 
@@ -639,7 +648,6 @@
     }
 
     @Test
-    @Flag(name = IP_REACHABILITY_IGNORE_NEVER_REACHABLE_NEIGHBOR_VERSION, enabled = true)
     fun testLoseProvisioning_ignoreNeverReachableIpv6DnsLost_withTwoIPv6Routes() {
         val TEST_DUAL_IPV6_ROUTERS_LINK_PROPERTIES = LinkProperties().apply {
             interfaceName = TEST_IFACE.name
@@ -861,7 +869,7 @@
             TEST_IPV6_LINKLOCAL_SCOPED_GATEWAY,
             NUD_CONFIRM_FAILED_CRITICAL,
             IPV6,
-                NUD_NEIGHBOR_GATEWAY
+            NUD_NEIGHBOR_GATEWAY
         )
     }
 
@@ -927,7 +935,7 @@
             TEST_IPV6_LINKLOCAL_SCOPED_GATEWAY,
             NUD_ORGANIC_FAILED_CRITICAL,
             IPV6,
-                NUD_NEIGHBOR_GATEWAY
+            NUD_NEIGHBOR_GATEWAY
         )
     }
 
diff --git a/tests/unit/src/android/net/ip/MulticastReportMonitorTest.kt b/tests/unit/src/android/net/ip/MulticastReportMonitorTest.kt
new file mode 100644
index 0000000..cbd2b17
--- /dev/null
+++ b/tests/unit/src/android/net/ip/MulticastReportMonitorTest.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.ip
+
+import android.net.MacAddress
+import android.net.ip.MulticastReportMonitor.Callback
+import android.os.Handler
+import android.os.HandlerThread
+import android.system.Os
+import android.system.OsConstants.AF_UNIX
+import android.system.OsConstants.SOCK_NONBLOCK
+import android.system.OsConstants.SOCK_STREAM
+import androidx.test.filters.SmallTest
+import com.android.net.module.util.HexDump
+import com.android.net.module.util.InterfaceParams
+import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.visibleOnHandlerThread
+import com.android.testutils.waitForIdle
+import java.io.FileDescriptor
+import libcore.io.IoUtils
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.timeout
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+/**
+ * Test for MulticastReportMonitor.
+ */
+@SmallTest
+@DevSdkIgnoreRunner.MonitorThreadLeak
+class MulticastReportMonitorTest {
+    companion object {
+        private const val TIMEOUT_MS: Long = 1000
+        private const val SLEEP_TIMEOUT_MS: Long = 100
+        private val TAG = this::class.simpleName
+    }
+
+    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 handlerThread by lazy {
+        HandlerThread("$TAG-handler-thread").apply{ start() }
+    }
+    private val handler by lazy { Handler(handlerThread.looper) }
+    private var writeSocket = FileDescriptor()
+    private lateinit var mMulticastReportMonitor: MulticastReportMonitor
+
+    @Mock private lateinit var callback: Callback
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        val readSocket = FileDescriptor()
+        Os.socketpair(AF_UNIX, SOCK_STREAM or SOCK_NONBLOCK, 0, writeSocket, readSocket)
+        mMulticastReportMonitor = MulticastReportMonitor(handler, ifParams, callback, readSocket)
+        visibleOnHandlerThread(handler) {
+            mMulticastReportMonitor.start()
+        }
+    }
+
+    @After
+    fun tearDown() {
+        IoUtils.closeQuietly(writeSocket)
+        handler.waitForIdle(TIMEOUT_MS)
+        Mockito.framework().clearInlineMocks()
+        handlerThread.quitSafely()
+        handlerThread.join()
+    }
+
+    @Test
+    fun testMulticastReportMonitorCallback() {
+        val matchedPacket = HexDump.hexStringToByteArray("000000")
+        val pktCnt = 2
+        for (i in 0..<pktCnt) {
+            Os.write(writeSocket, matchedPacket, 0, matchedPacket.size)
+            Thread.sleep(SLEEP_TIMEOUT_MS)
+        }
+        verify(callback, timeout(TIMEOUT_MS).times(pktCnt)).notifyMulticastAddrChange()
+    }
+}
diff --git a/tests/unit/src/com/android/networkstack/NetworkStackNotifierTest.kt b/tests/unit/src/com/android/networkstack/NetworkStackNotifierTest.kt
index efd4069..88319d4 100644
--- a/tests/unit/src/com/android/networkstack/NetworkStackNotifierTest.kt
+++ b/tests/unit/src/com/android/networkstack/NetworkStackNotifierTest.kt
@@ -67,6 +67,7 @@
 import org.mockito.MockitoAnnotations
 import kotlin.reflect.KClass
 import kotlin.test.assertEquals
+import kotlin.test.assertTrue
 
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
@@ -206,6 +207,10 @@
         assertEquals(mPendingIntent, note.contentIntent)
         assertEquals(CHANNEL_CONNECTED, note.channelId)
         assertEquals(timeout, note.timeoutAfter)
+        assertTrue(
+            note.flags and Notification.FLAG_LOCAL_ONLY != 0,
+            "Connected notifications should be local only"
+        )
         verify(mDependencies).getActivityPendingIntent(
                 eq(mCurrentUserContext), mIntentCaptor.capture(),
                 intThat { it or FLAG_IMMUTABLE != 0 })
@@ -294,6 +299,10 @@
                 eq(mCurrentUserContext), mIntentCaptor.capture(),
                 intThat { it or FLAG_IMMUTABLE != 0 })
         verifyVenueInfoIntent(mIntentCaptor.value)
+        assertTrue(
+            mNoteCaptor.value.flags and Notification.FLAG_LOCAL_ONLY != 0,
+            "Venue info notifications should be local only"
+        )
         verifyCanceledNotificationAfterDefaultNetworkLost()
     }
 
diff --git a/tests/unit/src/com/android/networkstack/NetworkStackServiceTest.kt b/tests/unit/src/com/android/networkstack/NetworkStackServiceTest.kt
index 7770eca..7f47f8b 100644
--- a/tests/unit/src/com/android/networkstack/NetworkStackServiceTest.kt
+++ b/tests/unit/src/com/android/networkstack/NetworkStackServiceTest.kt
@@ -29,8 +29,6 @@
 import android.net.dhcp.IDhcpServerCallbacks
 import android.net.ip.IIpClientCallbacks
 import android.net.ip.IpClient
-import android.os.Binder
-import android.os.Build
 import android.os.IBinder
 import android.os.Process
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -42,8 +40,6 @@
 import com.android.server.NetworkStackService.PermissionChecker
 import com.android.server.connectivity.NetworkMonitor
 import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter
-import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.assertThrows
 import java.io.FileDescriptor
 import java.io.PrintWriter
@@ -56,11 +52,9 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.any
-import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mockito.doReturn
 import org.mockito.Mockito.eq
 import org.mockito.Mockito.mock
-import org.mockito.Mockito.spy
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 
@@ -83,8 +77,13 @@
     private val deps = mock(Dependencies::class.java).apply {
         doReturn(mockIpMemoryStoreService).`when`(this).makeIpMemoryStoreService(any())
         doReturn(mockDhcpServer).`when`(this).makeDhcpServer(any(), any(), any(), any())
-        doReturn(mockNetworkMonitor).`when`(this).makeNetworkMonitor(any(), any(), any(), any(),
-                any())
+        doReturn(mockNetworkMonitor).`when`(this).makeNetworkMonitor(
+                any(),
+                any(),
+                any(),
+                any(),
+                any()
+        )
         doReturn(mockIpClient).`when`(this).makeIpClient(any(), any(), any(), any())
     }
     private val netd = mock(INetd::class.java).apply {
@@ -100,22 +99,7 @@
 
     private val connector = NetworkStackConnector(context, permChecker, deps)
 
-    @Test @IgnoreAfter(Build.VERSION_CODES.Q)
-    fun testDumpVersion_Q() {
-        prepareDumpVersionTest()
-
-        val dumpsysOut = StringWriter()
-        connector.dump(FileDescriptor(), PrintWriter(dumpsysOut, true /* autoFlush */),
-                arrayOf("version") /* args */)
-
-        assertEquals("NetworkStack version:\n" +
-                "NetworkStackConnector: ${INetworkStackConnector.VERSION}\n" +
-                "SystemServer: {9990001, 9990002, 9990003, 9990004, 9990005}\n" +
-                "Netd: $TEST_NETD_VERSION\n\n",
-                dumpsysOut.toString())
-    }
-
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test
     fun testDumpVersion() {
         prepareDumpVersionTest()
 
@@ -123,10 +107,14 @@
         val connectorHash = INetworkStackConnector.HASH
 
         val dumpsysOut = StringWriter()
-        connector.dump(FileDescriptor(), PrintWriter(dumpsysOut, true /* autoFlush */),
-                arrayOf("version") /* args */)
+        connector.dump(
+                FileDescriptor(),
+                PrintWriter(dumpsysOut, true /* autoFlush */),
+                arrayOf("version") /* args */
+        )
 
-        assertEquals("NetworkStack version:\n" +
+        assertEquals(
+                "NetworkStack version:\n" +
                 "LocalInterface:$connectorVersion:$connectorHash\n" +
                 "ipmemorystore:9990001:ipmemorystore_hash\n" +
                 "netd:$TEST_NETD_VERSION:$TEST_NETD_HASH\n" +
@@ -134,7 +122,8 @@
                 "networkstack:9990003:networkmonitor_hash\n" +
                 "networkstack:9990004:ipclient_hash\n" +
                 "networkstack:9990005:multiple_use_hash\n\n",
-                dumpsysOut.toString())
+                dumpsysOut.toString()
+        )
     }
 
     fun prepareDumpVersionTest() {
@@ -170,16 +159,9 @@
         verify(mockDhcpCb).onDhcpServerCreated(eq(IDhcpServer.STATUS_SUCCESS), any())
 
         // Call makeNetworkMonitor
-        // Use a spy of INetworkMonitorCallbacks and not a mock, as mockito can't create a mock on Q
-        // because of the missing CaptivePortalData class that is an argument of one of the methods
-        val mockBinder = mock(IBinder::class.java)
-        val mockNetworkMonitorCb = spy(INetworkMonitorCallbacks.Stub.asInterface(mockBinder))
+        val mockNetworkMonitorCb = mock(INetworkMonitorCallbacks::class.java)
         doReturn(9990003).`when`(mockNetworkMonitorCb).interfaceVersion
         doReturn("networkmonitor_hash").`when`(mockNetworkMonitorCb).interfaceHash
-        // Oneway transactions are always successful (return true). INetworkMonitorCallbacks is a
-        // oneway interface. This avoids the stub throwing because the method is not implemented by
-        // the (mock) remote.
-        doReturn(true).`when`(mockBinder).transact(anyInt(), any(), any(), eq(Binder.FLAG_ONEWAY))
 
         connector.makeNetworkMonitor(Network(123), "test_nm", mockNetworkMonitorCb)
 
@@ -187,8 +169,6 @@
         verify(mockNetworkMonitorCb).onNetworkMonitorCreated(any())
 
         // Call makeIpClient
-        // Use a spy of IIpClientCallbacks instead of a mock, as mockito cannot create a mock on Q
-        // because of the missing CaptivePortalData class that is an argument on one of the methods
         val mockIpClientCb = mock(IIpClientCallbacks::class.java)
         doReturn(9990004).`when`(mockIpClientCb).interfaceVersion
         doReturn("ipclient_hash").`when`(mockIpClientCb).interfaceHash
diff --git a/tests/unit/src/com/android/networkstack/ipmemorystore/IpMemoryStoreServiceTest.java b/tests/unit/src/com/android/networkstack/ipmemorystore/IpMemoryStoreServiceTest.java
index ee6c48b..02612f4 100644
--- a/tests/unit/src/com/android/networkstack/ipmemorystore/IpMemoryStoreServiceTest.java
+++ b/tests/unit/src/com/android/networkstack/ipmemorystore/IpMemoryStoreServiceTest.java
@@ -19,6 +19,7 @@
 import static android.net.ip.IpClient.NETWORK_EVENT_NUD_FAILURE_TYPES;
 import static android.net.ip.IpClient.ONE_DAY_IN_MS;
 import static android.net.ip.IpClient.ONE_WEEK_IN_MS;
+import static android.net.ip.IpClient.SIX_HOURS_IN_MS;
 import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM;
 import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_CONFIRM;
 import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC;
@@ -1459,6 +1460,71 @@
     }
 
     @Test
+    public void testStoreNetworkEvent_deleteCluster() {
+        final long now = System.currentTimeMillis();
+        storeNetworkEventsForNudFailures(now);
+
+        // Delete the entries with TEST_CLUSTER from the fixture table.
+        doLatched("Did not finish deleting", latch ->
+                mService.deleteCluster(TEST_CLUSTER, false /* needWipe */,
+                        onDeleteStatus((status, deletedCount) -> {
+                            assertTrue("Delete failed : " + status.resultCode, status.isSuccess());
+                            // The fixture stores 40 events under TEST_CLUSTER
+                            assertEquals("Unexpected deleted count : " + deletedCount,
+                                    40, deletedCount.intValue());
+                            latch.countDown();
+                        })), LONG_TIMEOUT_MS);
+
+        // Query network event counts for NUD failures within TEST_CLUSTER, should be empty given
+        // we've already deleted that cluster.
+        final long[] sinceTimes = new long[3];
+        sinceTimes[0] = now - ONE_WEEK_IN_MS;
+        sinceTimes[1] = now - ONE_DAY_IN_MS;
+        sinceTimes[2] = now - SIX_HOURS_IN_MS;
+        doLatched("Did not complete retrieving network event count", latch ->
+                mService.retrieveNetworkEventCount(TEST_CLUSTER,
+                        sinceTimes,
+                        NETWORK_EVENT_NUD_FAILURE_TYPES,
+                        onNetworkEventCountRetrieved(
+                            (status, counts) -> {
+                                assertTrue("Retrieve network event counts not successful : "
+                                        + status.resultCode, status.isSuccess());
+                                assertTrue(counts.length == 3);
+                                assertEquals(0, counts[0]);
+                                assertEquals(0, counts[1]);
+                                assertEquals(0, counts[2]);
+                                latch.countDown();
+                            })));
+
+        // Delete the entries with TEST_CLUSTER_1 from the fixture table.
+        doLatched("Did not finish deleting", latch ->
+                mService.deleteCluster(TEST_CLUSTER_1, false /* needWipe */,
+                        onDeleteStatus((status, deletedCount) -> {
+                            assertTrue("Delete failed : " + status.resultCode, status.isSuccess());
+                            // The fixture stores 40 events under TEST_CLUSTER
+                            assertEquals("Unexpected deleted count : " + deletedCount,
+                                    20, deletedCount.intValue());
+                            latch.countDown();
+                        })), LONG_TIMEOUT_MS);
+        // Query network event counts for NUD failures within TEST_CLUSTER_1, should be empty given
+        // we've already deleted that cluster as well.
+        doLatched("Did not complete retrieving network event count", latch ->
+                mService.retrieveNetworkEventCount(TEST_CLUSTER_1,
+                        sinceTimes,
+                        NETWORK_EVENT_NUD_FAILURE_TYPES,
+                        onNetworkEventCountRetrieved(
+                            (status, counts) -> {
+                                assertTrue("Retrieve network event counts not successful : "
+                                        + status.resultCode, status.isSuccess());
+                                assertTrue(counts.length == 3);
+                                assertEquals(0, counts[0]);
+                                assertEquals(0, counts[1]);
+                                assertEquals(0, counts[2]);
+                                latch.countDown();
+                            })));
+    }
+
+    @Test
     public void testRenameDb_noExistingDb_newDbCreated() throws Exception {
         mService.shutdown();
         TEST_DB.delete();
diff --git a/tests/unit/src/com/android/networkstack/metrics/ApfSessionInfoMetricsTest.java b/tests/unit/src/com/android/networkstack/metrics/ApfSessionInfoMetricsTest.java
index 69464cf..3e23a14 100644
--- a/tests/unit/src/com/android/networkstack/metrics/ApfSessionInfoMetricsTest.java
+++ b/tests/unit/src/com/android/networkstack/metrics/ApfSessionInfoMetricsTest.java
@@ -94,21 +94,21 @@
     public void testApfSessionInfoMetrics_VerifyApfCounterToEnum() throws Exception {
         verifyCounterName(Counter.RESERVED_OOB, CounterName.CN_UNKNOWN);
         verifyCounterName(Counter.TOTAL_PACKETS, CounterName.CN_TOTAL_PACKETS);
-        verifyCounterName(Counter.PASSED_ARP, CounterName.CN_PASSED_ARP);
         verifyCounterName(Counter.PASSED_DHCP, CounterName.CN_PASSED_DHCP);
+        verifyCounterName(Counter.PASSED_ETHER_OUR_SRC_MAC, CounterName.CN_PASSED_OUR_SRC_MAC);
         verifyCounterName(Counter.PASSED_IPV4, CounterName.CN_PASSED_IPV4);
         verifyCounterName(Counter.PASSED_IPV6_NON_ICMP, CounterName.CN_PASSED_IPV6_NON_ICMP);
-        verifyCounterName(Counter.PASSED_IPV4_UNICAST,  CounterName.CN_PASSED_IPV4_UNICAST);
+        verifyCounterName(Counter.PASSED_IPV4_UNICAST, CounterName.CN_PASSED_IPV4_UNICAST);
+        verifyCounterName(Counter.PASSED_IPV6_HOPOPTS, CounterName.CN_PASSED_IPV6_HOPOPTS);
         verifyCounterName(Counter.PASSED_IPV6_ICMP, CounterName.CN_PASSED_IPV6_ICMP);
         verifyCounterName(Counter.PASSED_IPV6_UNICAST_NON_ICMP,
                 CounterName.CN_PASSED_IPV6_UNICAST_NON_ICMP);
-        verifyCounterName(Counter.PASSED_ARP_NON_IPV4, CounterName.CN_UNKNOWN);
-        verifyCounterName(Counter.PASSED_ARP_UNKNOWN, CounterName.CN_UNKNOWN);
         verifyCounterName(Counter.PASSED_ARP_UNICAST_REPLY,
                 CounterName.CN_PASSED_ARP_UNICAST_REPLY);
         verifyCounterName(Counter.PASSED_NON_IP_UNICAST, CounterName.CN_PASSED_NON_IP_UNICAST);
-        verifyCounterName(Counter.PASSED_MDNS, CounterName.CN_PASSED_MDNS);
         verifyCounterName(Counter.DROPPED_ETH_BROADCAST, CounterName.CN_DROPPED_ETH_BROADCAST);
+        verifyCounterName(Counter.DROPPED_ETHER_OUR_SRC_MAC,
+                CounterName.CN_DROPPED_ETHER_OUR_SRC_MAC);
         verifyCounterName(Counter.DROPPED_RA, CounterName.CN_DROPPED_RA);
         verifyCounterName(Counter.DROPPED_GARP_REPLY, CounterName.CN_DROPPED_GARP_REPLY);
         verifyCounterName(Counter.DROPPED_ARP_OTHER_HOST, CounterName.CN_DROPPED_ARP_OTHER_HOST);
@@ -118,14 +118,21 @@
                 CounterName.CN_DROPPED_IPV4_BROADCAST_ADDR);
         verifyCounterName(Counter.DROPPED_IPV4_BROADCAST_NET,
                 CounterName.CN_DROPPED_IPV4_BROADCAST_NET);
+        verifyCounterName(Counter.DROPPED_IPV4_ICMP_INVALID,
+                CounterName.CN_DROPPED_IPV4_ICMP_INVALID);
         verifyCounterName(Counter.DROPPED_IPV4_MULTICAST, CounterName.CN_DROPPED_IPV4_MULTICAST);
         verifyCounterName(Counter.DROPPED_IPV6_ROUTER_SOLICITATION,
                 CounterName.CN_DROPPED_IPV6_ROUTER_SOLICITATION);
+        verifyCounterName(Counter.DROPPED_IPV6_MLD_INVALID,
+                CounterName.CN_DROPPED_IPV6_MLD_INVALID);
+        verifyCounterName(Counter.DROPPED_IPV6_MLD_REPORT,
+                CounterName.CN_DROPPED_IPV6_MLD_REPORT);
+        verifyCounterName(Counter.DROPPED_IPV6_MLD_V1_GENERAL_QUERY_REPLIED,
+                CounterName.CN_DROPPED_IPV6_MLD_V1_GENERAL_QUERY_REPLIED);
+        verifyCounterName(Counter.DROPPED_IPV6_MLD_V2_GENERAL_QUERY_REPLIED,
+                CounterName.CN_DROPPED_IPV6_MLD_V2_GENERAL_QUERY_REPLIED);
         verifyCounterName(Counter.DROPPED_IPV6_MULTICAST_NA,
                 CounterName.CN_DROPPED_IPV6_MULTICAST_NA);
-        verifyCounterName(Counter.DROPPED_IPV6_MULTICAST, CounterName.CN_DROPPED_IPV6_MULTICAST);
-        verifyCounterName(Counter.DROPPED_IPV6_MULTICAST_PING,
-                CounterName.CN_DROPPED_IPV6_MULTICAST_PING);
         verifyCounterName(Counter.DROPPED_IPV6_NON_ICMP_MULTICAST,
                 CounterName.CN_DROPPED_IPV6_NON_ICMP_MULTICAST);
         verifyCounterName(Counter.DROPPED_802_3_FRAME, CounterName.CN_DROPPED_802_3_FRAME);
@@ -135,12 +142,12 @@
                 CounterName.CN_DROPPED_ARP_REPLY_SPA_NO_HOST);
         verifyCounterName(Counter.DROPPED_IPV4_KEEPALIVE_ACK,
                 CounterName.CN_DROPPED_IPV4_KEEPALIVE_ACK);
-        verifyCounterName(Counter.DROPPED_IPV6_KEEPALIVE_ACK,
-                CounterName.CN_DROPPED_IPV6_KEEPALIVE_ACK);
         verifyCounterName(Counter.DROPPED_IPV4_NATT_KEEPALIVE,
                 CounterName.CN_DROPPED_IPV4_NATT_KEEPALIVE);
         verifyCounterName(Counter.DROPPED_MDNS, CounterName.CN_DROPPED_MDNS);
-        verifyCounterName(Counter.DROPPED_IPV4_TCP_PORT7_UNICAST, CounterName.CN_UNKNOWN);
+        verifyCounterName(Counter.DROPPED_MDNS_REPLIED, CounterName.CN_DROPPED_MDNS_REPLIED);
+        verifyCounterName(Counter.DROPPED_IPV4_TCP_PORT7_UNICAST,
+                CounterName.CN_DROPPED_IPV4_TCP_PORT7_UNICAST);
         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,
@@ -148,24 +155,26 @@
         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_IPV4_PING_REQUEST_REPLIED,
+                CounterName.CN_DROPPED_IPV4_PING_REQUEST_REPLIED);
+        verifyCounterName(Counter.DROPPED_IPV6_ICMP6_ECHO_REQUEST_INVALID,
+                CounterName.CN_DROPPED_IPV6_ICMP6_ECHO_REQUEST_INVALID);
+        verifyCounterName(Counter.DROPPED_IPV6_ICMP6_ECHO_REQUEST_REPLIED,
+                CounterName.CN_DROPPED_IPV6_ICMP6_ECHO_REQUEST_REPLIED);
         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);
+        verifyCounterName(Counter.DROPPED_IGMP_INVALID, CounterName.CN_DROPPED_IGMP_INVALID);
+        verifyCounterName(Counter.DROPPED_IGMP_V3_GENERAL_QUERY_REPLIED,
+                CounterName.CN_DROPPED_IGMP_V3_GENERAL_QUERY_REPLIED);
+        verifyCounterName(Counter.DROPPED_IGMP_V2_GENERAL_QUERY_REPLIED,
+                CounterName.CN_DROPPED_IGMP_V2_GENERAL_QUERY_REPLIED);
+        verifyCounterName(Counter.DROPPED_IGMP_REPORT, CounterName.CN_DROPPED_IGMP_REPORT);
     }
 }
diff --git a/tests/unit/src/com/android/networkstack/util/ProcfsParsingUtilsTest.kt b/tests/unit/src/com/android/networkstack/util/ProcfsParsingUtilsTest.kt
index 7f8cacb..7f99aca 100644
--- a/tests/unit/src/com/android/networkstack/util/ProcfsParsingUtilsTest.kt
+++ b/tests/unit/src/com/android/networkstack/util/ProcfsParsingUtilsTest.kt
@@ -18,9 +18,11 @@
 import android.net.MacAddress
 import android.net.apf.ProcfsParsingUtils
 import androidx.test.filters.SmallTest
-import com.android.internal.util.HexDump
+import com.android.net.module.util.HexDump
+import java.net.Inet4Address
 import java.net.Inet6Address
 import java.net.InetAddress
+import java.nio.ByteOrder
 import kotlin.test.assertEquals
 import org.junit.Test
 
@@ -38,6 +40,34 @@
     }
 
     @Test
+    fun testParseDefaultTtl() {
+        assertEquals(
+            128,
+            ProcfsParsingUtils.parseDefaultTtl(listOf("128"))
+        )
+
+        assertEquals(
+            64,
+            ProcfsParsingUtils.parseDefaultTtl(listOf())
+        )
+
+        assertEquals(
+            1,
+            ProcfsParsingUtils.parseDefaultTtl(listOf("0"))
+        )
+
+        assertEquals(
+            255,
+            ProcfsParsingUtils.parseDefaultTtl(listOf("256"))
+        )
+
+        assertEquals(
+            64,
+            ProcfsParsingUtils.parseDefaultTtl(listOf("ABC"))
+        )
+    }
+
+    @Test
     fun testParseAnycast6Address() {
         val inputString = listOf(
             "41 eth0  2a0034e2abc1334591a733387s2e322e 2",
@@ -135,4 +165,149 @@
             ProcfsParsingUtils.parseIPv6MulticastAddresses(inputString, "wlan0")
         )
     }
+
+    @Test
+    fun testParseIpv4MulticastAddressLittleEndian() {
+        val order = ByteOrder.LITTLE_ENDIAN
+
+        // the format refer to net/ipv4/igmp.c#igmp_mc_seq_show
+        val inputString = listOf(
+            "Idx\tDevice    : Count Querier\tGroup    Users Timer\tReporter",
+            "1\tlo        :     1      V3",
+            "\t\t\t\t010000E0     1 0:00000000\t\t0",
+            "2\tdummy0    :     1      V3",
+            "\t\t\t\t010000E0     1 0:00000000\t\t0",
+            "47\twlan0     :     1      V3",
+            "\t\t\t\t020000EF     1 0:00000000\t\t0",
+            "\t\t\t\t010000EF     1 0:00000000\t\t0",
+            "\t\t\t\t010000E0     1 0:00000000\t\t0",
+            "51\tv4-wlan0  :     1      V3",
+            "\t\t\t\t010000E0     1 0:00000000\t\t0"
+        )
+
+        val expectedResult = listOf(
+            InetAddress.getByAddress(
+                HexDump.hexStringToByteArray("EF000002")
+            ) as Inet4Address,
+            InetAddress.getByAddress(
+                HexDump.hexStringToByteArray("EF000001")
+            ) as Inet4Address,
+            InetAddress.getByAddress(
+                HexDump.hexStringToByteArray("E0000001")
+            ) as Inet4Address,
+        )
+
+        assertEquals(
+            expectedResult,
+            ProcfsParsingUtils.parseIPv4MulticastAddresses(
+                inputString, "wlan0", order)
+        )
+
+        assertEquals(
+            emptyList<Inet4Address>(),
+            ProcfsParsingUtils.parseIPv4MulticastAddresses(
+                inputString, "eth0", order)
+        )
+
+        assertEquals(
+            emptyList<Inet4Address>(),
+            ProcfsParsingUtils.parseIPv4MulticastAddresses(
+                emptyList<String>(), "eth0", order)
+        )
+    }
+
+    @Test
+    fun testParseIpv4MulticastAddressBigEndian() {
+        val order = ByteOrder.BIG_ENDIAN
+
+        // the format refer to net/ipv4/igmp.c#igmp_mc_seq_show
+        val inputString = listOf(
+            "Idx\tDevice    : Count Querier\tGroup    Users Timer\tReporter",
+            "1\tlo        :     1      V3",
+            "\t\t\t\tE0000001     1 0:00000000\t\t0",
+            "2\tdummy0    :     1      V3",
+            "\t\t\t\tE0000001     1 0:00000000\t\t0",
+            "47\twlan0     :     1      V3",
+            "\t\t\t\tEF000002     1 0:00000000\t\t0",
+            "\t\t\t\tEF000001     1 0:00000000\t\t0",
+            "\t\t\t\tE0000001     1 0:00000000\t\t0",
+            "51\tv4-wlan0  :     1      V3",
+            "\t\t\t\tE0000001     1 0:00000000\t\t0"
+        )
+
+        val expectedResult = listOf(
+            InetAddress.getByAddress(
+                HexDump.hexStringToByteArray("EF000002")
+            ) as Inet4Address,
+            InetAddress.getByAddress(
+                HexDump.hexStringToByteArray("EF000001")
+            ) as Inet4Address,
+            InetAddress.getByAddress(
+                HexDump.hexStringToByteArray("E0000001")
+            ) as Inet4Address,
+        )
+
+        assertEquals(
+            expectedResult,
+            ProcfsParsingUtils.parseIPv4MulticastAddresses(
+                inputString, "wlan0", order)
+        )
+
+        assertEquals(
+            emptyList<Inet4Address>(),
+            ProcfsParsingUtils.parseIPv4MulticastAddresses(
+                inputString, "eth0", order)
+        )
+
+        assertEquals(
+            emptyList<Inet4Address>(),
+            ProcfsParsingUtils.parseIPv4MulticastAddresses(
+                emptyList<String>(), "eth0", order)
+        )
+    }
+
+    @Test
+    fun testParseIpv4MulticastAddressError() {
+        val order = ByteOrder.LITTLE_ENDIAN
+
+        // the format refer to net/ipv4/igmp.c#igmp_mc_seq_show
+        // wlan0 addresses contain invalid char 'X'
+        val inputString = listOf(
+            "Idx\tDevice    : Count Querier\tGroup    Users Timer\tReporter",
+            "1\tlo        :     1      V3",
+            "\t\t\t\t010000E0     1 0:00000000\t\t0",
+            "2\tdummy0    :     1      V3",
+            "\t\t\t\t010000E0     1 0:00000000\t\t0",
+            "47\twlan0     :     1      V3",
+            "\t\t\t\t02XXXXEF     1 0:00000000\t\t0",
+            "\t\t\t\t01XXXXEF     1 0:00000000\t\t0",
+            "\t\t\t\t01XXXXE0     1 0:00000000\t\t0",
+            "51\tv4-wlan0  :     1      V3",
+            "\t\t\t\t010000E0     1 0:00000000\t\t0"
+        )
+
+        val expectedResult = listOf(
+            InetAddress.getByAddress(
+                HexDump.hexStringToByteArray("E0000001")
+            ) as Inet4Address
+        )
+
+        assertEquals(
+            expectedResult,
+            ProcfsParsingUtils.parseIPv4MulticastAddresses(
+                inputString, "wlan0", order)
+        )
+
+        assertEquals(
+            emptyList<Inet4Address>(),
+            ProcfsParsingUtils.parseIPv4MulticastAddresses(
+                inputString, "eth0", order)
+        )
+
+        assertEquals(
+            emptyList<Inet4Address>(),
+            ProcfsParsingUtils.parseIPv4MulticastAddresses(
+                emptyList<String>(), "eth0", order)
+        )
+    }
 }
diff --git a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
index 92120cf..e9bd616 100644
--- a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
+++ b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
@@ -208,7 +208,6 @@
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.lang.reflect.Constructor;
 import java.net.HttpURLConnection;
 import java.net.Inet6Address;
 import java.net.InetAddress;
@@ -2523,9 +2522,9 @@
         verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1))
                 .notifyPrivateDnsConfigResolved(any());
 
-        // Change the mode to opportunistic mode. Verify the callback.
+        // Change the mode to opportunistic mode. Verify the callback is fired a second time.
         wnm.notifyPrivateDnsSettingsChanged(new PrivateDnsConfig(true));
-        verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(1)).notifyPrivateDnsConfigResolved(
+        verify(mCallbacks, timeout(HANDLER_TIMEOUT_MS).times(2)).notifyPrivateDnsConfigResolved(
                 matchPrivateDnsConfigParcelWithDohOnly("some.doh.name" /* dohName */,
                         new String[0] /* dohIps */, "/dns-query{?dns}" /* dohPath */,
                         443 /* dohPort */));