Snap for 12032681 from 355c72b7022d4416fc9d227055ea40ffac4d1b17 to mainline-media-release

Change-Id: Ie35b13bf6982337ba801d88a1319151dc6ca81ad
diff --git a/Android.bp b/Android.bp
index 2dcaa6b..3712ba6 100644
--- a/Android.bp
+++ b/Android.bp
@@ -78,7 +78,7 @@
 java_defaults {
     name: "NetworkStackReleaseTargetSdk",
     min_sdk_version: "30",
-    target_sdk_version: "34",
+    target_sdk_version: "35",
 }
 
 java_defaults {
diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml
index b595a9e..451d8c3 100644
--- a/res/values-fa/strings.xml
+++ b/res/values-fa/strings.xml
@@ -21,6 +21,6 @@
     <string name="notification_channel_name_network_venue_info" msgid="6526543187249265733">"اطلاعات محل شبکه"</string>
     <string name="notification_channel_description_network_venue_info" msgid="5131499595382733605">"اعلان‌هایی که نمایش داده می‌شوند تا نشان دهند شبکه صفحه اطلاعات محل دارد"</string>
     <string name="connected" msgid="4563643884927480998">"متصل"</string>
-    <string name="tap_for_info" msgid="6849746325626883711">"متصل شد / برای مشاهده وب‌سایت ضربه بزنید"</string>
+    <string name="tap_for_info" msgid="6849746325626883711">"متصل شد / برای مشاهده وب‌سایت تک‌ضرب بزنید"</string>
     <string name="application_label" msgid="1322847171305285454">"مدیر شبکه"</string>
 </resources>
diff --git a/src/android/net/apf/ApfConstants.java b/src/android/net/apf/ApfConstants.java
index fe2cfd8..09be67b 100644
--- a/src/android/net/apf/ApfConstants.java
+++ b/src/android/net/apf/ApfConstants.java
@@ -65,7 +65,40 @@
     public static final int ICMP6_CHECKSUM_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN + 2;
     public static final int ICMP6_NS_TARGET_IP_OFFSET = ICMP6_TYPE_OFFSET + 8;
     public static final int ICMP6_NS_OPTION_TYPE_OFFSET = ICMP6_NS_TARGET_IP_OFFSET + 16;
+    // From RFC4861:
+    public static final int ICMP6_RA_HEADER_LEN = 16;
+    public static final int ICMP6_RA_CHECKSUM_OFFSET =
+            ETH_HEADER_LEN + IPV6_HEADER_LEN + 2;
+    public static final int ICMP6_RA_CHECKSUM_LEN = 2;
+    public static final int ICMP6_RA_OPTION_OFFSET =
+            ETH_HEADER_LEN + IPV6_HEADER_LEN + ICMP6_RA_HEADER_LEN;
+    public static final int ICMP6_RA_ROUTER_LIFETIME_OFFSET =
+            ETH_HEADER_LEN + IPV6_HEADER_LEN + 6;
+    public static final int ICMP6_RA_ROUTER_LIFETIME_LEN = 2;
+    // Prefix information option.
+    public static final int ICMP6_PREFIX_OPTION_TYPE = 3;
+    public static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET = 4;
+    public static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN = 4;
+    public static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN = 4;
 
+    // From RFC4861: source link-layer address
+    public static final int ICMP6_SOURCE_LL_ADDRESS_OPTION_TYPE = 1;
+    // From RFC4861: mtu size option
+    public static final int ICMP6_MTU_OPTION_TYPE = 5;
+    // From RFC6106: Recursive DNS Server option
+    public static final int ICMP6_RDNSS_OPTION_TYPE = 25;
+    // From RFC6106: DNS Search List option
+    public static final int ICMP6_DNSSL_OPTION_TYPE = 31;
+    // From RFC8910: Captive-Portal option
+    public static final int ICMP6_CAPTIVE_PORTAL_OPTION_TYPE = 37;
+    // From RFC8781: PREF64 option
+    public static final int ICMP6_PREF64_OPTION_TYPE = 38;
+
+    // From RFC4191: Route Information option
+    public static final int ICMP6_ROUTE_INFO_OPTION_TYPE = 24;
+    // Above three options all have the same format:
+    public static final int ICMP6_4_BYTE_LIFETIME_OFFSET = 4;
+    public static final int ICMP6_4_BYTE_LIFETIME_LEN = 4;
     public static final int IPPROTO_HOPOPTS = 0;
 
     // NOTE: this must be added to the IPv4 header length in MemorySlot.IPV4_HEADER_SIZE
diff --git a/src/android/net/apf/ApfCounterTracker.java b/src/android/net/apf/ApfCounterTracker.java
index e86aab1..f5416c5 100644
--- a/src/android/net/apf/ApfCounterTracker.java
+++ b/src/android/net/apf/ApfCounterTracker.java
@@ -63,8 +63,10 @@
         PASSED_IPV4_UNICAST,
         PASSED_IPV6_ICMP,
         PASSED_IPV6_NON_ICMP,
-        PASSED_IPV6_NS_MULTIPLE_OPTIONS,
+        PASSED_IPV6_NS_DAD,
         PASSED_IPV6_NS_NO_ADDRESS,
+        PASSED_IPV6_NS_NO_SLLA_OPTION,
+        PASSED_IPV6_NS_TENTATIVE,
         PASSED_IPV6_UNICAST_NON_ICMP,
         PASSED_NON_IP_UNICAST,
         PASSED_MDNS,
@@ -83,6 +85,7 @@
         DROPPED_IPV6_NON_ICMP_MULTICAST,
         DROPPED_IPV6_NS_INVALID,
         DROPPED_IPV6_NS_OTHER_HOST,
+        DROPPED_IPV6_NS_REPLIED_NON_DAD,
         DROPPED_802_3_FRAME,
         DROPPED_ETHERTYPE_NOT_ALLOWED,
         DROPPED_IPV4_KEEPALIVE_ACK,
diff --git a/src/android/net/apf/ApfFilter.java b/src/android/net/apf/ApfFilter.java
index 07d4a7b..92c25e7 100644
--- a/src/android/net/apf/ApfFilter.java
+++ b/src/android/net/apf/ApfFilter.java
@@ -36,10 +36,28 @@
 import static android.net.apf.ApfConstants.ETH_TYPE_MAX;
 import static android.net.apf.ApfConstants.ETH_TYPE_MIN;
 import static android.net.apf.ApfConstants.FIXED_ARP_REPLY_HEADER;
+import static android.net.apf.ApfConstants.ICMP6_4_BYTE_LIFETIME_LEN;
+import static android.net.apf.ApfConstants.ICMP6_4_BYTE_LIFETIME_OFFSET;
+import static android.net.apf.ApfConstants.ICMP6_CAPTIVE_PORTAL_OPTION_TYPE;
 import static android.net.apf.ApfConstants.ICMP6_CHECKSUM_OFFSET;
 import static android.net.apf.ApfConstants.ICMP6_CODE_OFFSET;
+import static android.net.apf.ApfConstants.ICMP6_DNSSL_OPTION_TYPE;
+import static android.net.apf.ApfConstants.ICMP6_MTU_OPTION_TYPE;
 import static android.net.apf.ApfConstants.ICMP6_NS_OPTION_TYPE_OFFSET;
 import static android.net.apf.ApfConstants.ICMP6_NS_TARGET_IP_OFFSET;
+import static android.net.apf.ApfConstants.ICMP6_PREF64_OPTION_TYPE;
+import static android.net.apf.ApfConstants.ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN;
+import static android.net.apf.ApfConstants.ICMP6_PREFIX_OPTION_TYPE;
+import static android.net.apf.ApfConstants.ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN;
+import static android.net.apf.ApfConstants.ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET;
+import static android.net.apf.ApfConstants.ICMP6_RA_CHECKSUM_LEN;
+import static android.net.apf.ApfConstants.ICMP6_RA_CHECKSUM_OFFSET;
+import static android.net.apf.ApfConstants.ICMP6_RA_OPTION_OFFSET;
+import static android.net.apf.ApfConstants.ICMP6_RA_ROUTER_LIFETIME_LEN;
+import static android.net.apf.ApfConstants.ICMP6_RA_ROUTER_LIFETIME_OFFSET;
+import static android.net.apf.ApfConstants.ICMP6_RDNSS_OPTION_TYPE;
+import static android.net.apf.ApfConstants.ICMP6_ROUTE_INFO_OPTION_TYPE;
+import static android.net.apf.ApfConstants.ICMP6_SOURCE_LL_ADDRESS_OPTION_TYPE;
 import static android.net.apf.ApfConstants.ICMP6_TYPE_OFFSET;
 import static android.net.apf.ApfConstants.IPPROTO_HOPOPTS;
 import static android.net.apf.ApfConstants.IPV4_ANY_HOST_ADDRESS;
@@ -60,6 +78,7 @@
 import static android.net.apf.ApfConstants.IPV6_PAYLOAD_LEN_OFFSET;
 import static android.net.apf.ApfConstants.IPV6_SOLICITED_NODES_PREFIX;
 import static android.net.apf.ApfConstants.IPV6_SRC_ADDR_OFFSET;
+import static android.net.apf.ApfConstants.IPV6_UNSPECIFIED_ADDRESS;
 import static android.net.apf.ApfConstants.MDNS_PORT;
 import static android.net.apf.ApfConstants.TCP_HEADER_SIZE_OFFSET;
 import static android.net.apf.ApfConstants.TCP_UDP_DESTINATION_PORT_OFFSET;
@@ -68,7 +87,9 @@
 import static android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_OTHER_HOST;
 import static android.net.apf.ApfCounterTracker.Counter.FILTER_AGE_16384THS;
 import static android.net.apf.ApfCounterTracker.Counter.FILTER_AGE_SECONDS;
-import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_MULTIPLE_OPTIONS;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_DAD;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_NO_SLLA_OPTION;
+import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_TENTATIVE;
 import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_NO_ADDRESS;
 import static android.net.apf.BaseApfGenerator.MemorySlot;
 import static android.net.apf.BaseApfGenerator.Register.R0;
@@ -93,6 +114,7 @@
 import static com.android.net.module.util.NetworkStackConstants.ETHER_SRC_ADDR_OFFSET;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NA_HEADER_LEN;
+import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_TLLA;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_TLLA_LEN;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_NEIGHBOR_ADVERTISEMENT;
@@ -189,10 +211,10 @@
         public int[] ethTypeBlackList;
         public int minRdnssLifetimeSec;
         public int acceptRaMinLft;
-        public boolean shouldHandleLightDoze;
         public long minMetricsSessionDurationMs;
         public boolean hasClatInterface;
         public boolean shouldHandleArpOffload;
+        public boolean shouldHandleNdOffload;
     }
 
     /** A wrapper class of {@link SystemClock} to be mocked in unit tests. */
