Routes were announced and withdrawn through an HTTP form, each tagged with a free-text owner and checked against an allow-list and a per-owner limit. That was the lab's interface, not a router's. Routes now come from 'originate' entries in the configuration file and are in the local RIB before any session starts. The local RIB holds one route per prefix with no notion of owners or policy. local_route_policy is gone from the configuration. The HTTP API keeps its GET endpoints (status, config, peers, originated and received routes, events, metrics). The announce, withdraw, wire-bytes and well-known-community endpoints are removed, and the status page loses the announce form, the community presets and the byte decoder. Community text parsing moves from the web crate to bgp-packet so the configuration can use it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
160 lines
6.0 KiB
Rust
160 lines
6.0 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.clone(), 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] }
|
|
}
|
|
|
|
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 prefix in &u.announced_routes {
|
|
peer.send(&Message::Update(announce(prefix.clone(), 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(())
|
|
}
|