#!/bin/bash
# SPDX-License-Identifier: GPL-2.0

# This test is for the accept_untracked_na feature to
# enable RFC9131 behaviour. The following is the test-matrix.
# drop   accept  fwding                   behaviour
# ----   ------  ------  ----------------------------------------------
#    1        X       X  Don't update NC
#    0        0       X  Don't update NC
#    0        1       0  Don't update NC
#    0        1       1  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_untracked_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_untracked_na=${accept_untracked_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_untracked_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_untracked_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_untracked_na=$2 "
		"forwarding=$3")
	log_test $? 0 "${test_msg[*]}"
	cleanup
}

test_unsolicited_na_combinations() {
	# Args: drop_unsolicited_na accept_untracked_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