@@ -291,8 +313,8 @@
     // Tracks the value of /proc/sys/ipv6/conf/$iface/accept_ra_min_lft which affects router, RIO,
     // and PIO valid lifetimes.
     private final int mAcceptRaMinLft;
-    private final boolean mShouldHandleLightDoze;
     private final boolean mShouldHandleArpOffload;
+    private final boolean mShouldHandleNdOffload;
 
     private final NetworkQuirkMetrics mNetworkQuirkMetrics;
     private final IpClientRaInfoMetrics mIpClientRaInfoMetrics;
@@ -309,9 +331,6 @@
         if (!SdkLevel.isAtLeastT()) {
             return false;
         }
-        if (!mShouldHandleLightDoze) {
-            return false;
-        }
         return ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED.equals(intent.getAction());
     }
 
@@ -322,9 +341,6 @@
         if (!SdkLevel.isAtLeastT()) {
             return false;
         }
-        if (!mShouldHandleLightDoze) {
-            return false;
-        }
 
         return powerManager.isDeviceLightIdleMode();
     }
@@ -406,8 +422,8 @@
         mDrop802_3Frames = config.ieee802_3Filter;
         mMinRdnssLifetimeSec = config.minRdnssLifetimeSec;
         mAcceptRaMinLft = config.acceptRaMinLft;
-        mShouldHandleLightDoze = config.shouldHandleLightDoze;
         mShouldHandleArpOffload = config.shouldHandleArpOffload;
+        mShouldHandleNdOffload = config.shouldHandleNdOffload;
         mDependencies = dependencies;
         mNetworkQuirkMetrics = networkQuirkMetrics;
         mIpClientRaInfoMetrics = dependencies.getIpClientRaInfoMetrics();
@@ -434,7 +450,7 @@
         startFilter();
 
         // Listen for doze-mode transition changes to enable/disable the IPv6 multicast filter.
-        mDependencies.addDeviceIdleReceiver(mDeviceIdleReceiver, mShouldHandleLightDoze);
+        mDependencies.addDeviceIdleReceiver(mDeviceIdleReceiver);
 
         mDependencies.onApfFilterCreated(this);
         // mReceiveThread is created in startFilter() and halted in shutdown().
@@ -452,10 +468,9 @@
         }
 
         /** Add receiver for detecting doze mode change */
