A peer can now send several routes for one prefix, told apart by a path identifier, and each is a route of its own in the table. Wire: - The ADD-PATH capability lists several address families. It was read as exactly one, so the OPEN of a router offering it for IPv4 and IPv6 was malformed and the session never came up. - Prefixes in UPDATEs are `Nlri`: a prefix and a path identifier. The identifier is on the wire, in the body and in MP_REACH/MP_UNREACH, for the families the session uses ADD-PATH for in that direction. Sending and receiving are settled separately, so a session has one codec context for each. Negotiation: - `add_path` on a peer in the configuration: off (the default), receive, send or both, offered for every family offered to the peer. - In use for a family and a direction when one side offered to send and the other to receive. Shown on /statusz and in /api/peers. Table: - A route is known by its peer and path identifier together. Identifiers are stored only in rows that have a non-zero one, so routes of peers that send one route each cost nothing more. - Equal routes from one peer are chosen between by the lower identifier. - /api/routes/received gives each route's path_id. Journal: - A second journal in each shard records every change to any route, for followers that want all the routes of a prefix and not only the best: RibTable::reader_of_all_routes and Event::Routes. This is what sending several routes to a peer will read. It costs nothing while nobody follows it. Sending: prefixes go out with a path identifier where that was agreed, but there is still one route for each prefix to send: only originated routes are announced. Tested with the table and journal model tests extended to path identifiers and to followers of all routes, the codec property tests and fuzz targets extended to ADD-PATH sessions, and end to end in both directions and for the case where only one side offers it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
185 lines
6.2 KiB
Rust
185 lines
6.2 KiB
Rust
//! A minimal stand-in for a neighbouring router: connects to the daemon,
|
|
//! completes the handshake, optionally announces some prefixes, and prints
|
|
//! every UPDATE it receives. Useful for trying things out when no real
|
|
//! router is around.
|
|
//!
|
|
//! cargo run -p bgp-integration-tests --example fake_peer -- \
|
|
//! --server 127.0.0.1:1790 --asn 65001 --announce 203.0.113.0/24
|
|
//!
|
|
//! With `--echo`, every prefix received is re-announced back to the server
|
|
//! with this peer's ASN prepended, which lets a single daemon show its own
|
|
//! routes "coming back" in the received table.
|
|
//!
|
|
//! With `--listen ADDR` it instead waits for the server to dial it (one
|
|
//! connection at a time), which exercises the server's active side.
|
|
|
|
use std::net::Ipv4Addr;
|
|
use std::time::Duration;
|
|
|
|
use bgp_integration_tests::test_peer::TestPeer;
|
|
use bgp_packet::message::{
|
|
AsPathAttribute, CommunitiesPathAttribute, Message, NextHopPathAttribute, OriginPathAttribute,
|
|
PathAttribute, UpdateMessage,
|
|
};
|
|
use bgp_primitives::ip_prefix::IpPrefix;
|
|
use clap::Parser;
|
|
use eyre::Result;
|
|
use tokio::net::TcpListener;
|
|
|
|
#[derive(Parser, Debug)]
|
|
struct Args {
|
|
/// Address of the daemon's BGP listener to connect to.
|
|
#[arg(long, default_value = "127.0.0.1:1790", conflicts_with = "listen")]
|
|
server: String,
|
|
/// Instead of connecting, listen here and accept connections from the
|
|
/// server, one at a time, forever.
|
|
#[arg(long)]
|
|
listen: Option<String>,
|
|
/// ASN this peer speaks as.
|
|
#[arg(long, default_value_t = 65001)]
|
|
asn: u32,
|
|
/// Router ID to send in OPEN.
|
|
#[arg(long, default_value = "10.0.0.2")]
|
|
router_id: Ipv4Addr,
|
|
/// Hold time to negotiate.
|
|
#[arg(long, default_value_t = 30)]
|
|
hold_time: u16,
|
|
/// Prefixes to announce after the session is established.
|
|
#[arg(long)]
|
|
announce: Vec<String>,
|
|
/// Re-announce every received prefix back to the server, with our ASN
|
|
/// prepended and NO_EXPORT-tagged routes dropped.
|
|
#[arg(long)]
|
|
echo: bool,
|
|
}
|
|
|
|
const NO_EXPORT: (u16, u16) = (0xFFFF, 0xFF01);
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
let args = Args::parse();
|
|
match &args.listen {
|
|
Some(listen) => {
|
|
let listener = TcpListener::bind(listen).await?;
|
|
println!("listening on {}", listener.local_addr()?);
|
|
loop {
|
|
let (stream, from) = listener.accept().await?;
|
|
println!("accepted connection from {from}");
|
|
if let Err(e) = run(&args, TestPeer::from_stream(stream), from).await {
|
|
println!("session ended: {e}");
|
|
}
|
|
}
|
|
}
|
|
None => {
|
|
let addr = args.server.parse()?;
|
|
run(&args, TestPeer::connect(addr).await?, addr).await
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn run(args: &Args, mut peer: TestPeer, addr: std::net::SocketAddr) -> Result<()> {
|
|
let open = peer
|
|
.handshake(args.asn, args.router_id, args.hold_time)
|
|
.await?;
|
|
println!(
|
|
"established with {} (AS{} router-id {})",
|
|
addr, open.asn, open.identifier
|
|
);
|
|
|
|
for p in &args.announce {
|
|
let prefix = IpPrefix::try_from(p.as_str())?;
|
|
let update = announce(prefix, vec![args.asn], vec![(args.asn as u16, 1)]);
|
|
peer.send(&Message::Update(update)).await?;
|
|
println!("announced {prefix}");
|
|
}
|
|
|
|
let keepalive = Duration::from_secs((args.hold_time / 3).max(1) as u64);
|
|
let mut ticker = tokio::time::interval(keepalive);
|
|
loop {
|
|
tokio::select! {
|
|
_ = ticker.tick() => peer.send(&Message::KeepAlive).await?,
|
|
msg = peer.recv() => match msg? {
|
|
Message::KeepAlive => {}
|
|
Message::Update(u) => {
|
|
for w in &u.withdrawn_routes {
|
|
println!("WITHDRAW {w}");
|
|
}
|
|
for a in &u.announced_routes {
|
|
println!("UPDATE {a} attrs={:?}", u.path_attributes);
|
|
}
|
|
if args.echo {
|
|
echo_back(&mut peer, &u, args.asn).await?;
|
|
}
|
|
}
|
|
Message::Notification(n) => {
|
|
println!("NOTIFICATION {}; session over", n.error);
|
|
return Ok(());
|
|
}
|
|
other => println!("unexpected {other:?}"),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn announce(prefix: IpPrefix, as_path: Vec<u32>, communities: Vec<(u16, u16)>) -> UpdateMessage {
|
|
let mut path_attributes = vec![
|
|
PathAttribute::Origin(OriginPathAttribute::IGP),
|
|
AsPathAttribute::from_asns(as_path),
|
|
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(10, 0, 0, 2))),
|
|
];
|
|
if !communities.is_empty() {
|
|
path_attributes.push(PathAttribute::Communities(CommunitiesPathAttribute(
|
|
communities,
|
|
)));
|
|
}
|
|
UpdateMessage {
|
|
withdrawn_routes: vec![],
|
|
path_attributes,
|
|
announced_routes: vec![prefix.into()],
|
|
}
|
|
}
|
|
|
|
async fn echo_back(peer: &mut TestPeer, u: &UpdateMessage, asn: u32) -> Result<()> {
|
|
let mut as_path = vec![asn];
|
|
let mut communities = vec![];
|
|
for attr in &u.path_attributes {
|
|
match attr {
|
|
PathAttribute::AsPath(p) => {
|
|
as_path.extend(p.segments.iter().flat_map(|s| s.path.iter().copied()))
|
|
}
|
|
PathAttribute::Communities(c) => communities = c.0.clone(),
|
|
_ => {}
|
|
}
|
|
}
|
|
if communities.contains(&NO_EXPORT) {
|
|
println!(" not echoing: NO_EXPORT");
|
|
return Ok(());
|
|
}
|
|
for nlri in &u.announced_routes {
|
|
let prefix = nlri.prefix;
|
|
peer.send(&Message::Update(announce(
|
|
prefix,
|
|
as_path.clone(),
|
|
communities.clone(),
|
|
)))
|
|
.await?;
|
|
println!(" echoed {prefix} back with path {as_path:?}");
|
|
}
|
|
if !u.withdrawn_routes.is_empty() {
|
|
peer.send(&Message::Update(UpdateMessage {
|
|
withdrawn_routes: u.withdrawn_routes.clone(),
|
|
path_attributes: vec![],
|
|
announced_routes: vec![],
|
|
}))
|
|
.await?;
|
|
println!(
|
|
" echoed withdraw of {:?}",
|
|
u.withdrawn_routes
|
|
.iter()
|
|
.map(|p| p.to_string())
|
|
.collect::<Vec<_>>()
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|