Files
ferrix/scripts/two-routers.sh
T
Rayhaan JaufeerallyandClaude Fable 5.1 1de75fc610 Install a full table in the kernel in about a second
A million IPv4 routes took 1.6 s to install (release build); they take
1.1 s now, and 0.5 s to remove instead of 1.15 s. Most of what is left is
the kernel's own work on each route.

- Nexthop objects (Linux 5.3 and later): a gateway is put in the kernel
  once and routes name it, so the kernel checks it once and not for every
  route. The daemon makes its own objects, uses nobody else's, is told
  when one of its own is removed and puts it back, and removes them when
  it stops, which takes all the routes that name them along at once.
  `without_nexthop_objects` turns this off; a kernel without them is
  noticed and served as before.
- Writing to the kernel is a task of its own, fed through a short queue,
  so the next batch is worked out while the kernel is busy with the last.
  "In step" is said when the kernel has done the batch that completes the
  table, not when it was handed over.
- What a next hop resolves to is kept, by next hop: it was looked up among
  the machine's addresses for every route.
- A quicker hash for the plan's maps, and no lookups in the ones that are
  empty.

Fixes a hole found on the way: when an interface went down and came back
within the second it takes us to react, the kernel had dropped our routes
and we did not put them back, since nothing about them had changed for
us. After such a change the kernel is now asked what it actually has.

scripts/in-namespace.sh runs one command in a scratch namespace;
FERRIX_ROUTES sets the size of the timing test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 17:13:44 +02:00

145 lines
5.2 KiB
Bash
Executable File

