selftests: netfilter: add nft_fib_nexthop test

Functional coverage of nft_fib6_eval()'s nexthop enumeration over
three route shapes:

  1) single external nexthop (nhid)
  2) external nexthop group (nhid -> group)
  3) old-style multipath (nexthop ... nexthop ...)

Each scenario places one nexthop on the input device (veth0). For
(2) and (3) the matching nexthop is the second member, so the walk
has to traverse beyond the primary nh. Two nft counters on prerouting
verify the data path: one increments only when fib reports veth0 as
the oif, the other counts "missing" results and must stay at zero.

  ./nft_fib_nexthop.sh
  PASS: single external nexthop (nhid -> veth0)
  PASS: nexthop group (dummy0 + veth0)
  PASS: old-style multipath (sibling on veth0)

Suggested-by: Florian Westphal <fw@strlen.de>
Signed-off-by: Jiayuan Chen <jiayuan.chen@linux.dev>
Signed-off-by: Florian Westphal <fw@strlen.de>
This commit is contained in:
Jiayuan Chen 2026-05-20 10:34:11 +08:00 committed by Florian Westphal
parent f81b0c2d28
commit a40aaaef2f
2 changed files with 153 additions and 0 deletions

View File

@ -26,6 +26,7 @@ TEST_PROGS := \
nft_concat_range.sh \
nft_conntrack_helper.sh \
nft_fib.sh \
nft_fib_nexthop.sh \
nft_flowtable.sh \
nft_interface_stress.sh \
nft_meta.sh \

View File

@ -0,0 +1,152 @@
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
# shellcheck disable=SC2154
#
# Exercise nft_fib6_eval()'s sibling/nh enumeration on three route shapes:
# 1) route via a single external nexthop (nhid)
# 2) route via an external nexthop group (nhid -> group, two members)
# 3) route via old-style multipath (nexthop ... nexthop ...)
#
# In each scenario the route's nexthop set contains veth0 (the iif of the
# test packet). nft_fib6_info_nh_uses_dev() must walk the set and report
# veth0 as a valid oif. For (2) and (3) the matching nexthop is the second
# member, so the walk has to traverse beyond the primary nh.
#
# After sending $PKTS ICMPv6 echo requests from ns1, check two counters on
# nsrouter:
# nf_ok -- `fib daddr . iif oif eq "veth0"` must equal $PKTS
# nf_bad -- `fib daddr . iif oif missing` must stay at 0
# Both rules also match on iif veth0 and ip6 daddr dead:dead::/64 so that
# kernel-generated ND/MLD/RA traffic cannot pollute the counters.
#
# Topology similar to nft_fib.sh, without ns2; two dummy interfaces on
# nsrouter host extra nh devices:
#
# dead:1::99 dead:1::1
# ns1 <----veth----> nsrouter --- dummy0 dead:2::1
# \-- dummy1 dead:9::1
source lib.sh
ret=0
PKTS=3
checktool "nft --version" "run test without nft"
checktool "ip -V" "run test without iproute2"
setup_ns nsrouter ns1
trap cleanup_all_ns EXIT
if ! ip link add veth0 netns "$nsrouter" type veth peer name eth0 netns "$ns1" \
> /dev/null 2>&1; then
echo "SKIP: No virtual ethernet pair device support in kernel"
exit $ksft_skip
fi
ip -net "$ns1" link set lo up
ip -net "$ns1" link set eth0 up
ip -net "$ns1" -6 addr add dead:1::99/64 dev eth0 nodad
ip -net "$ns1" -6 route add default via dead:1::1
ip -net "$nsrouter" link set lo up
ip -net "$nsrouter" link set veth0 up
ip -net "$nsrouter" -6 addr add dead:1::1/64 dev veth0 nodad
if ! ip -net "$nsrouter" link add dummy0 type dummy 2>/dev/null; then
echo "SKIP: dummy netdev not available"
exit $ksft_skip
fi
ip -net "$nsrouter" link set dummy0 up
ip -net "$nsrouter" -6 addr add dead:2::1/64 dev dummy0 nodad
ip -net "$nsrouter" link add dummy1 type dummy
ip -net "$nsrouter" link set dummy1 up
ip -net "$nsrouter" -6 addr add dead:9::1/64 dev dummy1 nodad
ip netns exec "$nsrouter" sysctl -q net.ipv6.conf.all.forwarding=1
load_fib_rule() {
# filter on iif + daddr so the counters only see our test packets
ip netns exec "$nsrouter" nft -f /dev/stdin <<EOF
flush ruleset
table ip6 t {
counter nf_ok { }
counter nf_bad { }
chain c {
type filter hook prerouting priority 0; policy accept;
iif "veth0" ip6 daddr dead:dead::/64 fib daddr . iif oif eq "veth0" counter name nf_ok
iif "veth0" ip6 daddr dead:dead::/64 fib daddr . iif oif missing counter name nf_bad
}
}
EOF
}
bad_counter() {
local counter=$1
local expect=$2
local tag=$3
echo "FAIL ($tag): counter $counter has unexpected value (expected \"$expect\")" 1>&2
ip netns exec "$nsrouter" nft list counter ip6 t "$counter" 1>&2
}
run_scenario() {
local what="$1"; shift
# counter output format is "packets PACKET_NUM bytes BYTES_NUM";
# we only care about the packet count
local expect_ok="packets $PKTS bytes"
local expect_bad="packets 0 bytes"
local lret=0
# reset route + nexthop state between scenarios
ip -net "$nsrouter" -6 route del dead:dead::/64 > /dev/null 2>&1 || true
ip -net "$nsrouter" nexthop flush > /dev/null 2>&1 || true
# run the scenario function passed by the caller
"$@" || echo "WARN ($what): scenario setup returned non-zero"
load_fib_rule || { echo "FAIL ($what): nft load"; ret=1; return; }
# ping a daddr inside dead:dead::/64 so fib has to walk the nh set
ip netns exec "$ns1" ping -6 -c "$PKTS" -i 0.1 -W 1 dead:dead::1 \
> /dev/null 2>&1 || true
# verify the packets went through the expected fib path
if ! ip netns exec "$nsrouter" nft list counter ip6 t nf_ok | grep -q "$expect_ok"; then
bad_counter nf_ok "$expect_ok" "$what"
lret=1
fi
if ! ip netns exec "$nsrouter" nft list counter ip6 t nf_bad | grep -q "$expect_bad"; then
bad_counter nf_bad "$expect_bad" "$what"
lret=1
fi
if [ $lret -eq 0 ]; then
echo "PASS: $what"
else
ret=1
fi
}
scenario_single_nh() {
ip -net "$nsrouter" nexthop add id 1 via dead:1::99 dev veth0
ip -net "$nsrouter" -6 route add dead:dead::/64 nhid 1
}
run_scenario "single external nexthop (nhid -> veth0)" scenario_single_nh
scenario_nh_group() {
ip -net "$nsrouter" nexthop add id 1 via dead:2::2 dev dummy0
ip -net "$nsrouter" nexthop add id 2 via dead:1::99 dev veth0
ip -net "$nsrouter" nexthop add id 100 group 1/2
ip -net "$nsrouter" -6 route add dead:dead::/64 nhid 100
}
run_scenario "nexthop group (dummy0 + veth0)" scenario_nh_group
scenario_old_multipath() {
ip -net "$nsrouter" -6 route add dead:dead::/64 \
nexthop via dead:2::2 dev dummy0 \
nexthop via dead:1::99 dev veth0
}
run_scenario "old-style multipath (sibling on veth0)" scenario_old_multipath
exit $ret