-        public void addDeviceIdleReceiver(@NonNull final BroadcastReceiver receiver,
-                boolean shouldHandleLightDoze) {
+        public void addDeviceIdleReceiver(@NonNull final BroadcastReceiver receiver) {
             final IntentFilter intentFilter = new IntentFilter(ACTION_DEVICE_IDLE_MODE_CHANGED);
-            if (SdkLevel.isAtLeastT() && shouldHandleLightDoze) {
+            if (SdkLevel.isAtLeastT()) {
                 intentFilter.addAction(ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED);
             }
             mContext.registerReceiver(receiver, intentFilter);
@@ -684,41 +699,6 @@
     // A class to hold information about an RA.
     @VisibleForTesting
     public class Ra {
-        // From RFC4861:
-        private static final int ICMP6_RA_HEADER_LEN = 16;
-        private static final int ICMP6_RA_CHECKSUM_OFFSET =
-                ETH_HEADER_LEN + IPV6_HEADER_LEN + 2;
-        private static final int ICMP6_RA_CHECKSUM_LEN = 2;
-        private static final int ICMP6_RA_OPTION_OFFSET =
-                ETH_HEADER_LEN + IPV6_HEADER_LEN + ICMP6_RA_HEADER_LEN;
-        private static final int ICMP6_RA_ROUTER_LIFETIME_OFFSET =
-                ETH_HEADER_LEN + IPV6_HEADER_LEN + 6;
-        private static final int ICMP6_RA_ROUTER_LIFETIME_LEN = 2;
-        // Prefix information option.
-        private static final int ICMP6_PREFIX_OPTION_TYPE = 3;
-        private static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET = 4;
-        private static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN = 4;
-        private static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN = 4;
-
-        // From RFC4861: source link-layer address
-        private static final int ICMP6_SOURCE_LL_ADDRESS_OPTION_TYPE = 1;
-        // From RFC4861: mtu size option
-        private static final int ICMP6_MTU_OPTION_TYPE = 5;
-        // From RFC6106: Recursive DNS Server option
-        private static final int ICMP6_RDNSS_OPTION_TYPE = 25;
-        // From RFC6106: DNS Search List option
-        private static final int ICMP6_DNSSL_OPTION_TYPE = 31;
-        // From RFC8910: Captive-Portal option
-        private static final int ICMP6_CAPTIVE_PORTAL_OPTION_TYPE = 37;
-        // From RFC8781: PREF64 option
-        private static final int ICMP6_PREF64_OPTION_TYPE = 38;
-
-        // From RFC4191: Route Information option
-        private static final int ICMP6_ROUTE_INFO_OPTION_TYPE = 24;
-        // Above three options all have the same format:
-        private static final int ICMP6_4_BYTE_LIFETIME_OFFSET = 4;
-        private static final int ICMP6_4_BYTE_LIFETIME_LEN = 4;
-
         // Note: mPacket's position() cannot be assumed to be reset.
         private final ByteBuffer mPacket;
 
@@ -1949,22 +1929,63 @@
         v6Gen.addLoad8(R0, ICMP6_CODE_OFFSET)
                 .addCountAndDropIfR0NotEquals(0, DROPPED_IPV6_NS_INVALID);
 
-        // target address (ICMPv6 NS/NA payload) is not interface addresses -> drop
-        v6Gen.addLoadImmediate(R0, ICMP6_NS_TARGET_IP_OFFSET)
-                .addCountAndDropIfBytesAtR0EqualsNoneOf(allIPv6Addrs, DROPPED_IPV6_NS_OTHER_HOST);
+        // target address (ICMPv6 NS payload)
+        //   1) is one of tentative addresses -> pass
+        //   2) is none of {non-tentative, anycast} addresses -> drop
+        final List<byte[]> tentativeIPv6Addrs = getIpv6Addresses(
+                false, /* includeNonTentative */
+                true, /* includeTentative */
+                false /* includeAnycast */
+        );
+        v6Gen.addLoadImmediate(R0, ICMP6_NS_TARGET_IP_OFFSET);
+        if (!tentativeIPv6Addrs.isEmpty()) {
+            v6Gen.addCountAndPassIfBytesAtR0EqualsAnyOf(
+                    tentativeIPv6Addrs, PASSED_IPV6_NS_TENTATIVE);
+        }
 
-        // Only offload the following cases:
-        //   1) NS packet with no options.
-        //   2) NS packet with only one option: nonce.
-        //   3) NS packet with only one option: SLLA.
-        // For packets containing more than one option,
-        // pass the packet to the CPU for processing.
-        // payload length > 32
-        //   (8 bytes ICMP6 header + 16 bytes target address + 8 bytes option) -> pass
+        final List<byte[]> nonTentativeIpv6Addrs = getIpv6Addresses(
+                true, /* includeNonTentative */
+                false, /* includeTentative */
+                true /* includeAnycast */
+        );
+        if (nonTentativeIpv6Addrs.isEmpty()) {
+            v6Gen.addCountAndDrop(DROPPED_IPV6_NS_OTHER_HOST);
+            return;
+        }
+        v6Gen.addCountAndDropIfBytesAtR0EqualsNoneOf(
+                nonTentativeIpv6Addrs, DROPPED_IPV6_NS_OTHER_HOST);
+
+        // if source ip is unspecified (::), it's DAD request -> pass
+        v6Gen.addLoadImmediate(R0, IPV6_SRC_ADDR_OFFSET)
+                .addCountAndPassIfBytesAtR0Equal(IPV6_UNSPECIFIED_ADDRESS, PASSED_IPV6_NS_DAD);
+
+        // Only offload NUD/Address resolution packets that have SLLA as the their first option.
+        // For option-less NUD packets or NUD/Address resolution packets where
+        // the first option is not SLLA, pass them to the kernel for handling.
+        // if payload len < 32 -> pass
         v6Gen.addLoad16(R0, IPV6_PAYLOAD_LEN_OFFSET)
-                .addCountAndPassIfR0GreaterThan(32, PASSED_IPV6_NS_MULTIPLE_OPTIONS);
+                .addCountAndPassIfR0LessThan(32, PASSED_IPV6_NS_NO_SLLA_OPTION);
 
-        v6Gen.addCountAndPass(Counter.PASSED_IPV6_ICMP);
+        // if the first option is not SLLA -> pass
+        // 0                   1                   2                   3
+        // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+        // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+        // |     Type      |    Length     |Link-Layer Addr  |
+        // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+        v6Gen.addLoad8(R0, ICMP6_NS_OPTION_TYPE_OFFSET)
+                .addCountAndPassIfR0NotEquals(ICMPV6_ND_OPTION_SLLA,
+                        PASSED_IPV6_NS_NO_SLLA_OPTION);
+
+        // Src IPv6 address check:
+        // if multicast address (FF::/8) or loopback address (00::/8) -> drop
+        v6Gen.addLoad8(R0, IPV6_SRC_ADDR_OFFSET)
+                .addCountAndDropIfR0IsOneOf(Set.of(0L, 0xffL), DROPPED_IPV6_NS_INVALID);
+
+        // if multicast MAC in SLLA option -> drop
+        v6Gen.addLoad8(R0, ICMP6_NS_OPTION_TYPE_OFFSET + 2)
+                .addCountAndDropIfR0AnyBitsSet(1, DROPPED_IPV6_NS_INVALID);
+        generateNonDadNaTransmitLocked(v6Gen);
+        v6Gen.addCountAndDrop(Counter.DROPPED_IPV6_NS_REPLIED_NON_DAD);
     }
 
     /**
@@ -2001,10 +2022,19 @@
         //     drop
         //   if ICMPv6 code is not 0:
         //     drop
-        //   if target IP is none of interface unicast IPv6 addresses (incl. anycast):
-        //     drop
-        //   if payload len > 32 (8 bytes ICMP6 header + 16 bytes target address + 8 bytes option):
+        //   if target IP is one of tentative IPv6 addresses:
         //     pass
+        //   if target IP is none of non-tentative IPv6 addresses (incl. anycast):
+        //     drop
+        //   if IPv6 src is unspecified (::):
+        //     pass
+        //   if payload len < 32 (8 bytes ICMP6 header + 16 bytes target address + 8 bytes option):
+        //     pass
+        //   if IPv6 src is multicast address (FF::/8) or loopback address (00::/8):
+        //     drop
+        //   if multicast MAC in SLLA option:
+        //     drop
+        //   transmit NA and drop
         // if it's ICMPv6 RS to any:
         //   drop
         // if it's ICMPv6 NA to anything in ff02::/120
@@ -2056,7 +2086,7 @@
         // Not ICMPv6 NS -> skip.
         gen.addLoad8(R0, ICMP6_TYPE_OFFSET); // warning: also used further below.
         final ApfV6Generator v6Gen = tryToConvertToApfV6Generator(gen);
-        if (v6Gen != null) {
+        if (v6Gen != null && mShouldHandleNdOffload) {
             final String skipNsPacketFilter = v6Gen.getUniqueLabel();
             v6Gen.addJumpIfR0NotEquals(ICMPV6_NEIGHBOR_SOLICITATION, skipNsPacketFilter);
             generateNsFilterLocked(v6Gen);
diff --git a/src/android/net/apf/BaseApfGenerator.java b/src/android/net/apf/BaseApfGenerator.java
index 4f4abb9..2eab5ab 100644
--- a/src/android/net/apf/BaseApfGenerator.java
+++ b/src/android/net/apf/BaseApfGenerator.java
@@ -22,6 +22,7 @@
 
 import android.annotation.NonNull;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.net.module.util.ByteUtils;
 import com.android.net.module.util.CollectionUtils;
 import com.android.net.module.util.HexDump;
@@ -41,10 +42,10 @@
 
     public BaseApfGenerator(int version, int ramSize, int clampSize,
             boolean disableCounterRangeCheck) {
-        this.mVersion = version;
-        this.mRamSize = ramSize;
-        this.mClampSize = clampSize;
-        this.mDisableCounterRangeCheck = disableCounterRangeCheck;
+        mVersion = version;
+        mRamSize = ramSize;
+        mClampSize = clampSize;
+        mDisableCounterRangeCheck = disableCounterRangeCheck;
     }
 
     /**
@@ -854,7 +855,8 @@
     /**
      * Return a unique label string.
      */
-    protected String getUniqueLabel() {
+    @VisibleForTesting
+    public String getUniqueLabel() {
         return "LABEL_" + mLabelCount++;
     }
 
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java
index 9dd9dc5..7b9cd73 100644
--- a/src/android/net/ip/IpClient.java
+++ b/src/android/net/ip/IpClient.java
@@ -48,8 +48,8 @@
 import static com.android.net.module.util.NetworkStackConstants.VENDOR_SPECIFIC_IE_ID;
 import static com.android.networkstack.apishim.ConstantsShim.IFA_F_MANAGETEMPADDR;
 import static com.android.networkstack.apishim.ConstantsShim.IFA_F_NOPREFIXROUTE;
-import static com.android.networkstack.util.NetworkStackUtils.APF_HANDLE_ARP_OFFLOAD_FORCE_DISABLE;
-import static com.android.networkstack.util.NetworkStackUtils.APF_HANDLE_LIGHT_DOZE_FORCE_DISABLE;
+import static com.android.networkstack.util.NetworkStackUtils.APF_HANDLE_ARP_OFFLOAD;
+import static com.android.networkstack.util.NetworkStackUtils.APF_HANDLE_ND_OFFLOAD;
 import static com.android.networkstack.util.NetworkStackUtils.APF_NEW_RA_FILTER_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.APF_POLLING_COUNTERS_VERSION;
 import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION;
@@ -733,10 +733,10 @@
     private final boolean mDhcp6PrefixDelegationEnabled;
     private final boolean mUseNewApfFilter;
     private final boolean mEnableIpClientIgnoreLowRaLifetime;
-    private final boolean mApfShouldHandleLightDoze;
     private final boolean mEnableApfPollingCounters;
     private final boolean mPopulateLinkAddressLifetime;
     private final boolean mApfShouldHandleArpOffload;
+    private final boolean mApfShouldHandleNdOffload;
 
     private InterfaceParams mInterfaceParams;
 
@@ -983,11 +983,10 @@
         mEnableIpClientIgnoreLowRaLifetime =
                 SdkLevel.isAtLeastV() || mDependencies.isFeatureEnabled(context,
                         IPCLIENT_IGNORE_LOW_RA_LIFETIME_VERSION);
-        // Light doze mode status checking API is only available at T or later releases.
-        mApfShouldHandleLightDoze = SdkLevel.isAtLeastT() && mDependencies.isFeatureNotChickenedOut(
-                mContext, APF_HANDLE_LIGHT_DOZE_FORCE_DISABLE);
         mApfShouldHandleArpOffload = mDependencies.isFeatureNotChickenedOut(
-                mContext, APF_HANDLE_ARP_OFFLOAD_FORCE_DISABLE);
+                mContext, APF_HANDLE_ARP_OFFLOAD);
+        mApfShouldHandleNdOffload = mDependencies.isFeatureNotChickenedOut(
+                mContext, APF_HANDLE_ND_OFFLOAD);
         mPopulateLinkAddressLifetime = mDependencies.isFeatureEnabled(context,
                 IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION);
 
@@ -2606,8 +2605,8 @@
         } else {
             apfConfig.acceptRaMinLft = 0;
         }
-        apfConfig.shouldHandleLightDoze = mApfShouldHandleLightDoze;
         apfConfig.shouldHandleArpOffload = mApfShouldHandleArpOffload;
+        apfConfig.shouldHandleNdOffload = mApfShouldHandleNdOffload;
         apfConfig.minMetricsSessionDurationMs = mApfCounterPollingIntervalMs;
         apfConfig.hasClatInterface = mHasSeenClatInterface;
         return mDependencies.maybeCreateApfFilter(mContext, apfConfig, mInterfaceParams,
diff --git a/src/com/android/networkstack/netlink/TcpSocketTracker.java b/src/com/android/networkstack/netlink/TcpSocketTracker.java
index 4140e64..0d77dca 100644
--- a/src/com/android/networkstack/netlink/TcpSocketTracker.java
+++ b/src/com/android/networkstack/netlink/TcpSocketTracker.java
@@ -50,7 +50,6 @@
 import android.net.NetworkCapabilities;
 import android.os.AsyncTask;
 import android.os.Build;
-import android.os.Handler;
 import android.os.IBinder;
 import android.os.PowerManager;
 import android.os.RemoteException;
@@ -67,6 +66,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.modules.utils.build.SdkLevel;
 import com.android.net.module.util.DeviceConfigUtils;
@@ -133,11 +133,10 @@
     private int mMinPacketsThreshold = DEFAULT_DATA_STALL_MIN_PACKETS_THRESHOLD;
     private int mTcpPacketsFailRateThreshold = DEFAULT_TCP_PACKETS_FAIL_PERCENTAGE;
 
-    // These variables are initialized when the NetworkMonitor enters DefaultState,
-    // and can only be accessed on the NetworkMonitor state machine thread after
-    // the NetworkMonitor state machine has been started.
     // TODO: Remove doze mode solution since uid networking blocked traffic is filtered out by
     //  the info provided by bpf maps.
+    private final Object mDozeModeLock = new Object();
+    @GuardedBy("mDozeModeLock")
     private boolean mInDozeMode = false;
 
     // These variables are initialized when the NetworkMonitor enters DefaultState,
@@ -227,21 +226,9 @@
                     family, InetDiagMessage.buildInetDiagReqForAliveTcpSockets(family));
         }
         mDependencies.addDeviceConfigChangedListener(mConfigListener);
-
-        mCm = mDependencies.getContext().getSystemService(ConnectivityManager.class);
-    }
-
-    /**
-     * Called from NetworkMonitor to notify NetworkMonitor is created.
-     * This is for initializing TcpSocketTracker from default state.
-     */
-    public void init(@NonNull final Handler handler, @NonNull LinkProperties lp,
-            @NonNull NetworkCapabilities nc) {
         mDependencies.addDeviceIdleReceiver(mDeviceIdleReceiver, mShouldDisableInDeepDoze,
-                mShouldDisableInLightDoze, handler);
-        setOpportunisticMode(false);
-        setLinkProperties(lp);
-        setNetworkCapabilities(nc);
+                mShouldDisableInLightDoze);
+        mCm = mDependencies.getContext().getSystemService(ConnectivityManager.class);
     }
 
     @Nullable
