export: advertise every route for a prefix with ADD-PATH (all_paths)

Before this change, a session on which ADD-PATH (RFC 7911) was
negotiated in the sending direction was sent path identifiers, but only
the best route for each prefix. A route collector such as bgp.tools
wants every route a router has accepted, from every neighbor, and from
one session: without this the collector saw one path per prefix, nearly
always the same neighbor's.

This change adds `all_paths` to the export policy. On a session with it,
and ADD-PATH send negotiated for a family, every accepted route for a
prefix of that family is advertised, each with a path identifier of its
own; for a family without ADD-PATH send the best route goes as before.
`all_paths` needs `learned_routes` (a configuration error otherwise) and
the peer's `add_path` to offer sending. The export policy, the role
rules, the well-known communities and the export limits apply to every
route alike, and the export limits count every route.

Implementation, in the advertiser:

* An exporter with `all_paths` follows the table's journal of all routes
  (`RibTable::reader_of_all_routes`, until now unused) instead of the
  best-route journal, and is given all the routes of a prefix whenever
  one of them changes (`News::Routes`).
* What the peer holds is `Announced`: the set of prefixes for a
  best-route session, as before; or for an all-paths session, for each
  prefix the routes announced, keyed by the peer they came from and that
  peer's own identifier, with the identifier they went out with and the
  form they went out in. A route keeps its identifier for as long as it
  is announced, goes out again only if it changed, and is withdrawn by
  its identifier; a new route gets the lowest identifier not in use for
  the prefix, from 1.
* Verdicts carry either kind (`Verdict::Best`, `Verdict::All`), and a
  reset of the table puts what was announced in doubt until heard of
  again, as before, with the same identifiers kept across the reset.
* The record for an all-paths session is about 50 bytes a route, some
  tens of megabytes for a full table from several neighbors; the
  best-route bookkeeping is unchanged in size.

Also, so that a collector session can be watched: the number of routes
advertised to each peer and not withdrawn since, every route counted
where several are sent for a prefix. The exporter keeps it in the
session's counters after every change; it is `advertised_routes` in
`/api/peers`, the "sent" column of the status page's neighbor table,
and the gauge `bgp_peer_advertised_routes{peer}`.

Documentation: a "Feeding a Route Collector" section in the guide to
passing routes on, with the configuration for a collector; `all_paths`
in the configuration reference; the ADD-PATH sections of the neighbor
guide and the standards reference; the limitation that was removed;
the new field, column and metric in the HTTP API, monitoring and
metrics references.

Testing: an integration test with two feeders and a collector that
negotiated ADD-PATH receive, which checks the two routes with their
identifiers, the withdrawal of one by its identifier, a changed route
keeping its identifier and a new route taking the free one; a
configuration test for `all_paths` without `learned_routes`; a new
`collector` phase of the interoperability lab in which FRR, as a route
collector, is told both of BIRD's routes for a prefix from BIRD's two
sessions and holds them apart, and loses one when its session is
disabled, with the advertised count checked at each step. The workspace tests, the kernel tests and every lab phase pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-10-09 01:16:48 +02:00
1 parent 03f4f58ecb
commit ec5c9be454
24 files changed
+660 -75

No files matched your search

+53 -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 graceful scale limits hijacks nexthops transit leaks v6only linklocal"
all_phases="basics roles rpki refresh flaps restart graceful scale limits hijacks nexthops transit collector leaks v6only linklocal"
phases="${PHASES:-$all_phases}"
scale_routes="${SCALE_ROUTES:-200000}"
@@ -319,9 +319,9 @@ frr_down() {
# --- BIRD -----------------------------------------------------------------------
# bird_config [extra-static-routes-file]
# bird_config [extra-static-routes-file] [more-configuration]
bird_config() {
local extra=${1:-}
local extra=${1:-} more=${2:-}
cat > "$work/bird.conf" <<END
log "$work/bird.log" all;
router id 10.9.0.3;
@@ -355,6 +355,7 @@ protocol bgp dut6 from dut {
source address 2001:db8:9::3;
ipv6 { import all; export where source = RTS_STATIC; };
}
$more
END
}
@@ -959,6 +960,55 @@ control_communities: true'
ok "back to normal"
}
# FRR is a route collector of ours, told every route with its path
# identifier (ADD-PATH). BIRD talks to us from a second address too, so
# that there are two routes for each of its prefixes; FRR is told both,
# and holds them apart. One of BIRD's sessions goes, and so does one of
# the two.
phase_collector() {
dut_down TERM
in_ns bird ip addr add 10.9.0.4/24 dev ix0
dut_config CUSTOMER 2000000 "" 'add_path: ADD_PATH_SEND
export { learned_routes: true all_paths: true }' "" 'peers {
name: "bird4b" endpoint { address: "10.9.0.4" } remote_asn: 4200000003
address_families: IPV4_UNICAST
role: PROVIDER
import { accept_bogons: true max_prefixes: 2000000 }
}'
bird_config "" 'protocol bgp dut4b from dut {
neighbor 10.9.0.1 as 64601;
source address 10.9.0.4;
ipv4 { import all; export where source = RTS_STATIC; };
}'
bird_ctl configure >/dev/null
frr_configure "router bgp 64602" "neighbor 10.9.0.1 local-role customer"
dut_up
wait_for "five sessions" established 5
wait_for "BIRD's route from both its sessions" bash -c "sudo ip netns exec lab-dut curl -sf 'http://127.0.0.1:8080/api/routes/received?prefix=10.3.0.0/24' | jq -e '.routes | length == 2'"
peer_is frr4 '.add_path_send | index("IPv4 unicast") != null' >/dev/null \
|| fail "ADD-PATH was not agreed with FRR: $(peer_is frr4 .add_path_send)"
wait_for "both routes at FRR, told apart" 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 | length == 2 and all(.aspath.string == \"64601 4200000003\")'"
# Its two prefixes twice, and our own route: what the page and the
# metrics say it holds from us.
wait_for "the count of routes the collector holds" peer_is frr4 '.advertised_routes == 5'
ok "the collector is told both of BIRD's routes for a prefix, with path identifiers"
bird_ctl disable dut4b >/dev/null
wait_for "one route left at FRR" 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 | length == 1'"
wait_for "the count of routes the collector holds" peer_is frr4 '.advertised_routes == 3'
ok "a route that goes is withdrawn from the collector by its identifier, the other stays"
frr_configure "router bgp 64602" "neighbor 10.9.0.1 local-role peer"
bird_config
bird_ctl configure >/dev/null
in_ns bird ip addr del 10.9.0.4/24 dev ix0
dut_down TERM
dut_config
dut_up
converged
ok "back to normal"
}
# A customer leaks, and the export limits hold. FRR, now our customer,
# announces a route outside its address space and then more routes than
# BIRD, our provider, is to be told of. A customer's routes go to