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>
357 lines
12 KiB
Rust
357 lines
12 KiB
Rust
//! Passing routes on: what one peer announced reaches the others, as far
|
|
//! as the rules for that let it.
|
|
|
|
use std::collections::BTreeSet;
|
|
use std::net::Ipv4Addr;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use bgp_integration_tests::announcement::{announcement, withdrawal};
|
|
use bgp_integration_tests::test_peer::TestPeer;
|
|
use bgp_integration_tests::test_server::TestServer;
|
|
use bgp_integration_tests::{TIMEOUT, prefix};
|
|
use bgp_packet::message::{
|
|
CommunitiesPathAttribute, Message, NextHopPathAttribute, PathAttribute, RouteRefreshMessage,
|
|
UpdateMessage,
|
|
};
|
|
use bgp_primitives::constants::AfiSafi;
|
|
use bgp_rib::{LocalRoute, LocalRouteAttrs};
|
|
use bgp_server::marks::{Control, Marks};
|
|
use bgp_server::policy::ExportPolicy;
|
|
use eyre::Result;
|
|
|
|
const LOCAL_ASN: u32 = 64601;
|
|
|
|
/// A server that passes on to every peer what it learned from the others.
|
|
async fn transit(customize: impl FnOnce(&mut bgp_server::config::Config)) -> Result<TestServer> {
|
|
TestServer::start_with(LOCAL_ASN, |config| {
|
|
config.export = Arc::new(ExportPolicy {
|
|
learned_routes: true,
|
|
all_paths: false,
|
|
..ExportPolicy::default()
|
|
});
|
|
customize(config);
|
|
})
|
|
.await
|
|
}
|
|
|
|
/// The peer with AS `64700 + n`, its session up.
|
|
async fn peer(server: &TestServer, n: u8) -> Result<TestPeer> {
|
|
server
|
|
.established(64700 + u32::from(n), Ipv4Addr::new(10, 0, 0, n))
|
|
.await
|
|
}
|
|
|
|
async fn announce(
|
|
peer: &mut TestPeer,
|
|
prefixes: &[&str],
|
|
path: &[u32],
|
|
communities: &[(u16, u16)],
|
|
) -> Result<()> {
|
|
peer.announce(announcement(prefixes, path).with_communities(communities))
|
|
.await
|
|
}
|
|
|
|
async fn withdraw(peer: &mut TestPeer, prefixes: &[&str]) -> Result<()> {
|
|
peer.announce(withdrawal(prefixes.iter().map(|p| prefix(p).into())))
|
|
.await
|
|
}
|
|
|
|
/// What a peer has been told so far, as the prefixes it would now have
|
|
/// and the path each came with.
|
|
#[derive(Default, Debug)]
|
|
struct Told {
|
|
routes: std::collections::BTreeMap<String, Vec<u32>>,
|
|
updates: usize,
|
|
}
|
|
|
|
impl Told {
|
|
fn take(&mut self, update: &UpdateMessage) {
|
|
self.updates += 1;
|
|
for route in &update.withdrawn_routes {
|
|
self.routes.remove(&route.prefix.to_string());
|
|
}
|
|
let path: Vec<u32> = update
|
|
.path_attributes
|
|
.iter()
|
|
.find_map(|attribute| match attribute {
|
|
PathAttribute::AsPath(path) => Some(
|
|
path.segments
|
|
.iter()
|
|
.flat_map(|segment| segment.path.clone())
|
|
.collect(),
|
|
),
|
|
_ => None,
|
|
})
|
|
.unwrap_or_default();
|
|
for route in &update.announced_routes {
|
|
self.routes.insert(route.prefix.to_string(), path.clone());
|
|
}
|
|
}
|
|
|
|
fn prefixes(&self) -> BTreeSet<&str> {
|
|
self.routes.keys().map(String::as_str).collect()
|
|
}
|
|
}
|
|
|
|
/// Reads what the peer is sent until it has exactly `expected`.
|
|
async fn told_becomes(peer: &mut TestPeer, told: &mut Told, expected: &[&str]) -> Result<()> {
|
|
let expected: BTreeSet<&str> = expected.iter().copied().collect();
|
|
let reading = async {
|
|
while told.prefixes() != expected {
|
|
let update = peer.recv_update().await?;
|
|
told.take(&update);
|
|
}
|
|
Ok::<(), eyre::Report>(())
|
|
};
|
|
match tokio::time::timeout(TIMEOUT, reading).await {
|
|
Ok(read) => read,
|
|
Err(_) => Err(eyre::eyre!(
|
|
"the peer has {:?}; expected {expected:?}",
|
|
told.prefixes()
|
|
)),
|
|
}
|
|
}
|
|
|
|
/// Nothing more is sent to the peer for a while.
|
|
async fn told_nothing(peer: &mut TestPeer) {
|
|
let more = tokio::time::timeout(Duration::from_millis(300), peer.recv_update()).await;
|
|
assert!(more.is_err(), "the peer was sent {more:?}");
|
|
}
|
|
|
|
/// What one peer announces the others are told, with our AS on the path
|
|
/// and ourselves as next hop; and what it takes back, they are told too.
|
|
#[tokio::test]
|
|
async fn test_routes_from_one_peer_reach_the_others() -> Result<()> {
|
|
let server = transit(|_| {}).await?;
|
|
let mut one = peer(&server, 1).await?;
|
|
let mut two = peer(&server, 2).await?;
|
|
|
|
announce(
|
|
&mut one,
|
|
&["10.1.0.0/16", "10.2.0.0/16", "10.3.0.0/16"],
|
|
&[64701, 65000],
|
|
&[],
|
|
)
|
|
.await?;
|
|
let mut told = Told::default();
|
|
told_becomes(
|
|
&mut two,
|
|
&mut told,
|
|
&["10.1.0.0/16", "10.2.0.0/16", "10.3.0.0/16"],
|
|
)
|
|
.await?;
|
|
assert_eq!(told.routes["10.1.0.0/16"], [LOCAL_ASN, 64701, 65000]);
|
|
assert_eq!(
|
|
told.updates, 1,
|
|
"routes that go out alike go in one message"
|
|
);
|
|
// Not back to where they came from.
|
|
told_nothing(&mut one).await;
|
|
|
|
// The next hop is ourselves: the address of our end of the session.
|
|
announce(&mut one, &["10.4.0.0/16"], &[64701], &[]).await?;
|
|
let update = two.recv_update().await?;
|
|
assert!(
|
|
update
|
|
.path_attributes
|
|
.contains(&PathAttribute::NextHop(NextHopPathAttribute(
|
|
Ipv4Addr::LOCALHOST
|
|
))),
|
|
"{update:?}"
|
|
);
|
|
told.take(&update);
|
|
|
|
// One is taken back; then the peer goes, and its routes with it.
|
|
withdraw(&mut one, &["10.2.0.0/16"]).await?;
|
|
told_becomes(
|
|
&mut two,
|
|
&mut told,
|
|
&["10.1.0.0/16", "10.3.0.0/16", "10.4.0.0/16"],
|
|
)
|
|
.await?;
|
|
drop(one);
|
|
told_becomes(&mut two, &mut told, &[]).await?;
|
|
Ok(())
|
|
}
|
|
|
|
/// A peer is told of the best route, and of the next best when that one
|
|
/// goes; and a peer that comes later is told what there is.
|
|
#[tokio::test]
|
|
async fn test_a_peer_is_kept_at_the_best_route() -> Result<()> {
|
|
let server = transit(|_| {}).await?;
|
|
let mut near = peer(&server, 1).await?;
|
|
let mut far = peer(&server, 2).await?;
|
|
let mut watcher = peer(&server, 3).await?;
|
|
let mut told = Told::default();
|
|
|
|
announce(&mut far, &["10.1.0.0/16"], &[64702, 65000, 65001], &[]).await?;
|
|
told_becomes(&mut watcher, &mut told, &["10.1.0.0/16"]).await?;
|
|
assert_eq!(told.routes["10.1.0.0/16"], [LOCAL_ASN, 64702, 65000, 65001]);
|
|
|
|
// A better route for it comes: that is the one told now.
|
|
announce(&mut near, &["10.1.0.0/16"], &[64701, 65001], &[]).await?;
|
|
let update = watcher.recv_update().await?;
|
|
told.take(&update);
|
|
assert_eq!(told.routes["10.1.0.0/16"], [LOCAL_ASN, 64701, 65001]);
|
|
assert!(
|
|
update.withdrawn_routes.is_empty(),
|
|
"replaced, not withdrawn"
|
|
);
|
|
|
|
// And goes again: back to the other, with no gap.
|
|
withdraw(&mut near, &["10.1.0.0/16"]).await?;
|
|
let update = watcher.recv_update().await?;
|
|
told.take(&update);
|
|
assert_eq!(told.routes["10.1.0.0/16"], [LOCAL_ASN, 64702, 65000, 65001]);
|
|
|
|
// The other peer was told of the better route while it was the best,
|
|
// and then that it is gone: the one left is its own, which it is not
|
|
// told of.
|
|
let mut told_far = Told::default();
|
|
told_becomes(&mut far, &mut told_far, &["10.1.0.0/16"]).await?;
|
|
let gone = far.recv_update().await?;
|
|
assert_eq!(gone.withdrawn_routes, vec![prefix("10.1.0.0/16")]);
|
|
told_nothing(&mut far).await;
|
|
|
|
// A peer that comes now is told what there is; and again if it asks.
|
|
let mut late = peer(&server, 4).await?;
|
|
let mut late_told = Told::default();
|
|
told_becomes(&mut late, &mut late_told, &["10.1.0.0/16"]).await?;
|
|
late.send(&Message::RouteRefresh(RouteRefreshMessage::request(
|
|
AfiSafi::IPV4_UNICAST,
|
|
)))
|
|
.await?;
|
|
let again = late.recv_update().await?;
|
|
assert_eq!(again.announced_routes, vec![prefix("10.1.0.0/16")]);
|
|
Ok(())
|
|
}
|
|
|
|
/// A route is not sent to a peer whose AS is on its path already, nor
|
|
/// anywhere if it says NO_EXPORT; and what we originate goes to everyone
|
|
/// beside what was learned.
|
|
#[tokio::test]
|
|
async fn test_routes_stop_where_they_must() -> Result<()> {
|
|
let server = transit(|config| {
|
|
config.originate = vec![LocalRoute::new(
|
|
prefix("10.99.0.0/16"),
|
|
LocalRouteAttrs::default(),
|
|
)];
|
|
})
|
|
.await?;
|
|
let mut one = peer(&server, 1).await?;
|
|
let mut two = peer(&server, 2).await?;
|
|
let mut told = Told::default();
|
|
told_becomes(&mut two, &mut told, &["10.99.0.0/16"]).await?;
|
|
assert_eq!(told.routes["10.99.0.0/16"], [LOCAL_ASN]);
|
|
|
|
// Through the second peer's AS already; and not for other networks.
|
|
announce(&mut one, &["10.1.0.0/16"], &[64701, 64702, 65000], &[]).await?;
|
|
announce(&mut one, &["10.2.0.0/16"], &[64701], &[(0xFFFF, 0xFF01)]).await?;
|
|
announce(&mut one, &["10.3.0.0/16"], &[64701], &[]).await?;
|
|
told_becomes(&mut two, &mut told, &["10.99.0.0/16", "10.3.0.0/16"]).await?;
|
|
told_nothing(&mut two).await;
|
|
assert_eq!(server.rib.len(), 3, "all three were taken");
|
|
Ok(())
|
|
}
|
|
|
|
/// Communities on a route say which peers it is passed on to, and are
|
|
/// gone when it is; and every route to a peer gets the marks of `export`.
|
|
#[tokio::test]
|
|
async fn test_communities_say_who_is_told() -> Result<()> {
|
|
let server = transit(|config| {
|
|
config.control = Some(Control {
|
|
local_asn: LOCAL_ASN,
|
|
});
|
|
config.export = Arc::new(ExportPolicy {
|
|
learned_routes: true,
|
|
all_paths: false,
|
|
marks: Marks::parse(&[], &["64601:7".to_string()], &[]).unwrap(),
|
|
prepend: 1,
|
|
});
|
|
config.originate = vec![LocalRoute::new(
|
|
prefix("10.99.0.0/16"),
|
|
LocalRouteAttrs {
|
|
// Our own route, and not for the second peer.
|
|
communities: vec![(0, 64702)],
|
|
..LocalRouteAttrs::default()
|
|
},
|
|
)];
|
|
})
|
|
.await?;
|
|
let mut one = peer(&server, 1).await?;
|
|
let mut two = peer(&server, 2).await?;
|
|
let mut three = peer(&server, 3).await?;
|
|
let (mut told_two, mut told_three) = (Told::default(), Told::default());
|
|
|
|
// To everyone but the second; to the second only; to nobody; and
|
|
// with nothing said.
|
|
announce(
|
|
&mut one,
|
|
&["10.1.0.0/16"],
|
|
&[64701],
|
|
&[(0, 64702), (3356, 3)],
|
|
)
|
|
.await?;
|
|
announce(&mut one, &["10.2.0.0/16"], &[64701], &[(64601, 64702)]).await?;
|
|
announce(&mut one, &["10.3.0.0/16"], &[64701], &[(0, 64601)]).await?;
|
|
announce(&mut one, &["10.4.0.0/16"], &[64701], &[]).await?;
|
|
|
|
told_becomes(&mut two, &mut told_two, &["10.2.0.0/16", "10.4.0.0/16"]).await?;
|
|
told_becomes(
|
|
&mut three,
|
|
&mut told_three,
|
|
&["10.99.0.0/16", "10.1.0.0/16", "10.4.0.0/16"],
|
|
)
|
|
.await?;
|
|
told_nothing(&mut two).await;
|
|
told_nothing(&mut three).await;
|
|
// Our AS once for passing it on, and once more by `export`.
|
|
assert_eq!(
|
|
told_three.routes["10.1.0.0/16"],
|
|
[LOCAL_ASN, LOCAL_ASN, 64701]
|
|
);
|
|
assert_eq!(told_three.routes["10.99.0.0/16"], [LOCAL_ASN, LOCAL_ASN]);
|
|
|
|
// What was for us to act on is gone, and the router's mark is on.
|
|
announce(
|
|
&mut one,
|
|
&["10.5.0.0/16"],
|
|
&[64701],
|
|
&[(0, 64702), (3356, 3)],
|
|
)
|
|
.await?;
|
|
let update = three.recv_update().await?;
|
|
assert!(
|
|
update
|
|
.path_attributes
|
|
.contains(&PathAttribute::Communities(CommunitiesPathAttribute(vec![
|
|
(3356, 3),
|
|
(64601, 7)
|
|
]))),
|
|
"{update:?}"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Without `learned_routes` a peer is told what we originate and nothing
|
|
/// else, as before there was such a thing.
|
|
#[tokio::test]
|
|
async fn test_nothing_learned_is_passed_on_unless_asked_for() -> Result<()> {
|
|
let server = TestServer::start_with(LOCAL_ASN, |config| {
|
|
config.originate = vec![LocalRoute::new(
|
|
prefix("10.99.0.0/16"),
|
|
LocalRouteAttrs::default(),
|
|
)];
|
|
})
|
|
.await?;
|
|
let mut one = peer(&server, 1).await?;
|
|
let mut two = peer(&server, 2).await?;
|
|
let mut told = Told::default();
|
|
told_becomes(&mut two, &mut told, &["10.99.0.0/16"]).await?;
|
|
announce(&mut one, &["10.1.0.0/16"], &[64701], &[]).await?;
|
|
server.wait_for_routes(1).await?;
|
|
told_nothing(&mut two).await;
|
|
Ok(())
|
|
}
|