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
This commit is contained in:
Pouria Mousavizadeh Tehrani
2026-04-17 23:55:18 +03:30
parent 72043945c3
commit c173f02045
2 changed files with 397 additions and 9 deletions
+380 -5
View File
@@ -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"
}
+17 -4
View File
@@ -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,