@@ -267,7 +254,9 @@
         // Traffic will be restricted in doze mode. TCP info may not reflect the correct network
         // behavior.
         // TODO: Traffic may be restricted by other reason. Get the restriction info from bpf in T+.
-        if (mInDozeMode) return false;
+        synchronized (mDozeModeLock) {
+            if (mInDozeMode) return false;
+        }
 
         FileDescriptor fd = null;
 
@@ -475,8 +464,10 @@
     public boolean isDataStallSuspected() {
         // Skip checking data stall since the traffic will be restricted and it will not be real
         // network stall.
-        if (mInDozeMode) return false;
-
+        // TODO: Traffic may be restricted by other reason. Get the restriction info from bpf in T+.
+        synchronized (mDozeModeLock) {
+            if (mInDozeMode) return false;
+        }
         final boolean ret = (getLatestPacketFailPercentage() >= getTcpPacketsFailRateThreshold());
         if (ret) {
             log("data stall suspected, uids: " + mLatestReportedUids.toString());
@@ -649,9 +640,11 @@
     }
 
     private void setDozeMode(boolean isEnabled) {
-        if (mInDozeMode == isEnabled) return;
-        mInDozeMode = isEnabled;
-        logd("Doze mode enabled=" + mInDozeMode);
+        synchronized (mDozeModeLock) {
+            if (mInDozeMode == isEnabled) return;
+            mInDozeMode = isEnabled;
+            logd("Doze mode enabled=" + mInDozeMode);
+        }
     }
 
     public void setOpportunisticMode(boolean isEnabled) {
@@ -755,8 +748,7 @@
         /** Add receiver for detecting doze mode change to control TCP detection. */
         @TargetApi(Build.VERSION_CODES.TIRAMISU)
         public void addDeviceIdleReceiver(@NonNull final BroadcastReceiver receiver,
-                boolean shouldDisableInDeepDoze, boolean shouldDisableInLightDoze,
-                @NonNull final Handler handler) {
+                boolean shouldDisableInDeepDoze, boolean shouldDisableInLightDoze) {
             // No need to register receiver if no related feature is enabled.
             if (!shouldDisableInDeepDoze && !shouldDisableInLightDoze) return;
 
@@ -767,8 +759,7 @@
             if (shouldDisableInLightDoze) {
                 intentFilter.addAction(ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED);
             }
-            mContext.registerReceiver(receiver, intentFilter, null /* broadcastPermission */,
-                    handler);
+            mContext.registerReceiver(receiver, intentFilter);
         }
 
         /** Remove broadcast receiver. */
diff --git a/src/com/android/networkstack/util/NetworkStackUtils.java b/src/com/android/networkstack/util/NetworkStackUtils.java
index ac2832b..a7563f4 100755
--- a/src/com/android/networkstack/util/NetworkStackUtils.java
+++ b/src/com/android/networkstack/util/NetworkStackUtils.java
@@ -277,12 +277,6 @@
     /**** BEGIN Feature Kill Switch Flags ****/
 
     /**
-     * Kill switch flag to disable the feature of handle light doze mode in Apf.
-     */
-    public static final String APF_HANDLE_LIGHT_DOZE_FORCE_DISABLE =
-            "apf_handle_light_doze_force_disable";
-
-    /**
      * Kill switch flag to disable the feature of skipping Tcp socket info polling when light
      * doze mode is enabled.
      */
@@ -302,9 +296,15 @@
 
     /**
      * Kill switch flag to disable the feature of handle arp offload in Apf.
+     * Warning: the following flag String is incorrect. The feature that is not chickened out is
+     * "ARP offload" not "ARP offload force disabled".
      */
-    public static final String APF_HANDLE_ARP_OFFLOAD_FORCE_DISABLE =
-            "apf_handle_arp_offload_force_disable";
+    public static final String APF_HANDLE_ARP_OFFLOAD = "apf_handle_arp_offload_force_disable";
+
+    /**
+     * Kill switch flag to disable the feature of handle nd offload in Apf.
+     */
+    public static final String APF_HANDLE_ND_OFFLOAD = "apf_handle_nd_offload";
 
     static {
         System.loadLibrary("networkstackutilsjni");
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java
index e564cd7..895fc54 100755
--- a/src/com/android/server/connectivity/NetworkMonitor.java
+++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -987,7 +987,9 @@
             final TcpSocketTracker tst = getTcpSocketTracker();
             if (tst != null) {
                 // Initialization.
-                tst.init(getHandler(), mLinkProperties, mNetworkCapabilities);
+                tst.setOpportunisticMode(false);
+                tst.setLinkProperties(mLinkProperties);
+                tst.setNetworkCapabilities(mNetworkCapabilities);
             }
             Log.d(TAG, "Starting on network " + mNetwork
                     + " with capport HTTPS URL " + Arrays.toString(mCaptivePortalHttpsUrls)
diff --git a/tests/unit/src/android/net/apf/ApfNewTest.kt b/tests/unit/src/android/net/apf/ApfNewTest.kt
index e4e9150..f016c75 100644
--- a/tests/unit/src/android/net/apf/ApfNewTest.kt
+++ b/tests/unit/src/android/net/apf/ApfNewTest.kt
@@ -29,14 +29,17 @@
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV4_NON_DHCP4
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_INVALID
 import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_OTHER_HOST
+import android.net.apf.ApfCounterTracker.Counter.DROPPED_IPV6_NS_REPLIED_NON_DAD
 import android.net.apf.ApfCounterTracker.Counter.PASSED_ALLOCATE_FAILURE
 import android.net.apf.ApfCounterTracker.Counter.PASSED_ARP
 import android.net.apf.ApfCounterTracker.Counter.PASSED_ARP_REQUEST
 import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV4
 import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV4_FROM_DHCPV4_SERVER
 import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_ICMP
-import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_MULTIPLE_OPTIONS
+import android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_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_TRANSMIT_FAILURE
 import android.net.apf.ApfCounterTracker.Counter.TOTAL_PACKETS
 import android.net.apf.ApfFilter.Dependencies
@@ -64,15 +67,23 @@
 import com.android.net.module.util.NetworkStackConstants.ARP_ETHER_IPV4_LEN
 import com.android.net.module.util.NetworkStackConstants.ARP_REPLY
 import com.android.net.module.util.NetworkStackConstants.ARP_REQUEST
+import com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.ICMPV6_NA_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.ICMPV6_NS_HEADER_LEN
+import com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN
 import com.android.net.module.util.Struct
 import com.android.net.module.util.arp.ArpPacket
 import com.android.net.module.util.structs.EthernetHeader
 import com.android.net.module.util.structs.Ipv4Header
 import com.android.net.module.util.structs.UdpHeader
 import com.android.networkstack.metrics.NetworkQuirkMetrics
+import com.android.networkstack.packets.NeighborAdvertisement
+import com.android.networkstack.packets.NeighborSolicitation
+import com.android.networkstack.util.NetworkStackUtils
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
 import com.android.testutils.DevSdkIgnoreRunner
+import java.net.Inet6Address
 import java.net.InetAddress
 import java.nio.ByteBuffer
 import kotlin.test.assertContentEquals
@@ -131,8 +142,12 @@
     private val senderIpv4Address = byteArrayOf(10, 0, 0, 2)
     private val arpBroadcastMacAddress = intArrayOf(0xff, 0xff, 0xff, 0xff, 0xff, 0xff)
             .map { it.toByte() }.toByteArray()
-    private val senderMacAddress = intArrayOf(0x01, 0x22, 0x33, 0x44, 0x55, 0x66)
-        .map { it.toByte() }.toByteArray()
+    private val senderMacAddress = intArrayOf(0x02, 0x22, 0x33, 0x44, 0x55, 0x66)
+            .map { it.toByte() }.toByteArray()
+    private val senderIpv6Address =
+        // 2001::200:1a:1122:3344
+        intArrayOf(0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x02, 0, 0, 0x1a, 0x11, 0x22, 0x33, 0x44)
+            .map{ it.toByte() }.toByteArray()
     private val hostIpv6Addresses = listOf(
         // 2001::200:1a:3344:1122
         intArrayOf(0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x02, 0, 0, 0x1a, 0x33, 0x44, 0x11, 0x22)
@@ -169,8 +184,12 @@
         MockitoAnnotations.initMocks(this)
         // mock anycast6 address from /proc/net/anycast6
         `when`(dependencies.getAnycast6Addresses(any())).thenReturn(hostAnycast6Addresses)
-        // mock host mac address and ethernet multicast addresses from /proc/net/dev_mcast
+
+        // mock ether multicast mac address from /proc/net/dev_mcast
         `when`(dependencies.getEtherMulticastAddresses(any())).thenReturn(hostMulticastMacAddresses)
+
+        // mock nd traffic class from /proc/sys/net/ipv6/conf/{ifname}/ndisc_tclass
+        `when`(dependencies.getNdTrafficClass(any())).thenReturn(0)
     }
 
     @After
@@ -616,7 +635,7 @@
                 program
         )
         assertContentEquals(
-                listOf("0: drop        counter=43"),
+                listOf("0: drop        counter=46"),
                 ApfJniUtils.disassembleApf(program).map { it.trim() }
         )
 
@@ -665,14 +684,14 @@
                 byteArrayOf(
                         encodeInstruction(opcode = 14, immLength = 2, register = 1), 1, 0
                 ) + largeByteArray + byteArrayOf(
-                        encodeInstruction(opcode = 21, immLength = 1, register = 0), 48, 6, 25
+                        encodeInstruction(opcode = 21, immLength = 1, register = 0), 48, 6, 13
                 ),
                 program
         )
         assertContentEquals(
                 listOf(
                         "0: data        256, " + "01".repeat(256),
-                        "259: debugbuf    size=1561"
+                        "259: debugbuf    size=1549"
                 ),
                 ApfJniUtils.disassembleApf(program).map { it.trim() }
         )
@@ -958,7 +977,7 @@
                 .generate()
         assertContentEquals(listOf(
                 "0: data        9, 112233445566778899",
-                "12: debugbuf    size=1788",
+                "12: debugbuf    size=1776",
                 "16: allocate    18",
                 "20: datacopy    src=3, len=6",
                 "23: datacopy    src=4, len=3",
@@ -2002,13 +2021,15 @@
         )
         verify(ipClientCallback, times(2)).installPacketFilter(any())
 
-        // validate Ethernet dst address check
-
         val lp = LinkProperties()
         for (addr in hostIpv6Addresses) {
             lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
         }
 
+        for (addr in hostIpv6TentativeAddresses) {
+            lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64, IFA_F_TENTATIVE, 0))
+        }
+
         apfFilter.setLinkProperties(lp)
         verify(ipClientCallback, times(3)).installPacketFilter(any())
         apfFilter.updateClatInterfaceState(true)
@@ -2016,14 +2037,16 @@
         verify(ipClientCallback, times(4)).installPacketFilter(programCaptor.capture())
         val program = programCaptor.value
 
+        // validate Ethernet dst address check
         // Using scapy to generate IPv6 NS packet:
         // eth = Ether(src="00:01:02:03:04:05", dst="00:05:04:03:02:01")
         // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
         // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
-        // pkt = eth/ip6/icmp6
-        val nonHostDstMacNsPkt = "00050403020100010203040586DD6000000000183AFF2001000000000000" +
-                                 "0200001A1122334420010000000000000200001A33441122870045290000" +
-                                 "000020010000000000000200001A33441122"
+        // icmp6_opt = ICMPv6NDOptDstLLAddr(lladdr="00:01:02:03:04:05")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val nonHostDstMacNsPkt = "00050403020100010203040586DD6000000000203AFF2001000000000000" +
+                                 "0200001A1122334420010000000000000200001A3344112287003D170000" +
+                                 "000020010000000000000200001A334411220201000102030405"
         // invalid unicast ether dst -> pass
         verifyProgramRun(
             APF_VERSION_6,
@@ -2036,48 +2059,52 @@
         // eth = Ether(src="00:01:02:03:04:05", dst="33:33:ff:03:02:01")
         // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
         // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
-        // pkt = eth/ip6/icmp6
-        val nonMcastDstMacNsPkt = "3333ff03020100010203040586DD6000000000183AFF2001000000000000" +
-                                  "0200001A1122334420010000000000000200001A33441122870045290000" +
-                                  "000020010000000000000200001A33441122"
+        // icmp6_opt = ICMPv6NDOptDstLLAddr(lladdr="00:01:02:03:04:05")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val nonMcastDstMacNsPkt = "3333FF03020100010203040586DD6000000000203AFF20010000000000" +
+                                  "000200001A1122334420010000000000000200001A3344112287003D17" +
+                                  "0000000020010000000000000200001A334411220201000102030405"
         // mcast dst mac is not one of solicited mcast mac derived from one of device's ip -> pass
         verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(nonMcastDstMacNsPkt),
-                DROPPED_IPV6_NS_OTHER_HOST
+            APF_VERSION_6,
+            program,
+            HexDump.hexStringToByteArray(nonMcastDstMacNsPkt),
+            DROPPED_IPV6_NS_OTHER_HOST
         )
 
         // Using scapy to generate IPv6 NS packet:
         // eth = Ether(src="00:01:02:03:04:05", dst="33:33:ff:44:11:22")
         // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
         // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
-        // pkt = eth/ip6/icmp6
-        val hostMcastDstMacNsPkt = "3333ff44112200010203040586DD6000000000183AFF2001000000000000" +
-                                   "0200001A1122334420010000000000000200001A33441122870045290000" +
-                                   "000020010000000000000200001A33441122"
-        // mcast dst mac is one of solicited mcast mac derived from one of device's ip -> pass
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="00:01:02:03:04:05")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val hostMcastDstMacNsPkt = "3333FF44112200010203040586DD6000000000203AFF20010000000000" +
+                                   "000200001A1122334420010000000000000200001A3344112287003E17" +
+                                   "0000000020010000000000000200001A334411220101000102030405"
+        // mcast dst mac is one of solicited mcast mac derived from one of device's ip
+        // -> drop and replied
         verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(hostMcastDstMacNsPkt),
