By default this is turned off.
 
+accept_unsolicited_na - BOOLEAN
+       Add a new neighbour cache entry in STALE state for routers on receiving an
+       unsolicited neighbour advertisement with target link-layer address option
+       specified. This is as per router-side behavior documented in RFC9131.
+       This has lower precedence than drop_unsolicited_na.
+
+        ====   ======  ======  ==============================================
+        drop   accept  fwding                   behaviour
+        ----   ------  ------  ----------------------------------------------
+           1        X       X  Drop NA packet and don't pass up the stack
+           0        0       X  Pass NA packet up the stack, don't update NC
+           0        1       0  Pass NA packet up the stack, don't update NC
+           0        1       1  Pass NA packet up the stack, and add a STALE
+                               NC entry
+        ====   ======  ======  ==============================================
+
+       This will optimize the return path for the initial off-link communication
+       that is initiated by a directly connected host, by ensuring that
+       the first-hop router which turns on this setting doesn't have to
+       buffer the initial return packets to do neighbour-solicitation.
+       The prerequisite is that the host is configured to send
+       unsolicited neighbour advertisements on interface bringup.
+       This setting should be used in conjunction with the ndisc_notify setting
+       on the host to satisfy this prerequisite.
+
+       By default this is turned off.
+
 enhanced_dad - BOOLEAN
        Include a nonce option in the IPv6 neighbor solicitation messages used for
        duplicate address detection per RFC7527. A received DAD NS will only signal
 
        struct inet6_dev *idev = __in6_dev_get(dev);
        struct inet6_ifaddr *ifp;
        struct neighbour *neigh;
+       bool create_neigh;
 
        if (skb->len < sizeof(struct nd_msg)) {
                ND_PRINTK(2, warn, "NA: packet too short\n");
        /* For some 802.11 wireless deployments (and possibly other networks),
         * there will be a NA proxy and unsolicitd packets are attacks
         * and thus should not be accepted.
+        * drop_unsolicited_na takes precedence over accept_unsolicited_na
         */
        if (!msg->icmph.icmp6_solicited && idev &&
            idev->cnf.drop_unsolicited_na)
                in6_ifa_put(ifp);
                return;
        }
