Pass learned routes on to peers, with marks and control communities

What was to be scripted is configuration, done natively:

- import { add_communities, add_large_communities, remove_communities,
  local_pref }: every route taken from a peer is marked (where it was
  learned, say), with nobody else's word for our marks taken, and given
  its preference, which selection goes by.
- export { add_communities, add_large_communities, remove_communities,
  as_path_prepend, learned_routes }, for a peer or for all that have none
  of their own.
- control_communities: the communities by which routes say which peers
  they are passed on to, as exchanges' route servers have them (0:P, L:P,
  0:L, and the large forms L:0:P, L:1:P, L:0:0), taken off as the route
  goes on.

And what the last of these is for: with export.learned_routes a peer is
told the best route of every prefix the others announced. The rules are
BGP's and cannot be turned off: not back where it came from, not to a
peer whose AS is on the path, not between two internal peers, not past
NO_EXPORT or NO_ADVERTISE; our AS on the path and ourselves as next hop
to another network, LOCAL_PREF to our own; to a route server's client
the path, MED and next hop as they came (RFC 7947). Which routes go where
follows from the roles of RFC 9234: a route that came down or across goes
on down only, so that with roles on every peer this makes us transit for
our customers and nothing else. A peer without a role is told everything,
and the daemon says so at startup.

Each session follows the table's best routes with a reader and keeps the
prefixes it has announced, so that it can withdraw them. Routes that go
out alike go in one UPDATE: changes are gathered for 50 ms first, and a
table that is given out anew is gathered whole (at most four sessions at
a time; it is tens of megabytes). For that to work the table's journal
had to give out together what was written together: every change now
carries a time on a table-wide clock, a reader's pass reads every shard
up to one time, no further than the changes that are complete in every
shard, and a resync takes its places in the journals as of one time.

One peer sending 1.23 M routes over a session, another told of them:
in our table after 0.47 s, all heard after 2.1 s, four routes an UPDATE.
The lab has a phase, transit: FRR as our customer and BIRD as our
provider, routes each way with the right paths and marks, one kept back
by a community, and pings between their networks through us.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-10-02 23:48:08 +02:00
1 parent 9367c38907
commit 1e2e8f2d86
34 files changed
+2938 -69

No files matched your search

+52 -3
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 nexthops v6only linklocal"
all_phases="basics roles rpki refresh flaps restart scale limits hijacks nexthops transit v6only linklocal"
phases="${PHASES:-$all_phases}"
scale_routes="${SCALE_ROUTES:-200000}"
@@ -768,7 +768,7 @@ phase_hijacks() {
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"
"no neighbor 10.9.0.1 route-map TO-DUT out" "no route-map TO-DUT"
converged
ok "back to normal"
}
@@ -818,7 +818,7 @@ phase_nexthops() {
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"
"no network 10.77.0.0/24" "no neighbor 10.9.0.1 route-map TO-DUT out" "no route-map TO-DUT"
bird_config
bird_ctl configure >/dev/null
dut_down TERM
@@ -828,6 +828,55 @@ phase_nexthops() {
ok "back to normal"
}
# Routes are passed on: FRR becomes our customer and BIRD stays our
# provider, so what FRR announces goes up to BIRD with our AS on the path,
# what BIRD announces goes down to FRR marked "only to customer", and
# packets go between their networks through us. A community on one of
# FRR's routes keeps it from BIRD.
phase_transit() {
dut_down TERM
dut_config CUSTOMER 2000000 "" "" "" \
'export { learned_routes: true }
control_communities: true'
in_ns dut sysctl -qw net.ipv4.ip_forward=1
dut_up
frr_configure "router bgp 64602" "neighbor 10.9.0.1 local-role customer" \
"ip prefix-list KEPT seq 5 permit 10.2.9.0/24" \
"route-map TO-DUT-TRANSIT permit 10" "match ip address prefix-list KEPT" "set large-community 64601:0:4200000003" \
"route-map TO-DUT-TRANSIT permit 20" \
"router bgp 64602" "address-family ipv4 unicast" \
"neighbor 10.9.0.1 route-map TO-DUT-TRANSIT out" "network 10.2.9.0/24"
wait_for "four sessions" established 4
wait_for "FRR's routes at the daemon" dut_has 10.2.9.0/24 frr4
# Up to the provider, with our AS on the path; not the one that says
# not to.
wait_for "FRR's route at BIRD, by way of us" bash -c "sudo birdc -s $work/bird.ctl show route for 10.2.0.0/24 all | grep -q 'BGP.as_path: 64601 64602'"
sleep 1
bird_ctl show route for 10.2.9.0/24 all | grep -q "64601 64602" && fail "BIRD was told the route whose community says not to"
ok "the customer's routes reach the provider with our AS on the path; one kept back by its community"
# Down to the customer, marked.
wait_for "BIRD's route at FRR, by way of us" bash -c "sudo ip netns exec lab-frr vtysh -N $pathspace -c 'show bgp ipv4 unicast 10.3.0.0/24 json' 2>/dev/null | jq -e '.paths[0].aspath.string == \"64601 4200000003\"'"
# Marked on the way in from the provider, with the provider's AS, and
# the mark kept as it goes down (RFC 9234 §5).
vty -c "show bgp ipv4 unicast 10.3.0.0/24" | grep -q "otc 4200000003" || fail "the route to the customer is not marked only to customer"
ok "the provider's routes reach the customer, marked only to customer"
# And packets follow them, through us.
in_ns frr ping -q -c 2 -W 2 -I 10.2.0.1 10.3.0.1 >/dev/null || fail "no ping from the customer's network to the provider's through us"
in_ns bird ping -q -c 2 -W 2 -I 10.3.0.1 10.2.0.1 >/dev/null || fail "no ping from the provider's network to the customer's through us"
ok "packets go between the customer's and the provider's networks through us"
frr_configure "router bgp 64602" "neighbor 10.9.0.1 local-role peer" \
"address-family ipv4 unicast" "no network 10.2.9.0/24" "no neighbor 10.9.0.1 route-map TO-DUT-TRANSIT out" "no route-map TO-DUT-TRANSIT"
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