-                PASSED_IPV6_ICMP
+            APF_VERSION_6,
+            program,
+            HexDump.hexStringToByteArray(hostMcastDstMacNsPkt),
+            DROPPED_IPV6_NS_REPLIED_NON_DAD
         )
 
         // Using scapy to generate IPv6 NS packet:
         // eth = Ether(src="00:01:02:03:04:05", dst="FF:FF:FF:FF:FF:FF")
         // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
         // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
-        // pkt = eth/ip6/icmp6
-        val broadcastNsPkt = "FFFFFFFFFFFF00010203040586DD6000000000183AFF2001000000000000" +
-                             "0200001A1122334420010000000000000200001A33441122870045290000" +
-                             "000020010000000000000200001A33441122"
-        // mcast dst mac is broadcast address -> pass
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="00:01:02:03:04:05")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val broadcastNsPkt = "FFFFFFFFFFFF00010203040586DD6000000000203AFF200100000000000002000" +
+                             "01A1122334420010000000000000200001A3344112287003E1700000000200100" +
+                             "00000000000200001A334411220101000102030405"
+        // mcast dst mac is broadcast address -> drop and replied
         verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(broadcastNsPkt),
-                PASSED_IPV6_ICMP
+            APF_VERSION_6,
+            program,
+            HexDump.hexStringToByteArray(broadcastNsPkt),
+            DROPPED_IPV6_NS_REPLIED_NON_DAD
         )
 
         // validate IPv6 dst address check
@@ -2086,80 +2113,87 @@
         // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
         // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
         // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
-        // pkt = eth/ip6/icmp6
-        val validHostDstIpNsPkt = "02030405060700010203040586DD6000000000183AFF200100000000000" +
-                                  "00200001A1122334420010000000000000200001A334411228700452900" +
-                                  "00000020010000000000000200001A33441122"
-        // dst ip is one of device's ip -> Pass
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="00:01:02:03:04:05")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val validHostDstIpNsPkt = "02030405060700010203040586DD6000000000203AFF200100000000000" +
+                                  "00200001A1122334420010000000000000200001A3344112287003E1700" +
+                                  "00000020010000000000000200001A334411220101000102030405"
+        // dst ip is one of device's ip -> drop and replied
         verifyProgramRun(
             APF_VERSION_6,
             program,
             HexDump.hexStringToByteArray(validHostDstIpNsPkt),
-            PASSED_IPV6_ICMP
+            DROPPED_IPV6_NS_REPLIED_NON_DAD
         )
 
         // Using scapy to generate IPv6 NS packet:
         // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
         // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::100:1b:aabb:ccdd", hlim=255)
         // icmp6 = ICMPv6ND_NS(tgt="2001::100:1b:aabb:ccdd")
