git: c173f02045c8 - main - tests/netinet6: Add SLAAC and RA validation tests to ndp
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Fri, 17 Apr 2026 22:58:52 UTC
The branch main has been updated by pouria:
URL: https://cgit.FreeBSD.org/src/commit/?id=c173f02045c8cfae219b26be99f9e02f291965fa
commit c173f02045c8cfae219b26be99f9e02f291965fa
Author: Pouria Mousavizadeh Tehrani <pouria@FreeBSD.org>
AuthorDate: 2026-04-17 20:25:18 +0000
Commit: Pouria Mousavizadeh Tehrani <pouria@FreeBSD.org>
CommitDate: 2026-04-17 22:52:24 +0000
tests/netinet6: Add SLAAC and RA validation tests to ndp
* RA hop limit validation
* RA source address validation
* Multi router RA validation
* Two hour rule RA validation
* SLAAC onlink prefix switching test
Reviewed by: glebius
Differential Revision: https://reviews.freebsd.org/D56128
---
tests/sys/netinet6/ndp.sh | 385 +++++++++++++++++++++++++++++++++++++++++++++-
tests/sys/netinet6/ra.py | 21 ++-
2 files changed, 397 insertions(+), 9 deletions(-)
diff --git a/tests/sys/netinet6/ndp.sh b/tests/sys/netinet6/ndp.sh
index 526ef27a7fb3..035f8fc9989f 100755
--- a/tests/sys/netinet6/ndp.sh
+++ b/tests/sys/netinet6/ndp.sh
@@ -3,6 +3,7 @@
# SPDX-License-Identifier: BSD-2-Clause
#
# Copyright (c) 2021 Alexander V. Chernikov
+# Copyright (c) 2026 Pouria Mousavizadeh Tehrani <pouria@FreeBSD.org>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
@@ -300,23 +301,28 @@ ndp_prefix_lifetime_extend_head() {
get_prefix_attr() {
local prefix=$1
local attr=$2
+ local jail=""
- ndp -p --libxo json | \
+ if [ -n "$3" ]; then
+ jail="jexec $3"
+ fi
+
+ ${jail} ndp -p --libxo json | \
jq -r '.ndp.["prefix-list"][] |
select(.prefix == "'${prefix}'") | .["'${attr}'"]'
}
# Given a prefix, return its expiry time in seconds.
prefix_expiry() {
- get_prefix_attr $1 "expires_sec"
+ get_prefix_attr $1 "expires_sec" $2
}
# Given a prefix, return its valid and preferred lifetimes.
prefix_lifetimes() {
local p v
- v=$(get_prefix_attr $1 "valid-lifetime")
- p=$(get_prefix_attr $1 "preferred-lifetime")
+ v=$(get_prefix_attr $1 "valid-lifetime" $2)
+ p=$(get_prefix_attr $1 "preferred-lifetime" $2)
echo $v $p
}
@@ -372,7 +378,7 @@ ndp_grand_linklayer_event_head() {
}
ndp_grand_linklayer_event_body() {
- local epair0 jname address mac
+ local epair0 jname prefix address mac
vnet_init
@@ -414,13 +420,382 @@ ndp_grand_linklayer_event_body() {
jexec ${jname}2 ndp -n ${prefix}1
}
+ndp_grand_linklayer_event_cleanup() {
+ vnet_cleanup
+}
+
+atf_test_case "ndp_input_validation_hlim" "cleanup"
+ndp_input_validation_hlim_head() {
+ atf_set descr 'Test RFC 4861 section 6.1.2: RA hop limit validation'
+ atf_set require.user root
+ atf_set require.progs python3 scapy
+}
+
+ndp_input_validation_hlim_body() {
+ local epair0 jname
+
+ vnet_init
+
+ jname="v6t-ndp_input_validation_hlim"
+
+ epair0=$(vnet_mkepair)
+
+ vnet_mkjail ${jname} ${epair0}a
+
+ ndp_if_up ${epair0}a ${jname}
+ ndp_if_up ${epair0}b
+ atf_check jexec ${jname} ifconfig ${epair0}a inet6 accept_rtadv
+
+ # Make sure that NAs from us are flagged as coming from a router.
+ atf_check -o ignore sysctl net.inet6.ip6.forwarding=1
+
+ # Send an invalid RA advertising a prefix.
+ atf_check -e ignore python3 $(atf_get_srcdir)/ra.py \
+ --sendif ${epair0}b \
+ --dst $(ndp_if_lladdr ${epair0}a ${jname}) \
+ --src $(ndp_if_lladdr ${epair0}b) \
+ --hoplimit 254
+
+ # Wait to make sure no router would appear.
+ sleep 0.5
+ atf_check -o empty jexec ${jname} ndp -r
+}
+
+ndp_input_validation_hlim_cleanup() {
+ vnet_cleanup
+}
+
+atf_test_case "ndp_input_validation_src_linklocal" "cleanup"
+ndp_input_validation_src_linklocal_head() {
+ atf_set descr 'Test RFC 4861 section 6.1.2: RA source address must be link-local'
+ atf_set require.user root
+ atf_set require.progs python3 scapy
+}
+
+ndp_input_validation_src_linklocal_body() {
+ local epair0 jname
+
+ vnet_init
+
+ jname="v6t-ndp_input_validation_src_linklocal"
+
+ epair0=$(vnet_mkepair)
+
+ vnet_mkjail ${jname} ${epair0}a
+
+ ndp_if_up ${epair0}a ${jname}
+ ndp_if_up ${epair0}b
+ atf_check jexec ${jname} ifconfig ${epair0}a inet6 accept_rtadv
+
+ # Make sure that NAs from us are flagged as coming from a router.
+ atf_check -o ignore sysctl net.inet6.ip6.forwarding=1
+
+ # Send an invalid RA with multicast source.
+ atf_check -e ignore python3 $(atf_get_srcdir)/ra.py \
+ --sendif ${epair0}b \
+ --dst $(ndp_if_lladdr ${epair0}a ${jname}) \
+ --src ff02::2
+
+ # Send an invalid RA with global unicast source.
+ atf_check -e ignore python3 $(atf_get_srcdir)/ra.py \
+ --sendif ${epair0}b \
+ --dst $(ndp_if_lladdr ${epair0}a ${jname}) \
+ --src 3fff::1
+
+ # Wait to make sure no router would appear.
+ sleep 0.5
+ atf_check -o empty jexec ${jname} ndp -r
+}
+
+ndp_input_validation_src_linklocal_cleanup() {
+ vnet_cleanup
+}
+
+atf_test_case "ndp_multirouter_pref" "cleanup"
+ndp_multirouter_pref_head() {
+ atf_set descr 'Test RFC 4861 section 6.3.4: multiple routers with different pref'
+ atf_set require.user root
+ atf_set require.progs jq python3 scapy
+}
+
+ndp_multirouter_pref_body() {
+ local epair0 jname prefix lladdr advrtrs
+
+ vnet_init
+
+ jname="v6t-ndp_multirouter_pref"
+ prefix="2001:db8:ffff:1000::"
+
+ epair0=$(vnet_mkepair)
+
+ vnet_mkjail ${jname} ${epair0}a
+
+ ndp_if_up ${epair0}a ${jname}
+ ndp_if_up ${epair0}b
+ atf_check jexec ${jname} ifconfig ${epair0}a inet6 accept_rtadv
+
+ # Make sure that NAs from us are flagged as coming from a router.
+ atf_check -o ignore sysctl net.inet6.ip6.forwarding=1
+
+ lladdr="$(ndp_if_lladdr ${epair0}b)"
+ lladdr="${lladdr%?}a"
+ # Send an RA with high preference.
+ atf_check -e ignore python3 $(atf_get_srcdir)/ra.py \
+ --sendif ${epair0}b \
+ --dst $(ndp_if_lladdr ${epair0}a ${jname}) \
+ --src ${lladdr} \
+ --rtrpref 1 --prefix ${prefix} \
+ --validlifetime 10 --preferredlifetime 5
+
+ lladdr="${lladdr%?}b"
+ # Send an RA with medium preference.
+ atf_check -e ignore python3 $(atf_get_srcdir)/ra.py \
+ --sendif ${epair0}b \
+ --dst $(ndp_if_lladdr ${epair0}a ${jname}) \
+ --src ${lladdr} \
+ --rtrpref 0 --prefix ${prefix} \
+ --validlifetime 10 --preferredlifetime 5
+
+ lladdr="${lladdr%?}c"
+ # Send an RA with low preference.
+ atf_check -e ignore python3 $(atf_get_srcdir)/ra.py \
+ --sendif ${epair0}b \
+ --dst $(ndp_if_lladdr ${epair0}a ${jname}) \
+ --src ${lladdr} \
+ --rtrpref 3 --prefix ${prefix} \
+ --validlifetime 10 --preferredlifetime 5
+
+ # Wait for a default router to appear.
+ while [ "$(jexec ${jname} ndp -r | wc -l)" -ne 3 ]; do
+ sleep 0.01
+ done
+ atf_check -s exit:0 \
+ -o match:"^${lladdr%?}a%${epair0}a if=${epair0}a, flags=, pref=high,.*" \
+ -o match:"^${lladdr%?}b%${epair0}a if=${epair0}a, flags=, pref=medium,.*" \
+ -o match:"^${lladdr%?}c%${epair0}a if=${epair0}a, flags=, pref=low,.*" \
+ jexec ${jname} ndp -r
+
+ # Make sure a default route is being installed
+ # XXX: for now, does not matter which router
+ atf_check -o match:"^default[[:space:]]+${lladdr%?}" \
+ jexec ${jname} netstat -rn6
+
+ # Make sure ndp knows about prefix advertising routers.
+ advrtrs=$(get_prefix_attr ${prefix}/64 "advertising-routers" "${jname}" | \
+ jq -r '. | length')
+ if [ "${advrtrs}" -ne 3 ]; then
+ atf_fail "Unexpected number of advertising routers: ${advrtrs}"
+ fi
+}
+
+ndp_muiltirouter_pref_cleanup() {
+ vnet_cleanup
+}
+
+atf_test_case "ndp_slaac_twohour_rule" "cleanup"
+ndp_slaac_twohour_rule_head() {
+ atf_set descr 'Test RFC 4862 section 5.5.3 (e): Two hour rule'
+ atf_set require.user root
+ atf_set require.progs jq python3 scapy
+}
+
+ndp_slaac_twohour_rule_body() {
+ local epair0 jname prefix ex1 ex2
+
+ vnet_init
+
+ jname="v6t-ndp_slaac_twohour_rule"
+ prefix="2001:db8:ffff:1000::"
+
+ epair0=$(vnet_mkepair)
+
+ vnet_mkjail ${jname} ${epair0}a
+
+ ndp_if_up ${epair0}a ${jname}
+ ndp_if_up ${epair0}b
+ atf_check jexec ${jname} ifconfig ${epair0}a inet6 accept_rtadv
+
+ # Make sure that NAs from us are flagged as coming from a router.
+ atf_check -o ignore sysctl net.inet6.ip6.forwarding=1
+
+ # Send an RA with 1 hour lifetime
+ atf_check -e ignore python3 $(atf_get_srcdir)/ra.py \
+ --sendif ${epair0}b \
+ --dst $(ndp_if_lladdr ${epair0}a ${jname}) \
+ --src $(ndp_if_lladdr ${epair0}b) \
+ --prefix ${prefix} --prefixlen 64 \
+ --validlifetime 3600 --preferredlifetime 3600
+
+ # Wait for a default router to appear.
+ while [ -z "$(jexec ${jname} ndp -r)" ]; do
+ sleep 0.01
+ done
+ ex1=$(prefix_expiry ${prefix}/64 "${jname}")
+
+ # Set the address lifetime to 2 hours and verify that the prefix is updated.
+ atf_check -e ignore python3 $(atf_get_srcdir)/ra.py \
+ --sendif ${epair0}b \
+ --dst $(ndp_if_lladdr ${epair0}a ${jname}) \
+ --src $(ndp_if_lladdr ${epair0}b) \
+ --prefix ${prefix} --prefixlen 64 \
+ --validlifetime 7200 --preferredlifetime 7200
+
+ # Verify that ndp sets the correct value from RA.
+ ex2=$(prefix_expiry ${prefix}/64 "${jname}")
+ if [ "${ex2}" -le "${ex1}" ]; then
+ atf_fail "Unexpected expiry time: ${ex2} <= ${ex1}"
+ fi
+ # Verify that address also updated the valid lifetime.
+ ex2=$(ifconfig -j "${jname}" ${epair0}a inet6 | grep vltime | awk '{print $NF}' )
+ if [ "${ex2}" -le 3600 ]; then
+ atf_fail "Unexpected expiry time: ${ex2} <= ${ex1}"
+ fi
+
+ # Set the address lifetime to 1 Hour and verify that
+ # the address of prefix is NOT updated to 1 hour.
+ atf_check -e ignore python3 $(atf_get_srcdir)/ra.py \
+ --sendif ${epair0}b \
+ --dst $(ndp_if_lladdr ${epair0}a ${jname}) \
+ --src $(ndp_if_lladdr ${epair0}b) \
+ --prefix ${prefix} --prefixlen 64 \
+ --validlifetime 3600 --preferredlifetime 3600
+
+ # Verify that ndp sets the received value from RA.
+ ex2=$(prefix_expiry ${prefix}/64 "${jname}")
+ if [ "${ex2}" -gt 3600 ]; then
+ atf_fail "Unexpected ndp expiry time: ${ex2} > 3600"
+ fi
+ # Verify that address NOT updated the valid lifetime.
+ ex2=$(ifconfig -j "${jname}" ${epair0}a inet6 | grep vltime | awk '{print $NF}' )
+ if [ "${ex2}" -le 3600 ]; then
+ atf_fail "Unexpected expiry time: ${ex2} <= 3600"
+ fi
+}
+
+ndp_slaac_twohour_rule_cleanup() {
+ vnet_cleanup
+}
+
+get_iface_prefix_flags() {
+ local prefix=$1
+ local iface=$2
+ local jail=""
+
+ if [ -n "$3" ]; then
+ jail="jexec $3"
+ fi
+
+ ${jail} ndp -p --libxo json | \
+ jq -r '.ndp.["prefix-list"][] |
+ select((.prefix == "'${prefix}'") and .interface == "'${iface}'") |
+ .flags'
+}
+
+atf_test_case "ndp_slaac_switch_onlink_prefix" "cleanup"
+ndp_slaac_switch_onlink_prefix_head() {
+ atf_set descr 'Test SLAAC onlink prefix switching when prefix received via multiple interfaces'
+ atf_set require.user root
+}
+
+ndp_slaac_switch_onlink_prefix_body() {
+ local epair0 epair1 jname prefix lladdr1 lladdr2 f1 f2
+
+ vnet_init
+
+ jname="v6t-ndp_slaac_switch_onlink_prefix"
+ prefix="2001:db8:ffff:1000::"
+
+ epair0=$(vnet_mkepair)
+ epair1=$(vnet_mkepair)
+
+ vnet_mkjail ${jname} ${epair0}a
+ atf_check ifconfig ${epair1}a vnet ${jname}
+
+ ndp_if_up ${epair0}a ${jname}
+ ndp_if_up ${epair1}a ${jname}
+ ndp_if_up ${epair0}b
+ ndp_if_up ${epair1}b
+
+ atf_check ifconfig -j ${jname} ${epair0}a inet6 accept_rtadv
+ atf_check ifconfig -j ${jname} ${epair1}a inet6 accept_rtadv
+ lladdr0=$(ndp_if_lladdr ${epair0}b)
+ lladdr1=$(ndp_if_lladdr ${epair1}b)
+
+ # Send an RA with high pref from epair0
+ atf_check -e ignore python3 $(atf_get_srcdir)/ra.py \
+ --sendif ${epair0}b \
+ --dst $(ndp_if_lladdr ${epair0}a ${jname}) \
+ --src ${lladdr0} \
+ --rtrpref 1 --prefix ${prefix} \
+ --validlifetime 10 --preferredlifetime 5
+
+ # Send an RA with medium pref from epair1
+ atf_check -e ignore python3 $(atf_get_srcdir)/ra.py \
+ --sendif ${epair1}b \
+ --dst $(ndp_if_lladdr ${epair1}a ${jname}) \
+ --src ${lladdr1} \
+ --rtrpref 0 --prefix ${prefix} \
+ --validlifetime 10 --preferredlifetime 5
+
+ # Wait for a default router to appear.
+ while [ -z "$(jexec ${jname} ndp -r)" ]; do
+ sleep 0.01
+ done
+
+ # Verify that we have a default route to epair0a
+ atf_check -o match:"^default[[:space:]]+${lladdr0}" \
+ jexec ${jname} netstat -rn6
+
+ # Verify that epair0a is_onlink and epair1a is_detached
+ f1=$(get_iface_prefix_flags "${prefix}/64" "${epair0}a" "${jname}")
+ f2=$(get_iface_prefix_flags "${prefix}/64" "${epair1}a" "${jname}")
+ if [ "${f1}" != "LAO" ]; then
+ atf_fail "Unexpected prefix flags on epair0a: ${f1}"
+ fi
+ if [ "${f2}" != "LAD" ]; then
+ atf_fail "Unexpected prefix flags on epair1a: ${f2}"
+ fi
+
+ # Send an RA to withdraw prefix from epair0
+ atf_check -e ignore python3 $(atf_get_srcdir)/ra.py \
+ --sendif ${epair0}b \
+ --dst $(ndp_if_lladdr ${epair0}a ${jname}) \
+ --src ${lladdr0} \
+ --rtrpref 1 --rtrltime 0 --prefix ${prefix} \
+ --validlifetime 0 --preferredlifetime 0
+
+ # Verify that epair1a is_onlink and epair0a is not
+ while [ "$(get_iface_prefix_flags ${prefix}/64 ${epair0}a ${jname})" == "LAO" ];
+ do
+ sleep 0.1
+ done
+ f2=$(get_iface_prefix_flags "${prefix}/64" "${epair1}a" "${jname}")
+ if [ "${f2}" != "LAO" ]; then
+ atf_fail "Unexpected prefix flags on epair1a: ${f2}"
+ fi
+
+ # Verify that we have a default route to epair1a
+ atf_check -o match:"^default[[:space:]]+${lladdr1}" \
+ jexec ${jname} netstat -rn6
+}
+
+ndp_slaac_switch_onlink_prefix_cleanup() {
+ vnet_cleanup
+}
+
+
atf_init_test_cases()
{
atf_add_test_case "ndp_add_gu_success"
atf_add_test_case "ndp_del_gu_success"
atf_add_test_case "ndp_slaac_default_route"
+ atf_add_test_case "ndp_slaac_twohour_rule"
+ atf_add_test_case "ndp_slaac_switch_onlink_prefix"
atf_add_test_case "ndp_prefix_len_mismatch"
atf_add_test_case "ndp_prefix_lifetime"
atf_add_test_case "ndp_prefix_lifetime_extend"
atf_add_test_case "ndp_grand_linklayer_event"
+ atf_add_test_case "ndp_input_validation_hlim"
+ atf_add_test_case "ndp_input_validation_src_linklocal"
+ atf_add_test_case "ndp_multirouter_pref"
}
diff --git a/tests/sys/netinet6/ra.py b/tests/sys/netinet6/ra.py
index 1b08c3e53c05..f71ab4b7499e 100644
--- a/tests/sys/netinet6/ra.py
+++ b/tests/sys/netinet6/ra.py
@@ -21,9 +21,19 @@ def main():
help='The source IP address')
parser.add_argument('--dst', nargs=1, required=True,
help='The destination IP address')
- parser.add_argument('--prefix', nargs=1, required=True,
+ parser.add_argument('--hoplimit', nargs=1, required=False,
+ type=int, default=255,
+ help='The hop limit of IPv6 packet')
+ parser.add_argument('--rtrpref', nargs=1, required=False,
+ type=int, default=1,
+ help='The router preference advertised')
+ parser.add_argument('--rtrltime', nargs=1, required=False,
+ type=int, default=1800,
+ help='The router preference advertised')
+ parser.add_argument('--prefix', nargs=1, required=False,
help='The prefix to be advertised')
- parser.add_argument('--prefixlen', nargs=1, required=True, type=int,
+ parser.add_argument('--prefixlen', nargs=1, required=False,
+ type=int, default=64,
help='The prefix length to be advertised')
parser.add_argument('--validlifetime', nargs=1, required=False,
type=int, default=4294967295,
@@ -34,8 +44,11 @@ def main():
args = parser.parse_args()
pkt = sp.Ether() / \
- sp.IPv6(src=args.src, dst=args.dst) / \
- sp.ICMPv6ND_RA(chlim=64) / \
+ sp.IPv6(src=args.src, dst=args.dst, hlim=args.hoplimit) / \
+ sp.ICMPv6ND_RA(chlim=64, prf=args.rtrpref, routerlifetime=args.rtrltime)
+
+ if (args.prefix):
+ pkt = pkt / \
sp.ICMPv6NDOptPrefixInfo(prefix=args.prefix,
prefixlen=args.prefixlen,
validlifetime=args.validlifetime,