-       neigh = neigh_lookup(&nd_tbl, &msg->target, dev);
+       /* RFC 9131 updates original Neighbour Discovery RFC 4861.
+        * An unsolicited NA can now create a neighbour cache entry
+        * on routers if it has Target LL Address option.
+        *
+        * drop   accept  fwding                   behaviour
+        * ----   ------  ------  ----------------------------------------------
+        *    1        X       X  Drop NA packet and don't pass up the stack
+        *    0        0       X  Pass NA packet up the stack, don't update NC
+        *    0        1       0  Pass NA packet up the stack, don't update NC
+        *    0        1       1  Pass NA packet up the stack, and add a STALE
+        *                          NC entry
+        * Note that we don't do a (daddr == all-routers-mcast) check.
+        */
+       create_neigh = !msg->icmph.icmp6_solicited && lladdr &&
+                      idev && idev->cnf.forwarding &&
+                      idev->cnf.accept_unsolicited_na;
+       neigh = __neigh_lookup(&nd_tbl, &msg->target, dev, create_neigh);
 
        if (neigh) {
                u8 old_flags = neigh->flags;
 
--- /dev/null
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+# This test is for the accept_unsolicited_na feature to
+# enable RFC9131 behaviour. The following is the test-matrix.
+# drop   accept  fwding                   behaviour
+# ----   ------  ------  ----------------------------------------------
+#    1        X       X  Drop NA packet and don't pass up the stack
+#    0        0       X  Pass NA packet up the stack, don't update NC
+#    0        1       0  Pass NA packet up the stack, don't update NC
+#    0        1       1  Pass NA packet up the stack, and add a STALE
+#                           NC entry
+
+ret=0
+# Kselftest framework requirement - SKIP code is 4.
+ksft_skip=4
+
+PAUSE_ON_FAIL=no
+PAUSE=no
+
+HOST_NS="ns-host"
+ROUTER_NS="ns-router"
+
+HOST_INTF="veth-host"
+ROUTER_INTF="veth-router"
+
+ROUTER_ADDR="2000:20::1"
+HOST_ADDR="2000:20::2"
+SUBNET_WIDTH=64
+ROUTER_ADDR_WITH_MASK="${ROUTER_ADDR}/${SUBNET_WIDTH}"
+HOST_ADDR_WITH_MASK="${HOST_ADDR}/${SUBNET_WIDTH}"
+
+IP_HOST="ip -6 -netns ${HOST_NS}"
+IP_HOST_EXEC="ip netns exec ${HOST_NS}"
+IP_ROUTER="ip -6 -netns ${ROUTER_NS}"
+IP_ROUTER_EXEC="ip netns exec ${ROUTER_NS}"
+
+tcpdump_stdout=
+tcpdump_stderr=
+
+log_test()
+{
+       local rc=$1
+       local expected=$2
+       local msg="$3"
+
+       if [ ${rc} -eq ${expected} ]; then
+               printf "    TEST: %-60s  [ OK ]\n" "${msg}"
+               nsuccess=$((nsuccess+1))
+       else
+               ret=1
+               nfail=$((nfail+1))
+               printf "    TEST: %-60s  [FAIL]\n" "${msg}"
+               if [ "${PAUSE_ON_FAIL}" = "yes" ]; then
+               echo
+                       echo "hit enter to continue, 'q' to quit"
+                       read a
+                       [ "$a" = "q" ] && exit 1
+               fi
+       fi
+
+       if [ "${PAUSE}" = "yes" ]; then
+               echo
+               echo "hit enter to continue, 'q' to quit"
+               read a
+               [ "$a" = "q" ] && exit 1
+       fi
+}
+
+setup()
+{
+       set -e
+
+       local drop_unsolicited_na=$1
+       local accept_unsolicited_na=$2
+       local forwarding=$3
+
+       # Setup two namespaces and a veth tunnel across them.
+       # On end of the tunnel is a router and the other end is a host.
+       ip netns add ${HOST_NS}
+       ip netns add ${ROUTER_NS}
+       ${IP_ROUTER} link add ${ROUTER_INTF} type veth \
+                peer name ${HOST_INTF} netns ${HOST_NS}
+
+       # Enable IPv6 on both router and host, and configure static addresses.
+       # The router here is the DUT
+       # Setup router configuration as specified by the arguments.
+       # forwarding=0 case is to check that a non-router
+       # doesn't add neighbour entries.
+        ROUTER_CONF=net.ipv6.conf.${ROUTER_INTF}
+       ${IP_ROUTER_EXEC} sysctl -qw \
+                ${ROUTER_CONF}.forwarding=${forwarding}
+       ${IP_ROUTER_EXEC} sysctl -qw \
+                ${ROUTER_CONF}.drop_unsolicited_na=${drop_unsolicited_na}
+       ${IP_ROUTER_EXEC} sysctl -qw \
+                ${ROUTER_CONF}.accept_unsolicited_na=${accept_unsolicited_na}
+       ${IP_ROUTER_EXEC} sysctl -qw ${ROUTER_CONF}.disable_ipv6=0
+       ${IP_ROUTER} addr add ${ROUTER_ADDR_WITH_MASK} dev ${ROUTER_INTF}
+
+       # Turn on ndisc_notify on host interface so that
+       # the host sends unsolicited NAs.
+       HOST_CONF=net.ipv6.conf.${HOST_INTF}
+       ${IP_HOST_EXEC} sysctl -qw ${HOST_CONF}.ndisc_notify=1
+       ${IP_HOST_EXEC} sysctl -qw ${HOST_CONF}.disable_ipv6=0
+       ${IP_HOST} addr add ${HOST_ADDR_WITH_MASK} dev ${HOST_INTF}
+
+       set +e
+}
+
+start_tcpdump() {
+       set -e
+       tcpdump_stdout=`mktemp`
+       tcpdump_stderr=`mktemp`
+       ${IP_ROUTER_EXEC} timeout 15s \
+                tcpdump --immediate-mode -tpni ${ROUTER_INTF} -c 1 \
+                "icmp6 && icmp6[0] == 136 && src ${HOST_ADDR}" \
+                > ${tcpdump_stdout} 2> /dev/null
+       set +e
+}
+
+cleanup_tcpdump()
+{
+       set -e
+       [[ ! -z  ${tcpdump_stdout} ]] && rm -f ${tcpdump_stdout}
+       [[ ! -z  ${tcpdump_stderr} ]] && rm -f ${tcpdump_stderr}
+       tcpdump_stdout=
+       tcpdump_stderr=
+       set +e
+}
+
+cleanup()
+{
+       cleanup_tcpdump
+       ip netns del ${HOST_NS}
+       ip netns del ${ROUTER_NS}
+}
+
+link_up() {
+       set -e
+       ${IP_ROUTER} link set dev ${ROUTER_INTF} up
+       ${IP_HOST} link set dev ${HOST_INTF} up
+       set +e
+}
+
+verify_ndisc() {
+       local drop_unsolicited_na=$1
+       local accept_unsolicited_na=$2
+       local forwarding=$3
+
+       neigh_show_output=$(${IP_ROUTER} neigh show \
+                to ${HOST_ADDR} dev ${ROUTER_INTF} nud stale)
+       if [ ${drop_unsolicited_na} -eq 0 ] && \
+                       [ ${accept_unsolicited_na} -eq 1 ] && \
+                       [ ${forwarding} -eq 1 ]; then
+               # Neighbour entry expected to be present for 011 case
+               [[ ${neigh_show_output} ]]
+       else
+               # Neighbour entry expected to be absent for all other cases
+               [[ -z ${neigh_show_output} ]]
+       fi
+}
+
+test_unsolicited_na_common()
+{
+       # Setup the test bed, but keep links down
+       setup $1 $2 $3
+
+       # Bring the link up, wait for the NA,
+       # and add a delay to ensure neighbour processing is done.
+       link_up
+       start_tcpdump
+
+       # Verify the neighbour table
+       verify_ndisc $1 $2 $3
+
+}
+
+test_unsolicited_na_combination() {
+       test_unsolicited_na_common $1 $2 $3
+       test_msg=("test_unsolicited_na: "
+               "drop_unsolicited_na=$1 "
+               "accept_unsolicited_na=$2 "
+               "forwarding=$3")
+       log_test $? 0 "${test_msg[*]}"
+       cleanup
+}
+
+test_unsolicited_na_combinations() {
+       # Args: drop_unsolicited_na accept_unsolicited_na forwarding
+
+       # Expect entry
+       test_unsolicited_na_combination 0 1 1
+
+       # Expect no entry
+       test_unsolicited_na_combination 0 0 0
+       test_unsolicited_na_combination 0 0 1
+       test_unsolicited_na_combination 0 1 0
+       test_unsolicited_na_combination 1 0 0
+       test_unsolicited_na_combination 1 0 1
+       test_unsolicited_na_combination 1 1 0
+       test_unsolicited_na_combination 1 1 1
+}
+
+###############################################################################
+# usage
+
+usage()
+{
+       cat <<EOF
+usage: ${0##*/} OPTS
+        -p          Pause on fail
+        -P          Pause after each test before cleanup
+EOF
+}
+
+###############################################################################
+# main
+
+while getopts :pPh o
+do
+       case $o in
+               p) PAUSE_ON_FAIL=yes;;
+               P) PAUSE=yes;;
+               h) usage; exit 0;;
+               *) usage; exit 1;;
+       esac
+done
+
+# make sure we don't pause twice
+[ "${PAUSE}" = "yes" ] && PAUSE_ON_FAIL=no
+
+if [ "$(id -u)" -ne 0 ];then
+       echo "SKIP: Need root privileges"
+       exit $ksft_skip;
+fi
+
+if [ ! -x "$(command -v ip)" ]; then
+       echo "SKIP: Could not run test without ip tool"
+       exit $ksft_skip
+fi
+
+if [ ! -x "$(command -v tcpdump)" ]; then
+       echo "SKIP: Could not run test without tcpdump tool"
+       exit $ksft_skip
+fi
+
+# start clean
+cleanup &> /dev/null
+
+test_unsolicited_na_combinations
+
+printf "\nTests passed: %3d\n" ${nsuccess}
+printf "Tests failed: %3d\n"   ${nfail}
+
+exit $ret