Harden import, the kernel task and the RTR client after review
Three independent reviews (hijack resistance, reliability, code health) of the whole tree. This is what they found that could let a bad route in or take the daemon down. Hijacks: - A route had two paths on a two-octet session, AS_PATH and AS4_PATH, read in two places by two rules. A peer could make the origin checked on import differ from the one checked at revalidation, and get an RPKI-invalid route into the table. AS4_PATH is now merged into AS_PATH once, on receipt (RFC 6793 §4.2.3), and nothing else knows of it. - The next hop is judged on import: it must be usable, agreed on (RFC 8950), and by default the peer itself. - Our own originated prefixes are not taken from other networks. - A route for a network the machine is on is never put in the kernel. - Paths with an AS_SET are refused (RFC 9774). - A configured peer gets one session, not one per BGP Identifier. - More IPv6 special-purpose prefixes are bogons. Reliability: - A batch of requests the kernel refused could overflow the netlink socket and end the daemon. The socket has room now, a lost answer or a silent kernel means asking what is there, and neither is fatal. - Working the routes out again within the grace period after a restart no longer drops the routes being kept. - That work can no longer be put off for ever by interface changes. - Peers whose sessions keep failing are slowed down. - Unused attribute sets are swept when they pile up, not only each minute. - Log lines a peer can cause are said once in a while. - An RTR answer is bounded in size and time; data cannot expire between two refreshes. - The shard of a key is taken from a stirred hash: neighbouring bits of the hash proved alike for prefixes in a row. The test that showed it, failing one run in sixteen, is deterministic now. The lab has a phase, hijacks, in which FRR tries the first three. route::apply is a struct with methods instead of nested closures, and the kernel task's loop is shorter. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
fd2248feb5
commit
9ca86ede2f
34 files changed
+1696
-470
No files matched your search
+46
-1
@@ -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 v6only linklocal"
|
||||
all_phases="basics roles rpki refresh flaps restart scale limits hijacks v6only linklocal"
|
||||
phases="${PHASES:-$all_phases}"
|
||||
scale_routes="${SCALE_ROUTES:-200000}"
|
||||
|
||||
@@ -726,6 +726,51 @@ phase_limits() {
|
||||
ok "back to normal"
|
||||
}
|
||||
|
||||
# FRR tries what a peer must not get away with: announcing our own
|
||||
# prefix, announcing the exchange LAN itself (which would take the
|
||||
# sessions with everyone on it), and sending our traffic to someone else.
|
||||
refused_for() { # refused_for reason: how many of frr4's routes were refused for it
|
||||
api "/api/routes/rejected?peer=frr4" | jq -e --arg why "$1" '.[0].by_reason[$why] // 0 | . >= 1'
|
||||
}
|
||||
phase_hijacks() {
|
||||
frr_configure \
|
||||
"ip prefix-list ELSEWHERE seq 5 permit 10.2.7.0/24" \
|
||||
"route-map TO-DUT permit 10" "match ip address prefix-list ELSEWHERE" "set ip next-hop 10.9.0.3" \
|
||||
"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.1.0.0/24" "network 10.9.0.0/24" "network 10.2.7.0/24"
|
||||
|
||||
# Our own prefix, from someone else.
|
||||
wait_for "our own prefix to be refused" refused_for "prefix is our own"
|
||||
dut_lacks 10.1.0.0/24 || fail "our own prefix was taken from FRR"
|
||||
ok "our own prefix is not taken from a peer"
|
||||
|
||||
# A route whose next hop is BIRD, from FRR.
|
||||
wait_for "the third-party next hop to be refused" refused_for "next hop is not the peer"
|
||||
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
|
||||
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"
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
# The exchange LAN loses its IPv4 addresses. One session over IPv6 with
|
||||
# each router carries both families, and IPv4 routes have IPv6 next hops
|
||||
# (RFC 8950), in BGP and in the kernel. Run after the rest: the lab stays
|
||||
|
||||
Reference in new issue
Block a user