#!/bin/sh
# Two daemons that route for each other, on a real kernel: the proof that
# what BGP learns ends up as forwarding.
#
# namespace a namespace b
# lan 10.1.0.1/24 lan 10.2.0.1/24
# 2001:db8:a::1/48 2001:db8:b::1/48
# a0 10.0.0.1/24 ------ veth ------ b0 10.0.0.2/24
# 2001:db8::1/64 2001:db8::2/64
#
# Each originates its own "lan" prefixes and installs what it learns in
# its kernel. The test passes when each kernel has the other's prefixes as
# BGP routes and a ping from one lan address to the other gets through,
# over IPv4 and IPv6; and when the routes go again as a daemon is stopped.
#
# Run it on Linux as a user who can sudo, after `cargo build -p bgp`;
# scripts/kernel-tests.sh does both.
set -eu
cd "$(dirname "$0")/.."
bgp="${CARGO_TARGET_DIR:-target}/debug/bgp"
[ -x "$bgp" ] || { echo "$bgp not found: cargo build -p bgp" >&2; exit 1; }
work="$(mktemp -d)"
a="ferrix-a-$$"
b="ferrix-b-$$"
pids=""
cleanup() {
status=$?
for pid in $pids; do sudo kill "$pid" 2>/dev/null || true; done
sudo ip netns del "$a" 2>/dev/null || true
sudo ip netns del "$b" 2>/dev/null || true
if [ "$status" -ne 0 ]; then
echo "--- FAILED; the daemons said:" >&2
tail -n 30 "$work"/*.log >&2 || true
fi
rm -rf "$work"
exit "$status"
}
trap cleanup EXIT INT TERM
in_a() { sudo ip netns exec "$a" "$@"; }
in_b() { sudo ip netns exec "$b" "$@"; }
sudo modprobe dummy 2>/dev/null || true
sudo ip netns add "$a"
sudo ip netns add "$b"
sudo ip link add a0 netns "$a" type veth peer name b0 netns "$b"
for side in a b; do
case $side in a) n=1; run=in_a ;; b) n=2; run=in_b ;; esac
$run ip link set lo up
$run ip addr add "10.0.0.$n/24" dev "${side}0"
$run ip -6 addr add "2001:db8::$n/64" dev "${side}0" nodad
$run ip link set "${side}0" up
$run ip link add lan type dummy
$run ip addr add "10.$n.0.1/24" dev lan
$run ip -6 addr add "2001:db8:$side::1/48" dev lan nodad
$run ip link set lan up
done
# One session for each IP version: routes are sent with the session's own
# address as next hop, which has to be of the routes' version.
config() {
side=$1; n=$2; other=$3
cat > "$work/$side.textpb" <<END
router_id: "10.0.0.$n"
local_asn: 6500$n
listen_endpoints { address: "10.0.0.$n" port: 179 }
listen_endpoints { address: "2001:db8::$n" port: 179 }
peers {
name: "v4" endpoint { address: "10.0.0.$other" } remote_asn: 6500$other
address_families: IPV4_UNICAST
import { accept_bogons: true max_prefixes: 100 }
}
peers {
name: "v6" endpoint { address: "2001:db8::$other" } remote_asn: 6500$other
address_families: IPV6_UNICAST
import { accept_bogons: true max_prefixes: 100 }
}
connect_retry_seconds: 1
originate { prefix: "10.$n.0.0/24" }
originate { prefix: "2001:db8:$side::/48" }
kernel {}
END
}
config a 1 2
config b 2 1
# As the user, with what the daemon needs and no more: CAP_NET_ADMIN for
# the routes, CAP_NET_BIND_SERVICE for port 179.
start() {
namespace=$1; side=$2
sudo ip netns exec "$namespace" \
setpriv --reuid "$(id -u)" --regid "$(id -g)" --init-groups \
--inh-caps +net_admin,+net_bind_service --ambient-caps +net_admin,+net_bind_service \
"$bgp" "$work/$side.textpb" > "$work/$side.log" 2>&1 &
pids="$pids $!"
}
start "$a" a
pid_a=$!
start "$b" b
# Waits until `ip route` in a namespace shows, or no longer shows, a route.
expect() {
run=$1; family=$2; want=$3; route=$4
for _ in $(seq 100); do
if $run ip "$family" route show proto bgp | sed 's/ */ /g; s/ nhid [0-9]*//; s/ pref medium//; s/ *$//' | grep -qxF "$route"; then
[ "$want" = present ] && return 0
else
[ "$want" = absent ] && return 0
fi
sleep 0.2
done
echo "expected the route \"$route\" to be $want; the kernel's BGP routes are:" >&2
$run ip "$family" route show proto bgp >&2
return 1
}
expect in_a -4 present "10.2.0.0/24 via 10.0.0.2 dev a0 metric 20"
expect in_b -4 present "10.1.0.0/24 via 10.0.0.1 dev b0 metric 20"
expect in_a -6 present "2001:db8:b::/48 via 2001:db8::2 dev a0 metric 20"
expect in_b -6 present "2001:db8:a::/48 via 2001:db8::1 dev b0 metric 20"
echo "ok: each kernel has the other's prefixes as BGP routes"
# From one lan address to the other: there and back by the routes BGP put
# there.
in_a ping -q -c 2 -W 2 -I 10.1.0.1 10.2.0.1 > /dev/null
in_b ping -q -c 2 -W 2 -I 10.2.0.1 10.1.0.1 > /dev/null
in_a ping -q -c 2 -W 2 -I 2001:db8:a::1 2001:db8:b::1 > /dev/null
echo "ok: packets get through, over IPv4 and IPv6"
# One daemon is stopped in good order. It takes its routes out of its
# kernel; the other, told the session is over, does the same with what it
# learned.
sudo ip netns pids "$a" | xargs sudo kill -TERM
expect in_a -4 absent "10.2.0.0/24 via 10.0.0.2 dev a0 metric 20"
expect in_a -6 absent "2001:db8:b::/48 via 2001:db8::2 dev a0 metric 20"
expect in_b -4 absent "10.1.0.0/24 via 10.0.0.1 dev b0 metric 20"
expect in_b -6 absent "2001:db8:a::/48 via 2001:db8::1 dev b0 metric 20"
if in_b ping -q -c 1 -W 1 -I 10.2.0.1 10.1.0.1 > /dev/null 2>&1; then
echo "packets still get through with the routes gone" >&2
exit 1
fi
echo "ok: the routes go when a daemon stops"