The test files had grown their own copies of the same helpers: prefix() twelve times, established() seven, an UPDATE builder six, a seventeen field PeerConfig literal in fifteen places, a timeout constant in twenty one. Each is in crates/integration-tests/src once now: peer_config() and TestServer::with_peer for a configured peer, Announcement for building UPDATEs, established(), the waits (wait_until, wait_for, wait_for_routes, wait_for_sessions), get_json, and TIMEOUT. A new field on PeerConfig is one line to add, not fifteen. TestPeer::recv has a deadline, so a test that gets nothing fails instead of hanging; is_closed, which read one byte and took a KEEPALIVE for an open connection, is closed_within and closed_unanswered. Net 788 lines fewer; every test checks what it did. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
164 lines
5.7 KiB
Rust
164 lines
5.7 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::announcement::{Announcement, withdrawal};
|
|
use bgp_integration_tests::test_peer::TestPeer;
|
|
use bgp_packet::message::{Message, 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())?;
|
|
peer.announce(announce(prefix, vec![args.asn], vec![(args.asn as u16, 1)]))
|
|
.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?,
|
|
// However long the server is quiet: this peer stays on the line.
|
|
msg = peer.recv_untimed() => 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)>) -> Announcement {
|
|
Announcement::of([prefix.into()], &as_path)
|
|
.with_next_hop(Ipv4Addr::new(10, 0, 0, 2))
|
|
.with_communities(&communities)
|
|
}
|
|
|
|
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.announce(announce(prefix, as_path.clone(), communities.clone()))
|
|
.await?;
|
|
println!(" echoed {prefix} back with path {as_path:?}");
|
|
}
|
|
if !u.withdrawn_routes.is_empty() {
|
|
peer.announce(withdrawal(u.withdrawn_routes.iter().cloned()))
|
|
.await?;
|
|
println!(
|
|
" echoed withdraw of {:?}",
|
|
u.withdrawn_routes
|
|
.iter()
|
|
.map(|p| p.to_string())
|
|
.collect::<Vec<_>>()
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|