Policy scripts: the operator's own rules for routes, in Rhai
A new crate, bgp-policy, runs functions of a script on routes: on their
way in, after the general checks of a peer's import and before the
checks that can change (RPKI, next hop reachability); and on their way
out to a peer. A function is given the route and the peer, may refuse
the route, and may change its communities, large communities,
LOCAL_PREF and MED, and on the way out prepend our AS:
fn from_peer(route, peer) {
if route.has_community("65535:666") { return false; }
route.add_community(LEARNED_FROM[peer.role]);
route.local_pref = PREFERENCE[peer.role];
true
}
Configuration: policy { path | source, import_function, export_function }
and, for a peer, import { function } and export { function }. The script
is compiled at startup; `bgp --check` reads it and says which function
is whose. A function that fails on a route has refused it. Scripts have
no files, network or clock, and are stopped after 200,000 steps.
Route selection now goes by a LOCAL_PREF on a route from an external
peer: the decoder never lets a peer's own through, so one that is there
was set by policy.
What it costs was measured before it was built on (benches/policy.rs):
a call that tests a few communities and changes two takes 1 to 3
microseconds, some two hundred times what Rust would. So a function is
asked once for the routes of an UPDATE, which share their attributes,
unless it asks for the prefix. One peer sending 1.23 M routes over a
session: 0.37 s without a function, 0.80 s with one that marks and
ranks every route, 3.4 s if it also looks at the prefix.
Export applies to what is announced, which today is what the daemon
originates; learned routes are not passed on yet.
Tested by unit tests of the crate, sessions end to end, a fuzz target on
the example script (testdata/policy.rhai), and a lab phase in which a
script changes which of FRR's and BIRD's route is chosen and what each
of them is told.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
f1d80c66df
commit
cbf0df1eef
52 files changed
+3467
-43
No files matched your search
+77
-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 hijacks nexthops v6only linklocal"
|
||||
all_phases="basics roles rpki refresh flaps restart scale limits hijacks nexthops policy v6only linklocal"
|
||||
phases="${PHASES:-$all_phases}"
|
||||
scale_routes="${SCALE_ROUTES:-200000}"
|
||||
|
||||
@@ -130,8 +130,10 @@ END
|
||||
# --- the daemon under test ------------------------------------------------------
|
||||
|
||||
# dut_config [frr-role] [frr-max-prefixes] [more of frr4's import policy]
|
||||
# [more of frr4] [more of bird4] [more of the configuration]
|
||||
dut_config() {
|
||||
local frr_role=${1:-PEER} frr_limit=${2:-2000000} frr_import=${3:-}
|
||||
local frr_more=${4:-} bird_more=${5:-} more=${6:-}
|
||||
cat > "$work/dut.textpb" <<END
|
||||
router_id: "10.9.0.1"
|
||||
local_asn: 64601
|
||||
@@ -143,6 +145,7 @@ peers {
|
||||
address_families: IPV4_UNICAST
|
||||
role: $frr_role
|
||||
import { accept_bogons: true max_prefixes: $frr_limit hold_down_seconds: 5 $frr_import }
|
||||
$frr_more
|
||||
}
|
||||
peers {
|
||||
name: "frr6" endpoint { address: "2001:db8:9::2" } remote_asn: 64602
|
||||
@@ -156,6 +159,7 @@ peers {
|
||||
role: PROVIDER require_role: true
|
||||
add_path: ADD_PATH_RECEIVE
|
||||
import { accept_bogons: true max_prefixes: 2000000 }
|
||||
$bird_more
|
||||
}
|
||||
peers {
|
||||
name: "bird6" endpoint { address: "2001:db8:9::3" } remote_asn: 4200000003
|
||||
@@ -168,6 +172,7 @@ originate { prefix: "2001:db8:1::/48" large_communities: "64601:1:2" }
|
||||
kernel { stale_seconds: 3 }
|
||||
rpki { caches { address: "127.0.0.1" port: 3323 } retry_seconds: 1 }
|
||||
http_endpoint { address: "127.0.0.1" port: 8080 }
|
||||
$more
|
||||
END
|
||||
}
|
||||
|
||||
@@ -823,6 +828,77 @@ phase_nexthops() {
|
||||
ok "back to normal"
|
||||
}
|
||||
|
||||
# The operator's own rules, as a script: routes are marked with where
|
||||
# they were learned and ranked by it, which changes which of two routes
|
||||
# is chosen; and what each neighbor is told is decided for each.
|
||||
phase_policy() {
|
||||
cat > "$work/policy.rhai" <<'END'
|
||||
const LEARNED_FROM = #{ peer: "64601:2000", provider: "64601:3000" };
|
||||
|
||||
// Every route on its way in. Here the provider's routes are the ones to
|
||||
// prefer, which is the other way about from what selection would do.
|
||||
fn incoming(route, peer) {
|
||||
route.remove_community("64601:*");
|
||||
route.add_community(LEARNED_FROM[peer.role]);
|
||||
route.local_pref = if peer.role == "provider" { 300 } else { 100 };
|
||||
true
|
||||
}
|
||||
|
||||
fn to_frr(route, peer) {
|
||||
route.add_community("64601:777");
|
||||
route.prepend(2);
|
||||
true
|
||||
}
|
||||
|
||||
// BIRD is not told our IPv4 prefix.
|
||||
fn to_bird(route, peer) { !route.within("10.1.0.0/24") }
|
||||
END
|
||||
dut_down TERM
|
||||
dut_config PEER 2000000 "" \
|
||||
'export { function: "to_frr" }' 'export { function: "to_bird" }' \
|
||||
'policy { path: "policy.rhai" import_function: "incoming" }'
|
||||
"$bgp" --check "$work/dut.textpb" | grep -q "frr4: import incoming, export to_frr" \
|
||||
|| fail "--check does not say which functions are whose: $("$bgp" --check "$work/dut.textpb" 2>&1)"
|
||||
dut_up
|
||||
# A prefix nobody has said anything about in the RPKI, from both; of
|
||||
# the two, FRR's would be chosen.
|
||||
echo "route 10.77.0.0/24 blackhole;" > "$work/bird-both.conf"
|
||||
bird_config "$work/bird-both.conf"
|
||||
bird_ctl configure >/dev/null
|
||||
frr_configure "router bgp 64602" "address-family ipv4 unicast" "network 10.77.0.0/24"
|
||||
wait_for "four sessions" established 4
|
||||
wait_for "FRR's route for it" dut_has 10.77.0.0/24 frr4
|
||||
wait_for "BIRD's route for it" dut_has 10.77.0.0/24 bird4
|
||||
|
||||
# In: marked by where they were learned, and ranked.
|
||||
local marks
|
||||
marks=$(api "/api/routes/received?prefix=10.77.0.0/24" | jq -c '[.routes[] | {peer: .peer_name, marks: (tostring | [scan("64601:[0-9]+")] | unique)}] | sort_by(.peer)')
|
||||
[ "$marks" = '[{"peer":"bird4","marks":["64601:3000"]},{"peer":"frr4","marks":["64601:2000"]}]' ] \
|
||||
|| fail "the routes are marked $marks"
|
||||
wait_for "BIRD's route to be the one chosen" 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 '.import_function == "incoming" and .export_function == "to_frr"' >/dev/null \
|
||||
|| fail "the status does not say which functions are the peer's"
|
||||
ok "routes are marked with where they were learned, and the provider's is chosen and installed"
|
||||
|
||||
# Out: FRR is told our route with a longer path and a mark; BIRD is not
|
||||
# told it at all, and is told the IPv6 one.
|
||||
wait_for "our route at FRR" bash -c "sudo ip netns exec lab-frr vtysh -N $pathspace -c 'show bgp ipv4 unicast 10.1.0.0/24 json' 2>/dev/null | jq -e '.paths[0] | .aspath.string == \"64601 64601 64601\" and (.community.string | contains(\"64601:777\")) and (.community.string | contains(\"64601:100\"))'"
|
||||
wait_for "our IPv6 route at BIRD" bird_has 2001:db8:1::/48
|
||||
sleep 1
|
||||
bird_has 10.1.0.0/24 && fail "BIRD was told the route its function refuses"
|
||||
ok "FRR is told our route with a longer path and a mark; BIRD is not told it"
|
||||
|
||||
frr_configure "router bgp 64602" "address-family ipv4 unicast" "no network 10.77.0.0/24"
|
||||
bird_config
|
||||
bird_ctl configure >/dev/null
|
||||
dut_down TERM
|
||||
dut_config
|
||||
dut_up
|
||||
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