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>
145 lines
5.2 KiB
Bash
Executable File
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"
|