-        // pkt = eth/ip6/icmp6
-        val validHostAnycastDstIpNsPkt = "02030405060700010203040586DD6000000000183AFF20010000" +
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="00:01:02:03:04:05")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val validHostAnycastDstIpNsPkt = "02030405060700010203040586DD6000000000203AFF20010000" +
                                          "000000000200001A1122334420010000000000000100001BAABB" +
-                                         "CCDD8700E0C00000000020010000000000000100001BAABBCCDD"
-        // dst ip is device's anycast address -> Pass
+                                         "CCDD8700D9AE0000000020010000000000000100001BAABBCCDD" +
+                                         "0101000102030405"
+        // dst ip is device's anycast address -> drop and replied
         verifyProgramRun(
             APF_VERSION_6,
             program,
             HexDump.hexStringToByteArray(validHostAnycastDstIpNsPkt),
-            PASSED_IPV6_ICMP
+            DROPPED_IPV6_NS_REPLIED_NON_DAD
         )
 
         // Using scapy to generate IPv6 NS packet:
         // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
         // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:4444:5555", hlim=255)
         // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
-        // pkt = eth/ip6/icmp6
-        val nonHostUcastDstIpNsPkt = "02030405060700010203040586DD6000000000183AFF200100000000" +
-                                     "00000200001A1122334420010000000000000200001A444455558700" +
-                                     "EFF50000000020010000000000000200001A33441122"
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="00:01:02:03:04:05")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val nonHostUcastDstIpNsPkt = "02030405060700010203040586DD6000000000203AFF2001000000000" +
+                                     "0000200001A1122334420010000000000000200001A444455558700E8" +
+                                     "E30000000020010000000000000200001A334411220101000102030405"
         // unicast dst ip is not one of device's ip -> pass
         verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(nonHostUcastDstIpNsPkt),
-                DROPPED_IPV6_NS_OTHER_HOST
+            APF_VERSION_6,
+            program,
+            HexDump.hexStringToByteArray(nonHostUcastDstIpNsPkt),
+            DROPPED_IPV6_NS_OTHER_HOST
         )
 
         // Using scapy to generate IPv6 NS packet:
         // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
         // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="ff02::1:ff44:1133", hlim=255)
         // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
-        // pkt = eth/ip6/icmp6
-        val nonHostMcastDstIpNsPkt = "02030405060700010203040586DD6000000000183AFF200100000000" +
-                                     "00000200001A11223344FF0200000000000000000001FF4411338700" +
-                                     "9C2E0000000020010000000000000200001A33441122"
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="00:01:02:03:04:05")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val nonHostMcastDstIpNsPkt = "02030405060700010203040586DD6000000000203AFF2001000000000" +
+                                     "0000200001A11223344FF0200000000000000000001FF441133870095" +
+                                     "1C0000000020010000000000000200001A334411220101000102030405"
         // mcast dst ip is not one of solicited mcast ip derived from one of device's ip -> pass
         verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(nonHostMcastDstIpNsPkt),
-                DROPPED_IPV6_NS_OTHER_HOST
+            APF_VERSION_6,
+            program,
+            HexDump.hexStringToByteArray(nonHostMcastDstIpNsPkt),
+            DROPPED_IPV6_NS_OTHER_HOST
         )
 
         // Using scapy to generate IPv6 NS packet:
         // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
         // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="ff02::1:ff44:1122", hlim=255)
         // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
-        // pkt = eth/ip6/icmp6
-        val hostMcastDstIpNsPkt = "02030405060700010203040586DD6000000000183AFF200100000000" +
-                                  "00000200001A11223344FF0200000000000000000001FF4411228700" +
-                                  "9C2E0000000020010000000000000200001A33441122"
-        // mcast dst ip is one of solicited mcast ip derived from one of device's ip -> pass
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="00:01:02:03:04:05")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val hostMcastDstIpNsPkt = "02030405060700010203040586DD6000000000203AFF2001000000000000" +
+                                  "0200001A11223344FF0200000000000000000001FF4411228700952D0000" +
+                                  "000020010000000000000200001A334411220101000102030405"
+        // mcast dst ip is one of solicited mcast ip derived from one of device's ip
+        //   -> drop and replied
         verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(hostMcastDstIpNsPkt),
-                PASSED_IPV6_ICMP
+            APF_VERSION_6,
+            program,
+            HexDump.hexStringToByteArray(hostMcastDstIpNsPkt),
+            DROPPED_IPV6_NS_REPLIED_NON_DAD
         )
 
         // validate IPv6 NS payload check
@@ -2184,24 +2218,6 @@
         // Using scapy to generate IPv6 NS packet:
         // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
         // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
-        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
-        // icmp6_opt_1 = ICMPv6NDOptSrcLLAddr(lladdr="01:02:03:04:05:06")
-        // icmp6_opt_2 = ICMPv6NDOptUnknown(type=14, len=6, data='\x11\x22\x33\x44\x55\x66')
-        // pkt = eth/ip6/icmp6/icmp6_opt_1/icmp6_opt_2
-        val longNsPkt = "02030405060700010203040586DD6000000000283AFF20010000000000000200001A11" +
-                        "22334420010000000000000200001A3344112287009339000000002001000000000000" +
-                        "0200001A3344112201010102030405060E06112233445566"
-        // payload len > 32 -> pass
-        verifyProgramRun(
-                APF_VERSION_6,
-                program,
-                HexDump.hexStringToByteArray(longNsPkt),
-                PASSED_IPV6_NS_MULTIPLE_OPTIONS
-        )
-
-        // Using scapy to generate IPv6 NS packet:
-        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
-        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
         // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:4444:5555")
         // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="01:02:03:04:05:06")
         // pkt = eth/ip6/icmp6/icmp6_opt
