Merge "Implement NS filter and NA reply for ND offload in APFv6" into main am: 1f62e475de am: a3b6082ce3

Original change: https://android-review.googlesource.com/c/platform/packages/modules/NetworkStack/+/3119242

Change-Id: I7f896438d4f3f448f6e93ab9b488a0f6caf84915
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
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 9e0ed60..92c25e7 100644
--- a/src/android/net/apf/ApfFilter.java
+++ b/src/android/net/apf/ApfFilter.java
@@ -78,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;
@@ -86,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;
@@ -111,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;
@@ -1925,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);
     }
 
     /**
@@ -1977,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
diff --git a/tests/unit/src/android/net/apf/ApfNewTest.kt b/tests/unit/src/android/net/apf/ApfNewTest.kt
index 6b78c23..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
@@ -181,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
@@ -628,7 +635,7 @@
                 program
         )
         assertContentEquals(
-                listOf("0: drop        counter=43"),
+                listOf("0: drop        counter=46"),
                 ApfJniUtils.disassembleApf(program).map { it.trim() }
         )
 
@@ -677,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() }
         )
@@ -970,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",
@@ -2014,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)
@@ -2028,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,
@@ -2048,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
@@ -2098,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
@@ -2196,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
@@ -2262,9 +2266,315 @@
                 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()