Do not choose a route whose next hop cannot be reached (RFC 4271 §9.1.2.1)

Where routes go into the kernel, a route the kernel could not be given
is no longer taken into the table: its next hop is no neighbor on any
interface, or its prefix is a network the machine is itself on. Such a
route used to be able to win selection and then leave its prefix without
a route in the kernel, though another peer had one that worked.

It is held aside the way an RPKI-invalid route is, and by the same
means: judged on arrival against the interfaces as the kernel last
described them, looked for in the table when they change, and taken
from what is held when its next hop can be reached. The fib publishes
the interfaces for this; the next hop is resolved once for an UPDATE
and not for each of its routes.

The lab has a phase, nexthops: FRR's better route with a next hop that
is nowhere is held and BIRD's is installed, and they change places as
an address comes and goes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-10-02 20:29:31 +02:00
1 parent 3d946c32ce
commit f1d80c66df
15 files changed
+680 -63

No files matched your search

+65 -13
View File
@@ -29,7 +29,7 @@ cd "$(dirname "$0")/../.."
bgp="${CARGO_TARGET_DIR:-target}/release/bgp"
work=/tmp/ferrix-lab
pathspace=ferrixlab
all_phases="basics roles rpki refresh flaps restart scale limits hijacks v6only linklocal"
all_phases="basics roles rpki refresh flaps restart scale limits hijacks nexthops v6only linklocal"
phases="${PHASES:-$all_phases}"
scale_routes="${SCALE_ROUTES:-200000}"
@@ -129,9 +129,9 @@ END
# --- the daemon under test ------------------------------------------------------
# dut_config [frr-role] [frr-max-prefixes]
# dut_config [frr-role] [frr-max-prefixes] [more of frr4's import policy]
dut_config() {
local frr_role=${1:-PEER} frr_limit=${2:-2000000}
local frr_role=${1:-PEER} frr_limit=${2:-2000000} frr_import=${3:-}
cat > "$work/dut.textpb" <<END
router_id: "10.9.0.1"
local_asn: 64601
@@ -142,7 +142,7 @@ peers {
name: "frr4" endpoint { address: "10.9.0.2" } remote_asn: 64602
address_families: IPV4_UNICAST
role: $frr_role
import { accept_bogons: true max_prefixes: $frr_limit hold_down_seconds: 5 }
import { accept_bogons: true max_prefixes: $frr_limit hold_down_seconds: 5 $frr_import }
}
peers {
name: "frr6" endpoint { address: "2001:db8:9::2" } remote_asn: 64602
@@ -751,22 +751,74 @@ phase_hijacks() {
dut_lacks 10.2.7.0/24 || fail "a route by way of a third party was taken"
ok "a route whose next hop is someone else is not taken"
# The LAN's own prefix. The lab takes bogons, so the route is taken
# into the table; it must not get into the kernel, where it would
# compete with the connected route.
wait_for "the LAN's prefix at the daemon" dut_has 10.9.0.0/24 frr4
sleep 1
# The LAN's own prefix. The lab takes bogons, so the policy lets it
# by; it is still not a route the kernel could be given, and so not
# one to take: it would compete with the connected route.
wait_for "the LAN's prefix to be held" refused_for "next hop unreachable"
dut_lacks 10.9.0.0/24 || fail "the LAN's own prefix was taken from FRR"
in_ns dut ip -4 route show proto bgp | grep -q "^10.9.0.0/24 " && fail "the LAN's own prefix was installed by way of a peer"
api /api/kernel | jq -e '.unresolved["inside a network of this machine"] >= 1' >/dev/null \
|| fail "the kernel's status does not say why: $(api /api/kernel | jq -c .unresolved)"
in_ns dut ping -q -c 1 -W 2 10.9.0.3 >/dev/null || fail "the LAN is not reachable any more"
ok "a route for the exchange LAN itself stays out of the kernel"
ok "a route for the exchange LAN itself is not taken, and stays out of the kernel"
frr_configure \
"router bgp 64602" "address-family ipv4 unicast" \
"no network 10.1.0.0/24" "no network 10.9.0.0/24" "no network 10.2.7.0/24" \
"no neighbor 10.9.0.1 route-map TO-DUT out"
wait_for "the LAN's prefix to be withdrawn" dut_lacks 10.9.0.0/24
converged
ok "back to normal"
}
# FRR and BIRD both have a route for one prefix, and FRR's is the better.
# But its next hop is nowhere the machine can reach: then BIRD's is the
# one chosen and installed (RFC 4271 §9.1.2.1), until the next hop is
# somewhere, and again when it no longer is.
best_is() { # best_is prefix peer-name
api "/api/routes/received?prefix=$1&best=true" | jq -e --arg peer "$2" '.routes | length == 1 and .[0].peer_name == $peer'
}
phase_nexthops() {
dut_down TERM
dut_config PEER 2000000 "accept_third_party_next_hop: true"
dut_up
converged
# A prefix nobody has said anything about in the RPKI, from both.
echo "route 10.77.0.0/24 blackhole;" > "$work/bird-both.conf"
bird_config "$work/bird-both.conf"
bird_ctl configure >/dev/null
wait_for "BIRD's route for it" dut_has 10.77.0.0/24 bird4
frr_configure \
"ip prefix-list FAR seq 5 permit 10.77.0.0/24" \
"route-map TO-DUT permit 10" "match ip address prefix-list FAR" "set ip next-hop 10.250.0.1" \
"route-map TO-DUT permit 20" \
"router bgp 64602" "address-family ipv4 unicast" \
"neighbor 10.9.0.1 route-map TO-DUT out" "network 10.77.0.0/24"
wait_for "FRR's route to be held" refused_for "next hop unreachable"
peer_is frr4 '.held_routes >= 1' >/dev/null || fail "the route is not held: $(peer_is frr4 .)"
best_is 10.77.0.0/24 bird4 >/dev/null || fail "BIRD's route is not the one chosen"
dut_kernel -4 "10.77.0.0/24 via 10.9.0.3 dev ix0 metric 20" || fail "BIRD's route is not in the kernel"
ok "a better route whose next hop is nowhere is held; the one that works is chosen and installed"
# The next hop becomes a neighbor.
in_ns dut ip addr add 10.250.0.9/24 dev lan
wait_for "FRR's route to be taken" best_is 10.77.0.0/24 frr4
wait_for "it in the kernel" dut_kernel -4 "10.77.0.0/24 via 10.250.0.1 dev lan metric 20"
ok "when its next hop can be reached the better route is taken, chosen and installed"
# And stops being one.
in_ns dut ip addr del 10.250.0.9/24 dev lan
wait_for "BIRD's route to be chosen again" best_is 10.77.0.0/24 bird4
wait_for "it in the kernel" dut_kernel -4 "10.77.0.0/24 via 10.9.0.3 dev ix0 metric 20"
peer_is frr4 '.held_routes >= 1' >/dev/null || fail "FRR's route is not held again"
ok "and when it no longer can, the other route is back"
frr_configure \
"router bgp 64602" "address-family ipv4 unicast" \
"no network 10.77.0.0/24" "no neighbor 10.9.0.1 route-map TO-DUT out"
bird_config
bird_ctl configure >/dev/null
dut_down TERM
dut_config
dut_up
converged
ok "back to normal"
}