@@ -2250,6 +2266,377 @@
                 DROPPED_IPV6_NS_INVALID
         )
 
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:1234:5678")
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="01:02:03:04:05:06")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val tentativeTargetIpNsPkt = "02030405060700010203040586DD6000000000203AFF200100000000" +
+                                     "00000200001A1122334420010000000000000200001A334411228700" +
+                                     "16CE0000000020010000000000000200001A123456780101010203040506"
+        // target ip is one of tentative address -> pass
+        verifyProgramRun(
+                APF_VERSION_6,
+                program,
+                HexDump.hexStringToByteArray(tentativeTargetIpNsPkt),
+                PASSED_IPV6_NS_TENTATIVE
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1c:2255:6666")
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="01:02:03:04:05:06")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val invalidTargetIpNsPkt = "02030405060700010203040586DD6000000000203AFF200100000000000" +
+                                   "00200001A1122334420010000000000000200001A334411228700F6BC00" +
+                                   "00000020010000000000000200001C225566660101010203040506"
+        // target ip is none of {non-tentative, anycast} -> drop
+        verifyProgramRun(
+                APF_VERSION_6,
+                program,
+                HexDump.hexStringToByteArray(invalidTargetIpNsPkt),
+                DROPPED_IPV6_NS_OTHER_HOST
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="::", dst="ff02::1:ff44:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt = ICMPv6NDOptDstLLAddr(lladdr="02:03:04:05:06:07")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val dadNsPkt = "02030405060700010203040586DD6000000000203AFF000000000000000000000000000" +
+                       "00000FF0200000000000000000001FF4411228700F4A800000000200100000000000002" +
+                       "00001A334411220201020304050607"
+        // DAD NS request -> pass
+        verifyProgramRun(
+                APF_VERSION_6,
+                program,
+                HexDump.hexStringToByteArray(dadNsPkt),
+                PASSED_IPV6_NS_DAD
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // pkt = eth/ip6/icmp6
+        val noOptionNsPkt = "02030405060700010203040586DD6000000000183AFF2001000000000000020000" +
+                            "1A1122334420010000000000000200001A33441122870045290000000020010000" +
+                            "000000000200001A33441122"
+        // payload len < 32 -> pass
+        verifyProgramRun(
+                APF_VERSION_6,
+                program,
+                HexDump.hexStringToByteArray(noOptionNsPkt),
+                PASSED_IPV6_NS_NO_SLLA_OPTION
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="ff01::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="01:02:03:04:05:06")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val nonDadMcastSrcIpPkt = "02030405060700010203040586DD6000000000203AFFFF01000000000000" +
+                                  "0200001A1122334420010000000000000200001A3344112287005C130000" +
+                                  "000020010000000000000200001A334411220101010203040506"
+        // non-DAD src IPv6 is FF::/8 -> drop
+        verifyProgramRun(
+                APF_VERSION_6,
+                program,
+                HexDump.hexStringToByteArray(nonDadMcastSrcIpPkt),
+                DROPPED_IPV6_NS_INVALID
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="0001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="01:02:03:04:05:06")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val nonDadLoopbackSrcIpPkt = "02030405060700010203040586DD6000000000203AFF0001000000000" +
+                                     "0000200001A1122334420010000000000000200001A3344112287005B" +
+                                     "140000000020010000000000000200001A334411220101010203040506"
+        // non-DAD src IPv6 is 00::/8 -> drop
+        verifyProgramRun(
+                APF_VERSION_6,
+                program,
+                HexDump.hexStringToByteArray(nonDadLoopbackSrcIpPkt),
+                DROPPED_IPV6_NS_INVALID
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt1 = ICMPv6NDOptDstLLAddr(lladdr="01:02:03:04:05:06")
+        // icmp6_opt2 = ICMPv6NDOptSrcLLAddr(lladdr="01:02:03:04:05:06")
+        // pkt = eth/ip6/icmp6/icmp6_opt1/icmp6_opt2
+        val sllaNotFirstOptionNsPkt = "02030405060700010203040586DD6000000000283AFF200100000000" +
+                                      "00000200001A1122334420010000000000000200001A334411228700" +
+                                      "2FFF0000000020010000000000000200001A33441122020101020304" +
+                                      "05060101010203040506"
+        // non-DAD with multiple options, SLLA in 2nd option -> pass
+        verifyProgramRun(
+                APF_VERSION_6,
+                program,
+                HexDump.hexStringToByteArray(sllaNotFirstOptionNsPkt),
+                PASSED_IPV6_NS_NO_SLLA_OPTION
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt = ICMPv6NDOptDstLLAddr(lladdr="01:02:03:04:05:06")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val noSllaOptionNsPkt = "02030405060700010203040586DD6000000000203AFF200100000000000002" +
+                                "00001A1122334420010000000000000200001A3344112287003A1400000000" +
+                                "20010000000000000200001A334411220201010203040506"
+        // non-DAD with one option but not SLLA -> pass
+        verifyProgramRun(
+                APF_VERSION_6,
+                program,
+                HexDump.hexStringToByteArray(noSllaOptionNsPkt),
+                PASSED_IPV6_NS_NO_SLLA_OPTION
+        )
+
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="2001::200:1a:3344:1122", hlim=255)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="01:02:03:04:05:06")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val mcastMacSllaOptionNsPkt = "02030405060700010203040586DD6000000000203AFF200100000000" +
+                                      "00000200001A1122334420010000000000000200001A334411228700" +
+                                      "3B140000000020010000000000000200001A33441122010101020304" +
+                                      "0506"
+        // non-DAD, SLLA is multicast MAC -> drop
+        verifyProgramRun(
+                APF_VERSION_6,
+                program,
+                HexDump.hexStringToByteArray(mcastMacSllaOptionNsPkt),
+                DROPPED_IPV6_NS_INVALID
+        )
+        apfFilter.shutdown()
+    }
+
+    // The APFv6 code path is only turned on in V+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testNaTransmit() {
+        val apfFilter =
+            ApfFilter(
+                context,
+                getDefaultConfig(),
+                ifParams,
+                ipClientCallback,
+                metrics,
+                dependencies
+            )
+        val lp = LinkProperties()
+        for (addr in hostIpv6Addresses) {
+            lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
+        }
+
+        apfFilter.setLinkProperties(lp)
+        val programCaptor = ArgumentCaptor.forClass(ByteArray::class.java)
+        verify(ipClientCallback, times(3)).installPacketFilter(programCaptor.capture())
+        val program = programCaptor.allValues.last()
+        val validIpv6Addresses = hostIpv6Addresses + hostAnycast6Addresses
+        for (addr in validIpv6Addresses) {
+            // unicast solicited NS request
+            val receivedUcastNsPacket = generateNsPacket(
+                senderMacAddress,
+                apfFilter.mHardwareAddress,
+                senderIpv6Address,
+                addr,
+                addr
+            )
+
+            verifyProgramRun(
+                APF_VERSION_6,
+                program,
+                receivedUcastNsPacket,
+                DROPPED_IPV6_NS_REPLIED_NON_DAD
+            )
+
+            val transmittedUcastPacket = ApfJniUtils.getTransmittedPacket()
+            val expectedUcastNaPacket = generateNaPacket(
+                apfFilter.mHardwareAddress,
+                senderMacAddress,
+                addr,
+                senderIpv6Address,
+                0xe0000000.toInt(), //  R=1, S=1, O=1
+                addr
+            )
+
+            assertContentEquals(
+                expectedUcastNaPacket,
+                transmittedUcastPacket
+            )
+
+            val solicitedMcastAddr = NetworkStackUtils.ipv6AddressToSolicitedNodeMulticast(
+                InetAddress.getByAddress(addr) as Inet6Address
+            )!!
+            val mcastDa = NetworkStackUtils.ipv6MulticastToEthernetMulticast(solicitedMcastAddr)
+                .toByteArray()
+
+            // multicast solicited NS request
+            var receivedMcastNsPacket = generateNsPacket(
+                senderMacAddress,
+                mcastDa,
+                senderIpv6Address,
+                solicitedMcastAddr.address,
+                addr
+            )
+
+            verifyProgramRun(
+                APF_VERSION_6,
+                program,
+                receivedMcastNsPacket,
+                DROPPED_IPV6_NS_REPLIED_NON_DAD
+            )
+
+            val transmittedMcastPacket = ApfJniUtils.getTransmittedPacket()
+            val expectedMcastNaPacket = generateNaPacket(
+                apfFilter.mHardwareAddress,
+                senderMacAddress,
+                addr,
+                senderIpv6Address,
+                0xe0000000.toInt(), // R=1, S=1, O=1
+                addr
+            )
+
+            assertContentEquals(
+                expectedMcastNaPacket,
+                transmittedMcastPacket
+            )
+        }
+
+        apfFilter.shutdown()
+    }
+
+    // The APFv6 code path is only turned on in V+
+    @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+    @Test
+    fun testNaTransmitWithTclass() {
+        // mock nd traffic class from /proc/sys/net/ipv6/conf/{ifname}/ndisc_tclass to 20
+        `when`(dependencies.getNdTrafficClass(any())).thenReturn(20)
+        val apfFilter =
+            ApfFilter(
+                context,
+                getDefaultConfig(),
+                ifParams,
+                ipClientCallback,
+                metrics,
+                dependencies
+            )
+
+        val lp = LinkProperties()
+        for (addr in hostIpv6Addresses) {
+            lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
+        }
+        apfFilter.setLinkProperties(lp)
+        val programCaptor = ArgumentCaptor.forClass(ByteArray::class.java)
+        verify(ipClientCallback, times(3)).installPacketFilter(programCaptor.capture())
+        val program = programCaptor.allValues.last()
+        // Using scapy to generate IPv6 NS packet:
+        // eth = Ether(src="00:01:02:03:04:05", dst="02:03:04:05:06:07")
+        // ip6 = IPv6(src="2001::200:1a:1122:3344", dst="ff02::1:ff44:1122", hlim=255, tc=20)
+        // icmp6 = ICMPv6ND_NS(tgt="2001::200:1a:3344:1122")
+        // icmp6_opt = ICMPv6NDOptSrcLLAddr(lladdr="00:01:02:03:04:05")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val hostMcastDstIpNsPkt = "02030405060700010203040586DD6140000000203AFF2001000000000000" +
+                "0200001A11223344FF0200000000000000000001FF4411228700952D0000" +
+                "000020010000000000000200001A334411220101000102030405"
+        verifyProgramRun(
+            APF_VERSION_6,
+            program,
+            HexDump.hexStringToByteArray(hostMcastDstIpNsPkt),
+            DROPPED_IPV6_NS_REPLIED_NON_DAD
+        )
+
+        val transmitPkt = ApfJniUtils.getTransmittedPacket()
+        // Using scapy to generate IPv6 NA packet:
+        // eth = Ether(src="02:03:04:05:06:07", dst="00:01:02:03:04:05")
+        // ip6 = IPv6(src="2001::200:1a:3344:1122", dst="2001::200:1a:1122:3344", hlim=255, tc=20)
+        // icmp6 = ICMPv6ND_NA(tgt="2001::200:1a:3344:1122", R=1, S=1, O=1)
+        // icmp6_opt = ICMPv6NDOptDstLLAddr(lladdr="02:03:04:05:06:07")
+        // pkt = eth/ip6/icmp6/icmp6_opt
+        val expectedNaPacket = "00010203040502030405060786DD6140000000203AFF2001000000000000020" +
+                "0001A3344112220010000000000000200001A1122334488005610E000000020" +
+                "010000000000000200001A334411220201020304050607"
+        assertContentEquals(
+            HexDump.hexStringToByteArray(expectedNaPacket),
+            transmitPkt
+        )
+    }
+
+    @Test
+    fun testNdOffloadDisabled() {
+        val apfConfig = getDefaultConfig()
+        apfConfig.shouldHandleNdOffload = false
+        val apfFilter =
+            ApfFilter(
+                context,
+                apfConfig,
+                ifParams,
+                ipClientCallback,
+                metrics,
+                dependencies
+            )
+        val lp = LinkProperties()
+        for (addr in hostIpv6Addresses) {
+            lp.addLinkAddress(LinkAddress(InetAddress.getByAddress(addr), 64))
+        }
+
+        apfFilter.setLinkProperties(lp)
+        val programCaptor = ArgumentCaptor.forClass(ByteArray::class.java)
+        verify(ipClientCallback, times(3)).installPacketFilter(programCaptor.capture())
+        val program = programCaptor.allValues.last()
+        val validIpv6Addresses = hostIpv6Addresses + hostAnycast6Addresses
+        for (addr in validIpv6Addresses) {
+            // unicast solicited NS request
+            val receivedUcastNsPacket = generateNsPacket(
+                senderMacAddress,
+                apfFilter.mHardwareAddress,
+                senderIpv6Address,
+                addr,
+                addr
+            )
+
+            verifyProgramRun(
+                APF_VERSION_6,
+                program,
+                receivedUcastNsPacket,
+                PASSED_IPV6_ICMP
+            )
+
+            val solicitedMcastAddr = NetworkStackUtils.ipv6AddressToSolicitedNodeMulticast(
+                InetAddress.getByAddress(addr) as Inet6Address
+            )!!
+            val mcastDa = NetworkStackUtils.ipv6MulticastToEthernetMulticast(solicitedMcastAddr)
+                .toByteArray()
+
+            // multicast solicited NS request
+            var receivedMcastNsPacket = generateNsPacket(
+                senderMacAddress,
+                mcastDa,
+                senderIpv6Address,
+                solicitedMcastAddr.address,
+                addr
+            )
+
+            verifyProgramRun(
+                APF_VERSION_6,
+                program,
+                receivedMcastNsPacket,
+                PASSED_IPV6_ICMP
+            )
+        }
         apfFilter.shutdown()
     }
 
@@ -2369,6 +2756,53 @@
         config.ieee802_3Filter = false
         config.ethTypeBlackList = IntArray(0)
         config.shouldHandleArpOffload = true
+        config.shouldHandleNdOffload = true
         return config
     }
+
+    private fun generateNsPacket(
+        srcMac: ByteArray,
+        dstMac: ByteArray,
+        srcIp: ByteArray,
+        dstIp: ByteArray,
+        target: ByteArray,
+    ): ByteArray {
+        val nsPacketBuf = NeighborSolicitation.build(
+            MacAddress.fromBytes(srcMac),
+            MacAddress.fromBytes(dstMac),
+            InetAddress.getByAddress(srcIp) as Inet6Address,
+            InetAddress.getByAddress(dstIp) as Inet6Address,
+            InetAddress.getByAddress(target) as Inet6Address
+        )
+
+        val nsPacket = ByteArray(
+            ETHER_HEADER_LEN + IPV6_HEADER_LEN + ICMPV6_NS_HEADER_LEN + 8 // option length
+        )
+        nsPacketBuf.get(nsPacket)
+        return nsPacket
+    }
+
+    private fun generateNaPacket(
+        srcMac: ByteArray,
+        dstMac: ByteArray,
+        srcIp: ByteArray,
+        dstIp: ByteArray,
+        flags: Int,
+        target: ByteArray,
+    ): ByteArray {
+        val naPacketBuf = NeighborAdvertisement.build(
+            MacAddress.fromBytes(srcMac),
+            MacAddress.fromBytes(dstMac),
+            InetAddress.getByAddress(srcIp) as Inet6Address,
+            InetAddress.getByAddress(dstIp) as Inet6Address,
+            flags,
+            InetAddress.getByAddress(target) as Inet6Address
+        )
+        val naPacket = ByteArray(
+            ETHER_HEADER_LEN + IPV6_HEADER_LEN + ICMPV6_NA_HEADER_LEN + 8 // lla option length
+        )
+
+        naPacketBuf.get(naPacket)
+        return naPacket
+    }
 }
diff --git a/tests/unit/src/android/net/apf/ApfTest.java b/tests/unit/src/android/net/apf/ApfTest.java
index 32ee7c3..9c36df7 100644
--- a/tests/unit/src/android/net/apf/ApfTest.java
+++ b/tests/unit/src/android/net/apf/ApfTest.java
@@ -47,10 +47,8 @@
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ECHO_REQUEST_TYPE;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
-import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
@@ -1759,32 +1757,14 @@
         doTestApfFilterMulticastPingWhileDozing(true /* isLightDozing */);
     }
 
-    @Test
-    @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
-    public void testShouldHandleLightDozeKillSwitch() throws Exception {
-        final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
-        final ApfConfiguration configuration = getDefaultConfig();
-        configuration.shouldHandleLightDoze = false;
-        final ApfFilter apfFilter = TestApfFilter.createTestApfFilter(mContext, ipClientCallback,
-                configuration, mNetworkQuirkMetrics, mDependencies);
-        final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
-                ArgumentCaptor.forClass(BroadcastReceiver.class);
-        verify(mDependencies).addDeviceIdleReceiver(receiverCaptor.capture(), anyBoolean());
-        final BroadcastReceiver receiver = receiverCaptor.getValue();
-        doReturn(true).when(mPowerManager).isDeviceLightIdleMode();
-        receiver.onReceive(mContext, new Intent(ACTION_DEVICE_LIGHT_IDLE_MODE_CHANGED));
-        assertFalse(apfFilter.isInDozeMode());
-    }
-
     private void doTestApfFilterMulticastPingWhileDozing(boolean isLightDozing) throws Exception {
         final MockIpClientCallback ipClientCallback = new MockIpClientCallback();
         final ApfConfiguration configuration = getDefaultConfig();
-        configuration.shouldHandleLightDoze = true;
         final ApfFilter apfFilter = TestApfFilter.createTestApfFilter(mContext, ipClientCallback,
                 configuration, mNetworkQuirkMetrics, mDependencies);
         final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
                 ArgumentCaptor.forClass(BroadcastReceiver.class);
-        verify(mDependencies).addDeviceIdleReceiver(receiverCaptor.capture(), anyBoolean());
+        verify(mDependencies).addDeviceIdleReceiver(receiverCaptor.capture());
         final BroadcastReceiver receiver = receiverCaptor.getValue();
 
         // Construct a multicast ICMPv6 ECHO request.
diff --git a/tests/unit/src/com/android/networkstack/netlink/TcpSocketTrackerTest.java b/tests/unit/src/com/android/networkstack/netlink/TcpSocketTrackerTest.java
index e8f14b5..ed423e4 100644
--- a/tests/unit/src/com/android/networkstack/netlink/TcpSocketTrackerTest.java
+++ b/tests/unit/src/com/android/networkstack/netlink/TcpSocketTrackerTest.java
@@ -55,8 +55,6 @@
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
 import android.os.PowerManager;
 import android.util.Log;
 import android.util.Log.TerribleFailureHandler;
@@ -739,20 +737,12 @@
             boolean featureEnabled) throws Exception {
         final TcpSocketTracker tst = new TcpSocketTracker(mDependencies, mNetwork);
         tst.setNetworkCapabilities(CELL_NOT_METERED_CAPABILITIES);
-
-        // Verify that device idle mode receiver does not register as the event for NM creation
-        // is not yet received.
-        verify(mDependencies, never()).addDeviceIdleReceiver(any(),
-                anyBoolean(), anyBoolean(), any());
-
-        final Handler nmHandler = new Handler(Looper.getMainLooper());
-        tst.init(nmHandler, new LinkProperties(), CELL_NOT_METERED_CAPABILITIES);
         final ArgumentCaptor<BroadcastReceiver> receiverCaptor =
                 ArgumentCaptor.forClass(BroadcastReceiver.class);
 
         // Enable doze mode with 1 netlink message.
         verify(mDependencies).addDeviceIdleReceiver(receiverCaptor.capture(),
-                anyBoolean(), anyBoolean(), eq(nmHandler));
+                anyBoolean(), anyBoolean());
         final BroadcastReceiver receiver = receiverCaptor.getValue();
         if (dozeModeType == DEEP_DOZE) {
             doReturn(true).when(mPowerManager).isDeviceIdleMode();
diff --git a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
index a949f80..d5d1c90 100644
--- a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
+++ b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
@@ -633,7 +633,6 @@
         doReturn(0).when(mRandom).nextInt();
 
         doReturn(mNetd).when(mTstDependencies).getNetd();
-        doNothing().when(mTst).init(any(), any(), any());
         // DNS probe timeout should not be defined more than half of HANDLER_TIMEOUT_MS. Otherwise,
         // it will fail the test because of timeout expired for querying AAAA and A sequentially.
         doReturn(200).when(mResources)
@@ -2731,30 +2730,12 @@
     }
 
     @Test
-    public void testTcpSocketTracker_init() throws Exception {
-        setDataStallEvaluationType(DATA_STALL_EVALUATION_TYPE_TCP);
-        final WrappedNetworkMonitor wnm = makeCellMeteredNetworkMonitor();
-        // makeCellMeteredNetworkMonitor() creates the NM first and then assign
-        // new NetworkCapabilities, so notifyNMCreated() will start with a empty NC
-        // then update CELL_METERED_CAPABILITIES in the follow up call.
-        final InOrder inOrder = inOrder(mTst);
-        inOrder.verify(mTst).init(
-                eq(wnm.getHandler()),
-                eq(new LinkProperties()),
-                eq(new NetworkCapabilities(null)));
-        inOrder.verify(mTst).setNetworkCapabilities(eq(CELL_METERED_CAPABILITIES));
-    }
-
-    @Test
     public void testDataStall_setOpportunisticMode() {
         setDataStallEvaluationType(DATA_STALL_EVALUATION_TYPE_TCP);
         WrappedNetworkMonitor wnm = makeCellNotMeteredNetworkMonitor();
         InOrder inOrder = inOrder(mTst);
-        // Initialized.
-        inOrder.verify(mTst).init(
-                eq(wnm.getHandler()),
-                eq(new LinkProperties()),
-                eq(new NetworkCapabilities(null)));
+        // Initialized with default value.
+        inOrder.verify(mTst).setOpportunisticMode(false);
 
         // Strict mode.
         wnm.notifyPrivateDnsSettingsChanged(new PrivateDnsConfig("dns.google", new InetAddress[0]));