Rework the speaker: sans-I/O FSM, sharded table, RFC 7606, ADD-PATH

The work of the speaker-cleanup branch as one commit; the branch keeps
the step-by-step history.

Sessions
- The state machine is a pure function from events to actions, driven by
  one task per connection. Idle, Connect and Active belong to a dialer
  that keeps configured peers up with backoff.
- Capabilities are negotiated: address families, four-octet AS numbers
  with AS_TRANS and AS4_PATH, route refresh, ADD-PATH per family and
  direction.
- Collision detection per RFC 4271 section 6.8, by address and
  identifier.
- Connections are taken only from configured peers, a few per address.
- A panic in a session is contained and answered with a Cease; a core
  task that stops shuts the server down with a Cease to every peer.
- Writes that make no progress are given up (RFC 9687).

Codec
- Malformed UPDATEs are handled as RFC 7606 says: treat-as-withdraw,
  attribute discard, or session reset, with the NOTIFICATION to send.
- Address families are open types; any can be named and ignored.
- Prefixes are fixed-size values, normalised, with path identifiers.
- Lengths are filled in after writing; one definition of each type code.
- Property tests and fuzz targets for every decoder.

Routing table
- Adj-RIBs-In and Loc-RIB in one sharded table that sessions write to
  directly. Routes are sixteen bytes; attribute sets are interned in
  wire form. Best-path selection per RFC 4271 section 9.1.2.2 with
  deterministic MED.
- A journal per shard lets followers read changes at their own pace;
  one that falls behind is resynchronised.
- On a RIPE RIS dump of 148 peers and 39.3M routes: loaded in 2.0 s on
  12 cores, about 52 bytes a route.

Announcing
- Originated routes come from the configuration. Each session has its
  own exporter with an Adj-RIB-Out, so a slow peer delays only itself.
- Routes to a peer in our own AS have an empty path and a LOCAL_PREF.

Operations
- HTTP status page, JSON API, event stream and Prometheus metrics.
- Benchmarks on synthetic and real tables.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-10-02 12:35:54 +02:00
1 parent 1ec60255ea
commit 630c175918
138 files changed
+21650 -2925

No files matched your search

+12
View File
@@ -1,6 +1,11 @@
[package]
name = "bgp-integration-tests"
authors.workspace = true
edition.workspace = true
homepage.workspace = true
license.workspace = true
repository.workspace = true
version.workspace = true
publish = false
[dependencies]
@@ -8,6 +13,13 @@ bgp-packet.workspace = true
bgp-primitives.workspace = true
bgp-rib.workspace = true
bgp-server.workspace = true
bgp-web.workspace = true
bytes.workspace = true
clap.workspace = true
eyre.workspace = true
reqwest.workspace = true
serde_json.workspace = true
tokio.workspace = true
[lints]
workspace = true
@@ -0,0 +1,184 @@
//! 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(())
}
+105 -19
View File
@@ -1,8 +1,8 @@
use std::net::{Ipv4Addr, SocketAddr};
use bgp_packet::message::{BGP_HEADER_LEN, Message};
use bgp_packet::message::{BGP_HEADER_LEN, Message, UpdateMessage};
use bgp_packet::open::{Capability, OpenMessage, OpenOption};
use bgp_primitives::constants::{AddressFamilyId, SubsequentAfi};
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
use bgp_primitives::parser::ParserContext;
use bytes::BytesMut;
use eyre::{Result, bail, eyre};
@@ -14,18 +14,41 @@ use tokio::net::TcpStream;
pub struct TestPeer {
stream: TcpStream,
pub ctx: ParserContext,
/// The address families for which this peer sends path identifiers
/// with its prefixes (ADD-PATH, RFC 7911). What it expects to be sent
/// is `ctx.add_path`. A test sets both to match the capabilities it
/// exchanged.
pub send_add_path: Vec<AfiSafi>,
/// Whether the OPEN we sent offered four-octet AS numbers. They are in
/// use only if the server's did too.
offered_four_octet_asn: bool,
}
impl TestPeer {
pub async fn connect(addr: SocketAddr) -> Result<Self> {
let stream = TcpStream::connect(addr).await?;
Ok(Self { stream, ctx: ParserContext::default() })
Ok(Self::from_stream(stream))
}
/// Wraps an already-connected stream, e.g. one accepted from a server
/// that dialled us.
pub fn from_stream(stream: TcpStream) -> Self {
Self {
stream,
ctx: ParserContext::default(),
send_add_path: Vec::new(),
offered_four_octet_asn: true,
}
}
/// Send a BGP message.
pub async fn send(&mut self, msg: &Message) -> Result<()> {
let send_ctx = ParserContext {
add_path: self.send_add_path.clone(),
..self.ctx.clone()
};
let mut buf = BytesMut::new();
msg.to_wire(&self.ctx, &mut buf)?;
msg.to_wire(&send_ctx, &mut buf)?;
self.stream.write_all(&buf).await?;
Ok(())
}
@@ -42,11 +65,23 @@ impl TestPeer {
let mut buf = vec![0u8; total_len];
buf[..BGP_HEADER_LEN as usize].copy_from_slice(&header);
self.stream.read_exact(&mut buf[BGP_HEADER_LEN as usize..]).await?;
self.stream
.read_exact(&mut buf[BGP_HEADER_LEN as usize..])
.await?;
let (_, msg) = Message::from_wire(&self.ctx, &buf)
.map_err(|e| eyre!("Failed to parse BGP message: {e:?}"))?;
Ok(msg)
Message::decode(&self.ctx, &buf).map_err(|e| eyre!("Failed to parse BGP message: {e}"))
}
/// Read messages until an UPDATE arrives, skipping KEEPALIVEs. Any other
/// message type is an error.
pub async fn recv_update(&mut self) -> Result<UpdateMessage> {
loop {
match self.recv().await? {
Message::Update(u) => return Ok(u),
Message::KeepAlive => {}
other => bail!("Expected UPDATE, got {:?}", other),
}
}
}
/// Receive and unwrap as `OpenMessage`, failing if a different type arrives.
@@ -58,34 +93,67 @@ impl TestPeer {
matches!(opt, OpenOption::Capabilities(caps)
if caps.iter().any(|c| matches!(c, Capability::FourByteAsn { .. })))
});
self.ctx.four_octet_asn = Some(four_byte);
self.ctx.four_octet_asn = four_byte && self.offered_four_octet_asn;
Ok(o)
}
other => bail!("Expected OPEN, got {:?}", other),
}
}
/// Send an OPEN message with the given parameters and standard capabilities.
/// Send an OPEN message with the given parameters and the capabilities
/// of a typical dual-stack speaker: four-octet AS numbers, IPv4 and IPv6
/// unicast, route refresh.
pub async fn send_open(
&mut self,
local_asn: u32,
router_id: Ipv4Addr,
hold_time: u16,
) -> Result<()> {
let asn_field = if local_asn > u16::MAX as u32 { 23456 } else { local_asn as u16 };
let capabilities = vec![
Capability::FourByteAsn { asn: local_asn },
Capability::MultiProtocol {
afi: Afi::IPV4,
safi: Safi::UNICAST,
},
Capability::MultiProtocol {
afi: Afi::IPV6,
safi: Safi::UNICAST,
},
Capability::RouteRefresh,
];
self.send_open_with(local_asn, router_id, hold_time, capabilities)
.await
}
/// Send an OPEN message carrying exactly `capabilities`.
pub async fn send_open_with(
&mut self,
local_asn: u32,
router_id: Ipv4Addr,
hold_time: u16,
capabilities: Vec<Capability>,
) -> Result<()> {
self.offered_four_octet_asn = capabilities
.iter()
.any(|c| matches!(c, Capability::FourByteAsn { .. }));
// If the server's OPEN has been read already, what was settled
// then still depends on what is offered now.
self.ctx.four_octet_asn &= self.offered_four_octet_asn;
let asn_field = if local_asn > u16::MAX as u32 {
23456
} else {
local_asn as u16
};
let open = Message::Open(OpenMessage {
version: 4,
asn: asn_field,
hold_time,
identifier: router_id,
options: vec![OpenOption::Capabilities(vec![
Capability::FourByteAsn { asn: local_asn },
Capability::MultiProtocol {
afi: AddressFamilyId::Ipv4,
safi: SubsequentAfi::Unicast,
},
Capability::RouteRefresh,
])],
options: if capabilities.is_empty() {
vec![]
} else {
vec![OpenOption::Capabilities(capabilities)]
},
});
self.send(&open).await
}
@@ -99,6 +167,24 @@ impl TestPeer {
hold_time: u16,
) -> Result<OpenMessage> {
self.send_open(local_asn, router_id, hold_time).await?;
self.finish_handshake().await
}
/// As [`TestPeer::handshake`], with an OPEN carrying exactly
/// `capabilities`.
pub async fn handshake_with(
&mut self,
local_asn: u32,
router_id: Ipv4Addr,
hold_time: u16,
capabilities: Vec<Capability>,
) -> Result<OpenMessage> {
self.send_open_with(local_asn, router_id, hold_time, capabilities)
.await?;
self.finish_handshake().await
}
async fn finish_handshake(&mut self) -> Result<OpenMessage> {
let server_open = self.recv_open().await?;
// Expect server KEEPALIVE
match self.recv().await? {
+173 -12
View File
@@ -1,19 +1,32 @@
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use bgp_rib::AdjRibIn;
use bgp_packet::message::{NlriNextHop, PathAttribute};
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::LocalRib;
use bgp_rib::table::{Event, PeerIndex, Reader, RibTable};
use bgp_server::config::Config;
use bgp_server::server::BgpServerBuilder;
use bgp_server::error::ServerError;
use bgp_server::server::BgpServer;
use bgp_server::session::SessionRegistry;
use bgp_web::{AppState, RouterInfo};
use eyre::Result;
use tokio::net::TcpListener;
use tokio::task::JoinHandle;
/// A BGP server started on an ephemeral port for testing.
/// Aborts the server task when dropped.
pub struct TestServer {
handle: JoinHandle<Result<()>>,
handle: JoinHandle<Result<(), ServerError>>,
/// The single IPv4 loopback address the server is bound to.
pub addr: SocketAddr,
/// Handle to the server's Adj-RIB-In for test introspection.
pub adj_rib_in: AdjRibIn,
/// The server's routing table.
pub rib: Arc<RibTable>,
/// Handle to the server's locally originated routes.
pub local_rib: LocalRib,
/// Live view of the server's established sessions.
pub registry: SessionRegistry,
pub local_asn: u32,
}
impl TestServer {
@@ -24,28 +37,176 @@ impl TestServer {
/// Start a server with an explicit hold time (useful for timer tests).
pub async fn start_with_hold_time(local_asn: u32, hold_time_secs: u16) -> Result<Self> {
Self::start_inner(local_asn, hold_time_secs).await
Self::start_with(local_asn, |c| c.hold_time_secs = hold_time_secs).await
}
async fn start_inner(local_asn: u32, hold_time_secs: u16) -> Result<Self> {
let config = Config {
/// Start a server after letting `customize` edit the default test config
/// (e.g. to add configured peers).
pub async fn start_with(local_asn: u32, customize: impl FnOnce(&mut Config)) -> Result<Self> {
let mut config = Config {
router_id: Ipv4Addr::new(127, 0, 0, 1),
local_asn,
hold_time_secs,
hold_time_secs: 90,
listen_endpoints: vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)],
peers: vec![],
connect_retry: std::time::Duration::from_secs(5),
collision_detect_established_state: false,
// Most tests play a peer that connects from wherever the
// system puts it; the ones about who may connect turn this off.
accept_unconfigured_peers: true,
originate: vec![],
http_endpoint: None,
};
customize(&mut config);
let bound = BgpServerBuilder::new(config).bind().await?;
let bound = BgpServer::bind(config).await?;
let addr = bound
.local_addrs()
.into_iter()
.next()
.expect("at least one listener");
let adj_rib_in = bound.adj_rib_in.clone();
let rib = Arc::clone(&bound.rib);
let local_rib = bound.local_rib.clone();
let registry = bound.registry.clone();
let handle = tokio::spawn(bound.run());
Ok(Self { handle, addr, adj_rib_in })
Ok(Self {
handle,
addr,
rib,
local_rib,
registry,
local_asn,
})
}
}
/// A route in the server's table, as a test wants to look at it.
#[derive(Debug, Clone)]
pub struct ReceivedRoute {
pub prefix: IpPrefix,
pub peer: PeerIndex,
/// The peer's identifier for the route; zero without ADD-PATH.
pub path_id: u32,
/// The route's attributes, without MP_REACH_NLRI.
pub attributes: Vec<PathAttribute>,
/// The next hop, if the route came in MP_REACH_NLRI.
pub next_hop: Option<NlriNextHop>,
/// Whether it is the best route for its prefix.
pub best: bool,
}
/// A change of a prefix's best route.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RouteChange {
/// The prefix has a best route, or another one than before.
Announced { prefix: IpPrefix, peer: PeerIndex },
/// The prefix has no route any more.
Withdrawn { prefix: IpPrefix },
}
/// The changes of best route in a server's table from the moment this was
/// made, read from the table's journal.
pub struct RouteChanges {
reader: Reader,
pending: std::collections::VecDeque<RouteChange>,
}
impl RouteChanges {
/// The next change, waiting for one if need be. Changes to one prefix
/// that happen before they are read arrive as one.
pub async fn recv(&mut self) -> Result<RouteChange> {
loop {
if let Some(change) = self.pending.pop_front() {
return Ok(change);
}
self.reader.changed().await;
self.read();
}
}
fn read(&mut self) {
let pending = &mut self.pending;
let mut read = |event: Event<'_>| match event {
Event::Best { prefix, peer, .. } => {
pending.push_back(RouteChange::Announced { prefix, peer })
}
Event::Withdrawn { prefix } => pending.push_back(RouteChange::Withdrawn { prefix }),
// Not given to a follower of the best routes.
Event::Routes { .. } => {}
Event::Reset | Event::Synced => {}
};
while !self.reader.poll(1024, &mut read).caught_up {}
}
}
impl TestServer {
/// Every route in the server's table.
pub fn received_routes(&self) -> Vec<ReceivedRoute> {
let mut routes = Vec::new();
self.rib.for_each_route(|route| {
let (attributes, next_hop) = route.attrs.attributes().expect("stored attributes parse");
routes.push(ReceivedRoute {
prefix: route.prefix,
peer: route.peer,
path_id: route.path_id,
attributes,
next_hop,
best: route.best,
});
});
routes
}
/// Follows the changes of best route from now on.
pub fn route_changes(&self) -> RouteChanges {
let mut changes = RouteChanges {
reader: self.rib.reader(),
pending: Default::default(),
};
// A new reader is first given the table as it is. That is not a
// change.
changes.read();
changes.pending.clear();
changes
}
/// Waits for the server to stop by itself and returns why. A healthy
/// server never does.
pub async fn stopped(&mut self) -> Result<(), ServerError> {
(&mut self.handle)
.await
.expect("the server task does not panic")
}
/// Starts the HTTP API on an ephemeral loopback port and returns its
/// base URL (e.g. `http://127.0.0.1:12345`). The API task is aborted
/// when the returned guard drops.
pub async fn serve_http(&self) -> Result<(String, HttpGuard)> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let base_url = format!("http://{}", listener.local_addr()?);
let state = AppState {
info: std::sync::Arc::new(
RouterInfo::new(Ipv4Addr::new(127, 0, 0, 1), self.local_asn, "test")
.with_config("test.textpb", format!("local_asn: {}\n", self.local_asn)),
),
registry: self.registry.clone(),
local_rib: self.local_rib.clone(),
rib: Arc::clone(&self.rib),
};
let handle = tokio::spawn(bgp_web::serve_on(state, listener));
Ok((base_url, HttpGuard { handle }))
}
}
/// Aborts the HTTP API task when dropped.
pub struct HttpGuard {
handle: JoinHandle<std::io::Result<()>>,
}
impl Drop for HttpGuard {
fn drop(&mut self) {
self.handle.abort();
}
}
+288
View File
@@ -0,0 +1,288 @@
//! End-to-end tests of ADD-PATH (RFC 7911): a peer sending several routes
//! for one prefix, told apart by path identifiers.
use std::net::{Ipv4Addr, SocketAddr};
use std::time::Duration;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::TestServer;
use bgp_packet::message::{
AsPathAttribute, Message, NextHopPathAttribute, OriginPathAttribute, PathAttribute,
UpdateMessage,
};
use bgp_packet::open::{AddPathEntry, AddPathSendRecv, Capability, OpenMessage};
use bgp_primitives::constants::AfiSafi;
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
use bgp_rib::{LocalRoute, LocalRouteAttrs};
use bgp_server::config::PeerConfig;
use bgp_server::family::{FamilySet, SUPPORTED};
use eyre::Result;
use serde_json::Value;
const LOCAL_ASN: u32 = 65000;
const PEER_ASN: u32 = 65001;
const TIMEOUT: Duration = Duration::from_secs(5);
fn prefix(s: &str) -> IpPrefix {
IpPrefix::try_from(s).unwrap()
}
/// A server on which connections from this machine are those of a
/// configured peer, with ADD-PATH offered to it as `add_path` says.
async fn server_offering(
add_path: Option<AddPathSendRecv>,
originate: &[&str],
) -> Result<TestServer> {
let originate: Vec<LocalRoute> = originate
.iter()
.map(|p| LocalRoute::new(prefix(p), LocalRouteAttrs::default()))
.collect();
TestServer::start_with(LOCAL_ASN, |c| {
c.peers = vec![PeerConfig {
name: "route-server-client".into(),
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1),
remote_asn: PEER_ASN,
hold_time_secs: 90,
local_address: None,
families: FamilySet::supported(),
add_path,
}];
c.originate = originate;
// Keep the dialer out of the way.
c.connect_retry = Duration::from_secs(3600);
})
.await
}
/// A session from a peer whose OPEN offers ADD-PATH as `offer` says, for
/// IPv4 unicast. Returns the peer and the server's OPEN.
async fn peer_offering(
server: &TestServer,
offer: Option<AddPathSendRecv>,
) -> Result<(TestPeer, OpenMessage)> {
let mut capabilities = vec![Capability::FourByteAsn { asn: PEER_ASN }];
capabilities.extend(SUPPORTED.into_iter().map(Capability::multiprotocol));
if let Some(send_recv) = offer {
capabilities.push(Capability::AddPath {
entries: vec![AddPathEntry {
afi: AfiSafi::IPV4_UNICAST.afi,
safi: AfiSafi::IPV4_UNICAST.safi,
send_recv,
}],
});
}
let mut peer = TestPeer::connect(server.addr).await?;
let open = peer
.handshake_with(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 90, capabilities)
.await?;
tokio::time::timeout(TIMEOUT, async {
while server.registry.list().is_empty() {
tokio::time::sleep(Duration::from_millis(5)).await;
}
})
.await?;
Ok((peer, open))
}
fn add_path_offered(open: &OpenMessage) -> Vec<(AfiSafi, AddPathSendRecv)> {
open.capabilities()
.filter_map(|capability| match capability {
Capability::AddPath { entries } => Some(entries),
_ => None,
})
.flatten()
.map(|entry| (entry.family(), entry.send_recv))
.collect()
}
fn announcement(nlri: Nlri, as_path: &[u32]) -> Message {
Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(as_path.to_vec()),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
],
announced_routes: vec![nlri],
})
}
async fn wait_for_routes(server: &TestServer, routes: usize) -> Result<()> {
tokio::time::timeout(TIMEOUT, async {
while server.rib.len() != routes {
tokio::time::sleep(Duration::from_millis(5)).await;
}
})
.await?;
Ok(())
}
/// The routes the server holds, as (path identifier, AS path, best).
fn held(server: &TestServer) -> Vec<(u32, Vec<u32>, bool)> {
let mut held: Vec<(u32, Vec<u32>, bool)> = server
.received_routes()
.into_iter()
.map(|route| {
let path = route
.attributes
.iter()
.find_map(|attribute| match attribute {
PathAttribute::AsPath(path) => Some(path.segments[0].path.clone()),
_ => None,
})
.unwrap_or_default();
(route.path_id, path, route.best)
})
.collect();
held.sort();
held
}
/// A peer that sends several routes for one prefix: each is kept, the best
/// of them is chosen, and each is withdrawn on its own.
#[tokio::test]
async fn test_several_routes_for_a_prefix_from_one_peer() -> Result<()> {
let server = server_offering(Some(AddPathSendRecv::Receive), &[]).await?;
let (base, _http) = server.serve_http().await?;
let (mut peer, open) = peer_offering(&server, Some(AddPathSendRecv::Send)).await?;
// The server offered to receive, for every family it offers.
assert_eq!(
add_path_offered(&open),
SUPPORTED.map(|family| (family, AddPathSendRecv::Receive))
);
// In use for the one family the peer sends for, and in that direction.
let session = &server.registry.list()[0];
assert_eq!(
session.add_path_receive,
[AfiSafi::IPV4_UNICAST].into_iter().collect()
);
assert!(session.add_path_send.is_empty());
peer.send_add_path = vec![AfiSafi::IPV4_UNICAST];
let p = prefix("203.0.113.0/24");
let path = |path_id| Nlri { prefix: p, path_id };
peer.send(&announcement(path(1), &[PEER_ASN, 64999, 64998]))
.await?;
peer.send(&announcement(path(2), &[PEER_ASN, 64998]))
.await?;
wait_for_routes(&server, 2).await?;
assert_eq!(
held(&server),
[
(1, vec![PEER_ASN, 64999, 64998], false),
(2, vec![PEER_ASN, 64998], true),
]
);
assert_eq!(server.rib.prefix_count(), 1);
// The API tells them apart too.
let routes: Value = reqwest::get(format!("{base}/api/routes/received"))
.await?
.json()
.await?;
let mut ids: Vec<u64> = routes
.as_array()
.unwrap()
.iter()
.map(|route| route["path_id"].as_u64().unwrap())
.collect();
ids.sort_unstable();
assert_eq!(ids, [1, 2]);
let peers: Value = reqwest::get(format!("{base}/api/peers"))
.await?
.json()
.await?;
assert_eq!(
peers[0]["add_path_receive"],
serde_json::json!(["IPv4 unicast"])
);
// Announcing a path again replaces that path only.
peer.send(&announcement(path(1), &[PEER_ASN])).await?;
tokio::time::timeout(TIMEOUT, async {
while held(&server)[0].1.len() != 1 {
tokio::time::sleep(Duration::from_millis(5)).await;
}
})
.await?;
assert_eq!(
held(&server),
[(1, vec![PEER_ASN], true), (2, vec![PEER_ASN, 64998], false),]
);
// And withdrawing one leaves the other.
peer.send(&Message::Update(UpdateMessage {
withdrawn_routes: vec![path(1)],
path_attributes: vec![],
announced_routes: vec![],
}))
.await?;
wait_for_routes(&server, 1).await?;
assert_eq!(held(&server), [(2, vec![PEER_ASN, 64998], true)]);
// When the session ends, all of its routes go.
peer.send(&announcement(path(3), &[PEER_ASN, 1, 2, 3]))
.await?;
wait_for_routes(&server, 2).await?;
drop(peer);
wait_for_routes(&server, 0).await?;
Ok(())
}
/// ADD-PATH changes how prefixes are written. It must be in use only when
/// both sides said so; a peer that offers it to a server that does not is
/// spoken to, and listened to, in plain BGP.
#[tokio::test]
async fn test_add_path_is_not_used_unless_both_sides_offer_it() -> Result<()> {
let server = server_offering(None, &["10.100.0.0/16"]).await?;
let (mut peer, open) = peer_offering(&server, Some(AddPathSendRecv::Both)).await?;
assert!(add_path_offered(&open).is_empty());
let session = &server.registry.list()[0];
assert!(session.add_path_receive.is_empty() && session.add_path_send.is_empty());
// Plain prefixes both ways.
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
peer.send(&announcement(prefix("203.0.113.0/24").into(), &[PEER_ASN]))
.await?;
wait_for_routes(&server, 1).await?;
assert_eq!(held(&server), [(0, vec![PEER_ASN], true)]);
// And the other way round: offered by the server, not by the peer.
let server = server_offering(Some(AddPathSendRecv::Both), &["10.100.0.0/16"]).await?;
let (mut peer, open) = peer_offering(&server, None).await?;
assert_eq!(add_path_offered(&open).len(), SUPPORTED.len());
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
Ok(())
}
/// Where the peer offered to receive and the server to send, the server's
/// prefixes carry a path identifier, and a peer that reads them so
/// understands them.
#[tokio::test]
async fn test_prefixes_are_sent_with_path_identifiers_when_agreed() -> Result<()> {
let server = server_offering(Some(AddPathSendRecv::Both), &["10.100.0.0/16"]).await?;
let (mut peer, _) = peer_offering(&server, Some(AddPathSendRecv::Receive)).await?;
let session = &server.registry.list()[0];
assert_eq!(
session.add_path_send,
[AfiSafi::IPV4_UNICAST].into_iter().collect()
);
assert!(session.add_path_receive.is_empty());
// Read as agreed: with identifiers.
peer.ctx.add_path = vec![AfiSafi::IPV4_UNICAST];
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
assert_eq!(update.announced_routes.len(), 1);
assert_eq!(update.announced_routes[0].prefix, prefix("10.100.0.0/16"));
// The peer did not offer to send, so the server reads its prefixes
// plain.
peer.send(&announcement(prefix("203.0.113.0/24").into(), &[PEER_ASN]))
.await?;
wait_for_routes(&server, 1).await?;
assert_eq!(held(&server), [(0, vec![PEER_ASN], true)]);
Ok(())
}
+112
View File
@@ -0,0 +1,112 @@
//! Who may connect: only the addresses of configured peers, and each of
//! them only so many times at once.
use std::net::{Ipv4Addr, SocketAddr};
use std::time::Duration;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::TestServer;
use bgp_server::config::PeerConfig;
use bgp_server::family::FamilySet;
use eyre::Result;
use tokio::net::TcpListener;
const LOCAL_ASN: u32 = 65000;
const PEER_ASN: u32 = 65001;
const TIMEOUT: Duration = Duration::from_secs(5);
fn peer_at(endpoint: SocketAddr) -> PeerConfig {
PeerConfig {
name: "r1".into(),
endpoint,
remote_asn: PEER_ASN,
hold_time_secs: 90,
local_address: None,
families: FamilySet::supported(),
add_path: None,
}
}
/// Whether the server closed the connection without sending anything.
async fn closed_unanswered(peer: &mut TestPeer) -> bool {
matches!(
tokio::time::timeout(TIMEOUT, peer.recv()).await,
Ok(Err(_))
)
}
/// A host that is not a configured peer gets nothing: no OPEN to answer,
/// no session, no routes taken from it. Any host used to be able to open a
/// session as any AS it named.
#[tokio::test]
async fn test_a_host_that_is_not_a_configured_peer_is_refused() -> Result<()> {
// The only peer configured is somewhere else (documentation space).
let server = TestServer::start_with(LOCAL_ASN, |c| {
c.accept_unconfigured_peers = false;
c.peers = vec![peer_at("192.0.2.9:179".parse().unwrap())];
})
.await?;
let mut stranger = TestPeer::connect(server.addr).await?;
assert!(closed_unanswered(&mut stranger).await);
assert!(server.registry.list().is_empty());
Ok(())
}
/// The address of a configured peer is let in, and held to what is
/// configured for that peer.
#[tokio::test]
async fn test_a_configured_peer_is_let_in() -> Result<()> {
// Somewhere for the server's own dialling to go; never answered.
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let endpoint = listener.local_addr()?;
let server = TestServer::start_with(LOCAL_ASN, |c| {
c.accept_unconfigured_peers = false;
c.peers = vec![peer_at(endpoint)];
})
.await?;
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
.await?;
tokio::time::timeout(TIMEOUT, async {
while server.registry.list().is_empty() {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
assert_eq!(server.registry.list()[0].name.as_deref(), Some("r1"));
Ok(())
}
/// One address may hold only a few connections at a time, so that one host
/// cannot use up the server's sockets with connections that never get as
/// far as a session.
#[tokio::test]
async fn test_one_address_may_open_only_a_few_connections() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
// Four that say nothing: each is sent the server's OPEN and waited on.
let mut held = Vec::new();
for _ in 0..4 {
let mut peer = TestPeer::connect(server.addr).await?;
tokio::time::timeout(TIMEOUT, peer.recv_open()).await??;
held.push(peer);
}
let mut one_too_many = TestPeer::connect(server.addr).await?;
assert!(closed_unanswered(&mut one_too_many).await);
// When one of them goes, there is room again.
held.pop();
tokio::time::timeout(TIMEOUT, async {
loop {
let mut peer = TestPeer::connect(server.addr).await?;
if peer.recv_open().await.is_ok() {
return Ok::<_, eyre::Report>(());
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
})
.await??;
Ok(())
}
+500
View File
@@ -0,0 +1,500 @@
//! End-to-end tests for locally originated routes: announcing via the
//! `LocalRib` handle must produce UPDATEs on every established session.
use std::net::Ipv4Addr;
use std::time::Duration;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::{RouteChange, TestServer};
use bgp_packet::message::{
AsPathAttribute, CommunitiesPathAttribute, LargeCommunitiesPathAttribute, LargeCommunity,
LocalPrefPathAttribute, NextHopPathAttribute, OriginPathAttribute, PathAttribute,
UpdateMessage,
};
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::{LocalRoute, LocalRouteAttrs};
use bgp_server::session::SessionEvent;
use eyre::Result;
const LOCAL_ASN: u32 = 65000;
const PEER_ASN: u32 = 65001;
const TIMEOUT: Duration = Duration::from_secs(5);
fn prefix(s: &str) -> IpPrefix {
IpPrefix::try_from(s).unwrap()
}
fn route(p: &str, attrs: LocalRouteAttrs) -> LocalRoute {
LocalRoute::new(prefix(p), attrs)
}
async fn established_peer(server: &TestServer) -> Result<TestPeer> {
established_peer_with_id(server, Ipv4Addr::new(10, 0, 0, 1)).await
}
/// Distinct routers need distinct BGP Identifiers, or the second connection
/// is (correctly) treated as a collision with the first.
async fn established_peer_with_id(server: &TestServer, router_id: Ipv4Addr) -> Result<TestPeer> {
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(PEER_ASN, router_id, 90).await?;
Ok(peer)
}
async fn next_update(peer: &mut TestPeer) -> Result<UpdateMessage> {
tokio::time::timeout(TIMEOUT, peer.recv_update()).await?
}
/// Waits until the registry reports `n` established sessions.
async fn wait_for_sessions(server: &TestServer, n: usize) -> Result<()> {
tokio::time::timeout(TIMEOUT, async {
while server.registry.list().len() != n {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
Ok(())
}
// ---------------------------------------------------------------------------
// Session registry
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_registry_tracks_session_lifecycle() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut events = server.registry.subscribe();
assert!(server.registry.list().is_empty());
let peer = established_peer(&server).await?;
let up = tokio::time::timeout(TIMEOUT, events.recv()).await??;
let SessionEvent::Up(handle) = up else {
panic!("expected Up, got {up:?}")
};
assert_eq!(handle.peer.asn, PEER_ASN);
assert_eq!(handle.local_addr, server.addr);
assert!(handle.four_octet_asn);
assert_eq!(server.registry.list().len(), 1);
drop(peer);
let down = tokio::time::timeout(TIMEOUT, events.recv()).await??;
assert!(
matches!(down, SessionEvent::Down(p) if p == handle.peer),
"got {down:?}"
);
assert!(server.registry.list().is_empty());
Ok(())
}
// ---------------------------------------------------------------------------
// Announce / withdraw
// ---------------------------------------------------------------------------
/// A route announced while the session is up is sent immediately, with the
/// attributes the announcer chose and our address as the next hop.
#[tokio::test]
async fn test_announce_after_session_up() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = established_peer(&server).await?;
wait_for_sessions(&server, 1).await?;
server
.local_rib
.announce(route(
"10.100.1.0/24",
LocalRouteAttrs {
origin: OriginPathAttribute::IGP,
prepend: 0,
med: None,
communities: vec![(65000, 100), (65535, 65281)],
large_communities: vec![LargeCommunity {
global_admin: 65000,
data_1: 7,
data_2: 8,
}],
},
))
.await?;
let update = next_update(&mut peer).await?;
assert_eq!(update.announced_routes, vec![prefix("10.100.1.0/24")]);
assert!(update.withdrawn_routes.is_empty());
assert_eq!(
update.path_attributes,
vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![LOCAL_ASN]),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::LOCALHOST)),
PathAttribute::Communities(CommunitiesPathAttribute(vec![
(65000, 100),
(65535, 65281)
])),
PathAttribute::LargeCommunities(LargeCommunitiesPathAttribute {
communities: vec![LargeCommunity {
global_admin: 65000,
data_1: 7,
data_2: 8
}],
}),
]
);
Ok(())
}
/// Routes announced before any session exists are replayed when a session
/// reaches Established.
#[tokio::test]
async fn test_routes_replayed_on_session_up() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
server
.local_rib
.announce(route("10.100.1.0/24", LocalRouteAttrs::default()))
.await?;
server
.local_rib
.announce(route("10.100.2.0/24", LocalRouteAttrs::default()))
.await?;
let mut peer = established_peer(&server).await?;
let mut seen = vec![next_update(&mut peer).await?, next_update(&mut peer).await?]
.into_iter()
.flat_map(|u| u.announced_routes)
.map(|p| p.to_string())
.collect::<Vec<_>>();
seen.sort();
assert_eq!(seen, vec!["10.100.1.0/24", "10.100.2.0/24"]);
Ok(())
}
/// An announcement is fanned out to every established session.
#[tokio::test]
async fn test_announce_reaches_all_sessions() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut a = established_peer_with_id(&server, Ipv4Addr::new(10, 0, 0, 1)).await?;
let mut b = established_peer_with_id(&server, Ipv4Addr::new(10, 0, 0, 2)).await?;
wait_for_sessions(&server, 2).await?;
server
.local_rib
.announce(route("10.100.1.0/24", LocalRouteAttrs::default()))
.await?;
for peer in [&mut a, &mut b] {
let update = next_update(peer).await?;
assert_eq!(update.announced_routes, vec![prefix("10.100.1.0/24")]);
}
Ok(())
}
#[tokio::test]
async fn test_withdraw_sends_withdrawn_route() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = established_peer(&server).await?;
wait_for_sessions(&server, 1).await?;
server
.local_rib
.announce(route("10.100.1.0/24", LocalRouteAttrs::default()))
.await?;
let _ = next_update(&mut peer).await?;
server.local_rib.withdraw(prefix("10.100.1.0/24")).await?;
let update = next_update(&mut peer).await?;
assert_eq!(update.withdrawn_routes, vec![prefix("10.100.1.0/24")]);
assert!(update.announced_routes.is_empty());
assert!(update.path_attributes.is_empty());
Ok(())
}
/// Re-announcing with new attributes sends a fresh UPDATE (implicit replace).
#[tokio::test]
async fn test_reannounce_with_prepend_and_med() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = established_peer(&server).await?;
wait_for_sessions(&server, 1).await?;
server
.local_rib
.announce(route("10.100.1.0/24", LocalRouteAttrs::default()))
.await?;
let first = next_update(&mut peer).await?;
assert_eq!(
first.path_attributes[1],
AsPathAttribute::from_asns(vec![LOCAL_ASN])
);
server
.local_rib
.announce(route(
"10.100.1.0/24",
LocalRouteAttrs {
prepend: 3,
med: Some(42),
..LocalRouteAttrs::default()
},
))
.await?;
let second = next_update(&mut peer).await?;
assert_eq!(second.announced_routes, vec![prefix("10.100.1.0/24")]);
assert_eq!(
second.path_attributes[1],
AsPathAttribute::from_asns(vec![LOCAL_ASN; 4])
);
assert!(
second
.path_attributes
.iter()
.any(|a| matches!(a, PathAttribute::MultiExitDisc(m) if m.0 == 42))
);
Ok(())
}
/// The session stays healthy (keepalives keep flowing) while UPDATEs are sent.
#[tokio::test]
async fn test_keepalives_continue_alongside_updates() -> Result<()> {
let server = TestServer::start_with_hold_time(LOCAL_ASN, 3).await?;
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 3)
.await?;
wait_for_sessions(&server, 1).await?;
server
.local_rib
.announce(route("10.100.1.0/24", LocalRouteAttrs::default()))
.await?;
let mut saw_update = false;
let mut saw_keepalive = false;
tokio::time::timeout(TIMEOUT, async {
while !(saw_update && saw_keepalive) {
match peer.recv().await? {
bgp_packet::message::Message::Update(_) => saw_update = true,
bgp_packet::message::Message::KeepAlive => saw_keepalive = true,
other => eyre::bail!("unexpected {other:?}"),
}
}
Ok::<_, eyre::Error>(())
})
.await??;
Ok(())
}
/// Routes learned from a peer disappear from the Adj-RIB-In when its
/// session ends, with a Withdrawn event for each.
#[tokio::test]
async fn test_received_routes_removed_when_session_closes() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut rib_events = server.route_changes();
let mut session_events = server.registry.subscribe();
let mut peer = established_peer(&server).await?;
wait_for_sessions(&server, 1).await?;
peer.send(&bgp_packet::message::Message::Update(
bgp_packet::message::UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![PEER_ASN]),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(10, 0, 0, 1))),
],
announced_routes: vec![
prefix("203.0.113.0/24").into(),
prefix("198.51.100.0/24").into(),
],
},
))
.await?;
for _ in 0..2 {
let ev = tokio::time::timeout(TIMEOUT, rib_events.recv()).await??;
assert!(matches!(ev, RouteChange::Announced { .. }), "{ev:?}");
}
assert_eq!(server.received_routes().len(), 2);
drop(peer);
let mut withdrawn = vec![];
for _ in 0..2 {
match tokio::time::timeout(TIMEOUT, rib_events.recv()).await?? {
RouteChange::Withdrawn { prefix, .. } => withdrawn.push(prefix.to_string()),
other => panic!("expected Withdrawn, got {other:?}"),
}
}
withdrawn.sort();
assert_eq!(withdrawn, vec!["198.51.100.0/24", "203.0.113.0/24"]);
assert!(server.received_routes().is_empty());
// Withdrawals precede the Down event.
let _up = tokio::time::timeout(TIMEOUT, session_events.recv()).await??;
let down = tokio::time::timeout(TIMEOUT, session_events.recv()).await??;
assert!(matches!(down, SessionEvent::Down(_)), "{down:?}");
Ok(())
}
// ---------------------------------------------------------------------------
// Route refresh and shutdown
// ---------------------------------------------------------------------------
/// ROUTE-REFRESH for IPv4 unicast exactly as a router sends it (RFC 2918):
/// 19-byte header plus AFI, reserved, SAFI.
#[rustfmt::skip]
const ROUTE_REFRESH_IPV4: &[u8] = &[
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x17, 0x05,
0x00, 0x01, 0x00, 0x01,
];
/// A peer whose import policy changed asks for our routes again; it must get
/// every local route re-sent, and the session must stay up.
#[tokio::test]
async fn test_route_refresh_resends_local_routes() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = established_peer(&server).await?;
wait_for_sessions(&server, 1).await?;
server
.local_rib
.announce(route("10.100.1.0/24", LocalRouteAttrs::default()))
.await?;
server
.local_rib
.announce(route("10.100.2.0/24", LocalRouteAttrs::default()))
.await?;
let _ = next_update(&mut peer).await?;
let _ = next_update(&mut peer).await?;
peer.send_raw(ROUTE_REFRESH_IPV4).await?;
let mut resent = vec![next_update(&mut peer).await?, next_update(&mut peer).await?]
.into_iter()
.flat_map(|u| u.announced_routes)
.map(|p| p.to_string())
.collect::<Vec<_>>();
resent.sort();
assert_eq!(resent, vec!["10.100.1.0/24", "10.100.2.0/24"]);
assert_eq!(
server.registry.list().len(),
1,
"session must survive a route refresh"
);
Ok(())
}
/// Shutting down tells each peer why (Cease / Administrative Shutdown) and
/// closes the sessions, instead of leaving dead TCP connections behind.
#[tokio::test]
async fn test_shutdown_sends_cease_to_every_peer() -> Result<()> {
use bgp_packet::errors::{BgpError, CeaseSubcode};
use bgp_packet::message::Message;
let server = TestServer::start(LOCAL_ASN).await?;
let mut a = established_peer_with_id(&server, Ipv4Addr::new(10, 0, 0, 1)).await?;
let mut b = established_peer_with_id(&server, Ipv4Addr::new(10, 0, 0, 2)).await?;
wait_for_sessions(&server, 2).await?;
let left = server.registry.shutdown(Duration::from_secs(2)).await;
assert_eq!(left, 0, "sessions still open after shutdown");
assert!(server.registry.is_shutting_down());
for peer in [&mut a, &mut b] {
let notif = tokio::time::timeout(TIMEOUT, async {
loop {
match peer.recv().await? {
Message::Notification(n) => return Ok::<_, eyre::Error>(n),
Message::KeepAlive => {}
other => eyre::bail!("unexpected {other:?}"),
}
}
})
.await??;
assert!(
matches!(notif.error, BgpError::Cease(CeaseSubcode::AdminShutdown)),
"{notif:?}"
);
assert!(peer.is_closed().await);
}
Ok(())
}
// ---------------------------------------------------------------------------
// Routes from the configuration
// ---------------------------------------------------------------------------
/// Routes listed under `originate` are in the table before any peer
/// connects, and each session is sent them when it comes up.
#[tokio::test]
async fn test_configured_routes_are_announced_to_every_new_session() -> Result<()> {
let attrs = LocalRouteAttrs {
med: Some(7),
communities: vec![(65000, 100)],
..Default::default()
};
let configured = vec![
route("10.100.1.0/24", attrs),
route("10.100.2.0/24", LocalRouteAttrs::default()),
];
let server = TestServer::start_with(LOCAL_ASN, |c| c.originate = configured).await?;
assert_eq!(server.local_rib.snapshot().await.unwrap().len(), 2);
for router_id in [Ipv4Addr::new(10, 0, 0, 1), Ipv4Addr::new(10, 0, 0, 2)] {
let mut peer = established_peer_with_id(&server, router_id).await?;
let mut seen = [next_update(&mut peer).await?, next_update(&mut peer).await?];
seen.sort_by_key(|u| u.announced_routes[0].to_string());
assert_eq!(seen[0].announced_routes, vec![prefix("10.100.1.0/24")]);
assert!(
seen[0]
.path_attributes
.contains(&PathAttribute::Communities(CommunitiesPathAttribute(vec![
(65000, 100)
])))
);
assert_eq!(seen[1].announced_routes, vec![prefix("10.100.2.0/24")]);
}
Ok(())
}
/// A peer in our own AS is sent a route we originate with an empty AS path
/// and a LOCAL_PREF, whatever prepending is configured: our AS number goes
/// on the path only as the route leaves the AS. It used to be sent as to
/// an external peer, which an internal one refuses.
#[tokio::test]
async fn test_a_peer_in_our_own_as_gets_an_empty_path_and_a_local_pref() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(LOCAL_ASN, Ipv4Addr::new(10, 0, 0, 1), 90)
.await?;
// The peer reads as a router in the server's AS does: one outside it
// would drop the LOCAL_PREF.
peer.ctx.ibgp = true;
wait_for_sessions(&server, 1).await?;
assert!(server.registry.list()[0].ibgp);
server
.local_rib
.announce(route(
"10.100.1.0/24",
LocalRouteAttrs {
prepend: 2,
..LocalRouteAttrs::default()
},
))
.await?;
let update = next_update(&mut peer).await?;
assert_eq!(update.announced_routes, vec![prefix("10.100.1.0/24")]);
assert!(
update
.path_attributes
.contains(&PathAttribute::AsPath(AsPathAttribute { segments: vec![] })),
"{:?}",
update.path_attributes
);
assert!(
update
.path_attributes
.contains(&PathAttribute::LocalPref(LocalPrefPathAttribute(100))),
"{:?}",
update.path_attributes
);
Ok(())
}
@@ -2,17 +2,41 @@ use std::net::Ipv4Addr;
use std::time::Duration;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::RouteChange;
use bgp_integration_tests::test_server::TestServer;
use bgp_packet::errors::{BgpError, CeaseSubcode};
use bgp_packet::errors::{BgpError, CeaseSubcode, FsmSubcode, MsgHeaderSubcode, UpdateMsgSubcode};
use bgp_packet::message::{
Message, MpReachNlriPathAttribute, MpUnreachNlriPathAttribute, NlriNextHop,
NotificationMessage, PathAttribute, UpdateMessage,
AsPathAttribute, Message, MpReachNlriPathAttribute, MpUnreachNlriPathAttribute,
NextHopPathAttribute, NlriNextHop, NotificationMessage, OriginPathAttribute, PathAttribute,
UpdateMessage,
};
use bgp_primitives::constants::{AddressFamilyId, SubsequentAfi};
use bgp_primitives::constants::{AfiSafi, IpVersion};
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::RibEvent;
use eyre::Result;
const TIMEOUT: Duration = Duration::from_secs(5);
/// The attributes every announcement of IPv4 routes must carry. An UPDATE
/// without them has its routes withdrawn instead (RFC 7606).
fn mandatory_attributes() -> Vec<PathAttribute> {
let mut attributes = with_origin_and_as_path(PathAttribute::NextHop(NextHopPathAttribute(
Ipv4Addr::new(192, 0, 2, 1),
)));
// In order of type code: ORIGIN, AS_PATH, NEXT_HOP.
attributes.rotate_left(1);
attributes
}
/// `first` followed by the attributes every announcement must carry.
/// Routes in MP_REACH_NLRI have their next hop there.
fn with_origin_and_as_path(first: PathAttribute) -> Vec<PathAttribute> {
vec![
first,
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![65001]),
]
}
// ---------------------------------------------------------------------------
// Connectivity
// ---------------------------------------------------------------------------
@@ -74,6 +98,147 @@ async fn test_bad_marker_does_not_crash_server() -> Result<()> {
Ok(())
}
/// Reads until a NOTIFICATION arrives, skipping KEEPALIVEs.
async fn expect_notification(peer: &mut TestPeer) -> Result<NotificationMessage> {
tokio::time::timeout(TIMEOUT, async {
loop {
match peer.recv().await? {
Message::Notification(n) => return Ok(n),
Message::KeepAlive => {}
other => eyre::bail!("expected NOTIFICATION, got {other:?}"),
}
}
})
.await?
}
/// A header with the given length and type, followed by `body`.
fn framed(length: u16, msg_type: u8, body: &[u8]) -> Vec<u8> {
let mut buf = vec![0xFF; 16];
buf.extend_from_slice(&length.to_be_bytes());
buf.push(msg_type);
buf.extend_from_slice(body);
buf
}
/// What the peer sent decides the NOTIFICATION it gets back (RFC 4271 §6),
/// in every state, and the session ends.
#[tokio::test]
async fn test_malformed_messages_are_answered_with_the_matching_notification() -> Result<()> {
let server = TestServer::start(65000).await?;
let header = |subcode| BgpError::MsgHeader(subcode);
let mut bad_marker = framed(19, 4, &[]);
bad_marker[0] = 0;
let cases: Vec<(&str, Vec<u8>, BgpError, Vec<u8>)> = vec![
(
"length beyond the maximum",
framed(5000, 2, &[]),
header(MsgHeaderSubcode::BadMessageLength),
5000u16.to_be_bytes().to_vec(),
),
(
"length below a header",
framed(5, 4, &[]),
header(MsgHeaderSubcode::BadMessageLength),
vec![0, 5],
),
(
"unknown message type",
framed(19, 9, &[]),
header(MsgHeaderSubcode::BadMessageType),
vec![9],
),
(
"marker not all ones",
bad_marker,
header(MsgHeaderSubcode::ConnNotSynchronized),
vec![],
),
(
"UPDATE with attributes running past its end",
framed(23, 2, &[0, 0, 0, 40]),
BgpError::UpdateMsg(UpdateMsgSubcode::MalformedAttrs),
vec![],
),
];
for (i, (what, bytes, error, data)) in cases.into_iter().enumerate() {
// Established: a distinct router ID each time, so that a session
// still closing does not collide with the next one.
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(65001, Ipv4Addr::new(10, 0, 1, i as u8 + 1), 90)
.await?;
peer.send_raw(&bytes).await?;
let notification = expect_notification(&mut peer).await?;
assert_eq!(
(notification.error, notification.data),
(error.clone(), data.clone()),
"{what}, Established"
);
// OpenSent: the same bytes instead of an OPEN.
let mut peer = TestPeer::connect(server.addr).await?;
let _ = peer.recv_open().await?;
peer.send_raw(&bytes).await?;
let notification = expect_notification(&mut peer).await?;
assert_eq!(
(notification.error, notification.data),
(error, data),
"{what}, OpenSent"
);
}
// A well-formed message at the wrong time is an FSM error, not a
// message error.
let mut peer = TestPeer::connect(server.addr).await?;
let _ = peer.recv_open().await?;
peer.send(&Message::KeepAlive).await?;
let notification = expect_notification(&mut peer).await?;
assert_eq!(
notification.error,
BgpError::FsmError(FsmSubcode::UnexpectedOpenSent)
);
Ok(())
}
/// A message may arrive in pieces, with the server's own timers firing in
/// between. The server once threw away the piece it had already read when
/// that happened, so the message was silently lost (and the stream was only
/// back in step if the remainder happened to be thrown away as well).
#[tokio::test]
async fn test_message_split_across_a_keepalive_tick_is_still_one_message() -> Result<()> {
// Hold time 3 s: the server sends a KEEPALIVE every second.
let server = TestServer::start_with_hold_time(65000, 3).await?;
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 3).await?;
let prefix = IpPrefix::new(IpVersion::Ipv4, vec![10], 8)?;
let mut update = bytes::BytesMut::new();
Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: mandatory_attributes(),
announced_routes: vec![prefix.into()],
})
.to_wire(&peer.ctx, &mut update)?;
// The first half, a pause long enough for the server's keepalive timer
// to fire, then the rest.
peer.send_raw(&update[..10]).await?;
tokio::time::sleep(Duration::from_millis(1300)).await;
peer.send_raw(&update[10..]).await?;
tokio::time::timeout(TIMEOUT, async {
while !server.received_routes().iter().any(|e| e.prefix == prefix) {
tokio::time::sleep(Duration::from_millis(20)).await;
}
})
.await
.map_err(|_| eyre::eyre!("the split UPDATE never reached the RIB"))?;
assert_eq!(server.registry.list().len(), 1, "session was dropped");
Ok(())
}
// ---------------------------------------------------------------------------
// OpenConfirm → Established
// ---------------------------------------------------------------------------
@@ -173,9 +338,9 @@ async fn test_server_accepts_update() -> Result<()> {
let update = Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![],
path_attributes: mandatory_attributes(),
// 10.0.0.0/8 — one byte of prefix for a /8.
announced_routes: vec![IpPrefix::new(AddressFamilyId::Ipv4, vec![10], 8)?],
announced_routes: vec![IpPrefix::new(IpVersion::Ipv4, vec![10], 8)?.into()],
});
peer.send(&update).await?;
@@ -193,15 +358,16 @@ async fn test_server_accepts_ipv6_update() -> Result<()> {
.await?;
// 2001:db8::/32
let prefix = IpPrefix::new(AddressFamilyId::Ipv6, vec![0x20, 0x01, 0x0d, 0xb8], 32)?;
let prefix = IpPrefix::new(IpVersion::Ipv6, vec![0x20, 0x01, 0x0d, 0xb8], 32)?;
let update = Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
afi: AddressFamilyId::Ipv6,
safi: SubsequentAfi::Unicast,
next_hop: NlriNextHop::Ipv6("2001:db8::1".parse().unwrap()),
prefixes: vec![prefix],
})],
path_attributes: with_origin_and_as_path(PathAttribute::MpReachNlri(
MpReachNlriPathAttribute {
family: AfiSafi::IPV6_UNICAST,
next_hop: NlriNextHop::Ipv6("2001:db8::1".parse().unwrap()),
prefixes: vec![prefix.into()],
},
)),
announced_routes: vec![],
});
peer.send(&update).await?;
@@ -219,26 +385,27 @@ async fn test_server_accepts_ipv6_update() -> Result<()> {
#[tokio::test]
async fn test_rib_records_ipv4_announce() -> Result<()> {
let server = TestServer::start(65000).await?;
let mut events = server.adj_rib_in.subscribe();
let mut events = server.route_changes();
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90).await?;
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
.await?;
let prefix = IpPrefix::new(AddressFamilyId::Ipv4, vec![10], 8)?;
let prefix = IpPrefix::new(IpVersion::Ipv4, vec![10], 8)?;
peer.send(&Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![],
announced_routes: vec![prefix.clone()],
path_attributes: mandatory_attributes(),
announced_routes: vec![prefix.into()],
}))
.await?;
let event = tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
assert!(
matches!(&event, RibEvent::Announced { prefix: p, .. } if p == &prefix),
matches!(&event, RouteChange::Announced { prefix: p, .. } if p == &prefix),
"Expected Announced for 10.0.0.0/8, got {event:?}",
);
let snapshot = server.adj_rib_in.snapshot().await;
let snapshot = server.received_routes();
assert!(
snapshot.iter().any(|e| e.prefix == prefix),
"10.0.0.0/8 missing from RIB snapshot",
@@ -250,25 +417,26 @@ async fn test_rib_records_ipv4_announce() -> Result<()> {
#[tokio::test]
async fn test_rib_records_ipv4_withdraw() -> Result<()> {
let server = TestServer::start(65000).await?;
let mut events = server.adj_rib_in.subscribe();
let mut events = server.route_changes();
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90).await?;
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
.await?;
let prefix = IpPrefix::new(AddressFamilyId::Ipv4, vec![10], 8)?;
let prefix = IpPrefix::new(IpVersion::Ipv4, vec![10], 8)?;
// Announce first.
peer.send(&Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![],
announced_routes: vec![prefix.clone()],
path_attributes: mandatory_attributes(),
announced_routes: vec![prefix.into()],
}))
.await?;
tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
// Now withdraw.
peer.send(&Message::Update(UpdateMessage {
withdrawn_routes: vec![prefix.clone()],
withdrawn_routes: vec![prefix.into()],
path_attributes: vec![],
announced_routes: vec![],
}))
@@ -276,11 +444,11 @@ async fn test_rib_records_ipv4_withdraw() -> Result<()> {
let event = tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
assert!(
matches!(&event, RibEvent::Withdrawn { prefix: p, .. } if p == &prefix),
matches!(&event, RouteChange::Withdrawn { prefix: p, .. } if p == &prefix),
"Expected Withdrawn for 10.0.0.0/8, got {event:?}",
);
let snapshot = server.adj_rib_in.snapshot().await;
let snapshot = server.received_routes();
assert!(
!snapshot.iter().any(|e| e.prefix == prefix),
"10.0.0.0/8 should have been removed from RIB snapshot",
@@ -292,31 +460,33 @@ async fn test_rib_records_ipv4_withdraw() -> Result<()> {
#[tokio::test]
async fn test_rib_records_ipv6_announce() -> Result<()> {
let server = TestServer::start(65000).await?;
let mut events = server.adj_rib_in.subscribe();
let mut events = server.route_changes();
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90).await?;
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
.await?;
let prefix = IpPrefix::new(AddressFamilyId::Ipv6, vec![0x20, 0x01, 0x0d, 0xb8], 32)?;
let prefix = IpPrefix::new(IpVersion::Ipv6, vec![0x20, 0x01, 0x0d, 0xb8], 32)?;
peer.send(&Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
afi: AddressFamilyId::Ipv6,
safi: SubsequentAfi::Unicast,
next_hop: NlriNextHop::Ipv6("2001:db8::1".parse().unwrap()),
prefixes: vec![prefix.clone()],
})],
path_attributes: with_origin_and_as_path(PathAttribute::MpReachNlri(
MpReachNlriPathAttribute {
family: AfiSafi::IPV6_UNICAST,
next_hop: NlriNextHop::Ipv6("2001:db8::1".parse().unwrap()),
prefixes: vec![prefix.into()],
},
)),
announced_routes: vec![],
}))
.await?;
let event = tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
assert!(
matches!(&event, RibEvent::Announced { prefix: p, .. } if p == &prefix),
matches!(&event, RouteChange::Announced { prefix: p, .. } if p == &prefix),
"Expected Announced for 2001:db8::/32, got {event:?}",
);
let snapshot = server.adj_rib_in.snapshot().await;
let snapshot = server.received_routes();
assert!(
snapshot.iter().any(|e| e.prefix == prefix),
"2001:db8::/32 missing from RIB snapshot",
@@ -328,22 +498,24 @@ async fn test_rib_records_ipv6_announce() -> Result<()> {
#[tokio::test]
async fn test_rib_records_ipv6_withdraw() -> Result<()> {
let server = TestServer::start(65000).await?;
let mut events = server.adj_rib_in.subscribe();
let mut events = server.route_changes();
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90).await?;
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
.await?;
let prefix = IpPrefix::new(AddressFamilyId::Ipv6, vec![0x20, 0x01, 0x0d, 0xb8], 32)?;
let prefix = IpPrefix::new(IpVersion::Ipv6, vec![0x20, 0x01, 0x0d, 0xb8], 32)?;
// Announce first.
peer.send(&Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
afi: AddressFamilyId::Ipv6,
safi: SubsequentAfi::Unicast,
next_hop: NlriNextHop::Ipv6("2001:db8::1".parse().unwrap()),
prefixes: vec![prefix.clone()],
})],
path_attributes: with_origin_and_as_path(PathAttribute::MpReachNlri(
MpReachNlriPathAttribute {
family: AfiSafi::IPV6_UNICAST,
next_hop: NlriNextHop::Ipv6("2001:db8::1".parse().unwrap()),
prefixes: vec![prefix.into()],
},
)),
announced_routes: vec![],
}))
.await?;
@@ -353,9 +525,8 @@ async fn test_rib_records_ipv6_withdraw() -> Result<()> {
peer.send(&Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![PathAttribute::MpUnreachNlri(MpUnreachNlriPathAttribute {
afi: AddressFamilyId::Ipv6,
safi: SubsequentAfi::Unicast,
prefixes: vec![prefix.clone()],
family: AfiSafi::IPV6_UNICAST,
prefixes: vec![prefix.into()],
})],
announced_routes: vec![],
}))
@@ -363,11 +534,11 @@ async fn test_rib_records_ipv6_withdraw() -> Result<()> {
let event = tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
assert!(
matches!(&event, RibEvent::Withdrawn { prefix: p, .. } if p == &prefix),
matches!(&event, RouteChange::Withdrawn { prefix: p, .. } if p == &prefix),
"Expected Withdrawn for 2001:db8::/32, got {event:?}",
);
let snapshot = server.adj_rib_in.snapshot().await;
let snapshot = server.received_routes();
assert!(
!snapshot.iter().any(|e| e.prefix == prefix),
"2001:db8::/32 should have been removed from RIB snapshot",
@@ -0,0 +1,239 @@
//! End-to-end tests of route selection: routes for one prefix from several
//! peers, which of them the server chooses, and how that is shown.
use std::net::Ipv4Addr;
use std::time::Duration;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::{RouteChange, TestServer};
use bgp_packet::message::{
AsPathAttribute, Message, NextHopPathAttribute, OriginPathAttribute, PathAttribute,
UpdateMessage,
};
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
use eyre::Result;
use serde_json::Value;
const LOCAL_ASN: u32 = 65000;
const TIMEOUT: Duration = Duration::from_secs(5);
fn prefix(s: &str) -> IpPrefix {
IpPrefix::try_from(s).unwrap()
}
/// A session from the peer numbered `number`, in AS 65000 + `number`.
async fn peer(server: &TestServer, number: u8) -> Result<TestPeer> {
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(
LOCAL_ASN + u32::from(number),
Ipv4Addr::new(10, 0, 0, number),
90,
)
.await?;
Ok(peer)
}
fn announcement(prefixes: &[IpPrefix], as_path: &[u32]) -> Message {
Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(as_path.to_vec()),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
],
announced_routes: Nlri::plain(prefixes.iter().copied()),
})
}
fn withdrawal(prefixes: &[IpPrefix]) -> Message {
Message::Update(UpdateMessage {
withdrawn_routes: Nlri::plain(prefixes.iter().copied()),
path_attributes: vec![],
announced_routes: vec![],
})
}
/// Waits until the table holds `routes` routes.
async fn wait_for_routes(server: &TestServer, routes: usize) -> Result<()> {
tokio::time::timeout(TIMEOUT, async {
while server.rib.len() != routes {
tokio::time::sleep(Duration::from_millis(5)).await;
}
})
.await?;
Ok(())
}
/// The AS path of the route the server holds as best for `prefix`.
fn best_path(server: &TestServer, prefix: &IpPrefix) -> Option<Vec<u32>> {
let best = server
.received_routes()
.into_iter()
.find(|route| route.prefix == *prefix && route.best)?;
best.attributes
.iter()
.find_map(|attribute| match attribute {
PathAttribute::AsPath(path) => Some(path.segments[0].path.clone()),
_ => None,
})
}
async fn get_json(client: &reqwest::Client, url: &str) -> Result<Value> {
Ok(client
.get(url)
.send()
.await?
.error_for_status()?
.json()
.await?)
}
/// Two peers offer the same prefix. The route with the shorter AS path is
/// chosen; when it goes, the other is.
#[tokio::test]
async fn test_the_best_route_is_chosen_and_replaced_when_it_goes() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let (base, _http) = server.serve_http().await?;
let client = reqwest::Client::new();
let mut changes = server.route_changes();
let p = prefix("203.0.113.0/24");
let mut far = peer(&server, 1).await?;
far.send(&announcement(&[p], &[65001, 64999, 64998]))
.await?;
wait_for_routes(&server, 1).await?;
assert_eq!(best_path(&server, &p), Some(vec![65001, 64999, 64998]));
let mut near = peer(&server, 2).await?;
near.send(&announcement(&[p], &[65002, 64998])).await?;
wait_for_routes(&server, 2).await?;
assert_eq!(best_path(&server, &p), Some(vec![65002, 64998]));
// Both routes are kept and shown; one is marked.
let status = get_json(&client, &format!("{base}/api/status")).await?;
assert_eq!(status["received_routes"], 2);
assert_eq!(status["received_prefixes"], 1);
let routes = get_json(&client, &format!("{base}/api/routes/received")).await?;
let best: Vec<&Value> = routes
.as_array()
.unwrap()
.iter()
.filter(|route| route["best"] == true)
.collect();
assert_eq!(routes.as_array().unwrap().len(), 2);
assert_eq!(best.len(), 1);
assert_eq!(best[0]["peer_asn"], 65002);
// The session with the best route ends: the other route takes over.
drop(near);
wait_for_routes(&server, 1).await?;
assert_eq!(best_path(&server, &p), Some(vec![65001, 64999, 64998]));
far.send(&withdrawal(&[p])).await?;
wait_for_routes(&server, 0).await?;
assert_eq!(best_path(&server, &p), None);
// A follower of the best routes is told of each change: the first
// route, the better one, back to the first, none.
let mut seen = Vec::new();
while seen.last() != Some(&RouteChange::Withdrawn { prefix: p }) {
seen.push(tokio::time::timeout(TIMEOUT, changes.recv()).await??);
}
assert!(
seen.iter()
.all(|change| matches!(change, RouteChange::Announced { prefix, .. } | RouteChange::Withdrawn { prefix } if *prefix == p)),
"{seen:?}"
);
Ok(())
}
/// The event stream reports changes of best route, and only those.
#[tokio::test]
async fn test_changes_of_best_route_are_on_the_event_stream() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let (base, _http) = server.serve_http().await?;
let p = prefix("203.0.113.0/24");
// A route that is there before the client connects is not an event.
let mut first = peer(&server, 1).await?;
first
.send(&announcement(&[prefix("198.51.100.0/24")], &[65001]))
.await?;
wait_for_routes(&server, 1).await?;
let mut stream = reqwest::Client::new()
.get(format!("{base}/api/events"))
.send()
.await?
.error_for_status()?;
first.send(&announcement(&[p], &[65001, 64998])).await?;
wait_for_routes(&server, 2).await?;
first.send(&withdrawal(&[p])).await?;
wait_for_routes(&server, 1).await?;
let mut text = String::new();
let mut events: Vec<Value> = Vec::new();
tokio::time::timeout(TIMEOUT, async {
while !events.iter().any(|event| event["type"] == "best_withdrawn") {
let chunk = stream
.chunk()
.await?
.ok_or_else(|| eyre::eyre!("stream ended"))?;
text.push_str(std::str::from_utf8(&chunk)?);
events = text
.lines()
.filter_map(|line| line.strip_prefix("data:"))
.filter_map(|data| serde_json::from_str(data.trim()).ok())
.collect();
}
Ok::<_, eyre::Error>(())
})
.await??;
let about_routes: Vec<&Value> = events
.iter()
.filter(|event| {
event["type"]
.as_str()
.is_some_and(|t| t.starts_with("best_"))
})
.collect();
// The announcement and the withdrawal may have been read as one change
// or as two; either way nothing is said of the route that was there
// from the start, and the last word is that the prefix is gone.
assert!(about_routes.len() <= 2, "{about_routes:?}");
for event in &about_routes {
let named = event["prefix"]
.as_str()
.or(event["route"]["prefix"].as_str());
assert_eq!(named, Some("203.0.113.0/24"), "{event}");
}
assert_eq!(about_routes.last().unwrap()["type"], "best_withdrawn");
Ok(())
}
/// The table can be far too large to send whole: the API gives a part of
/// it, and says how much there is.
#[tokio::test]
async fn test_received_routes_are_returned_up_to_a_limit() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let (base, _http) = server.serve_http().await?;
let client = reqwest::Client::new();
let prefixes: Vec<IpPrefix> = (0..40).map(|n| prefix(&format!("10.{n}.0.0/16"))).collect();
let mut peer = peer(&server, 1).await?;
peer.send(&announcement(&prefixes, &[65001])).await?;
wait_for_routes(&server, 40).await?;
let count = |routes: Value| routes.as_array().unwrap().len();
let url = format!("{base}/api/routes/received");
assert_eq!(count(get_json(&client, &url).await?), 40);
assert_eq!(
count(get_json(&client, &format!("{url}?limit=7")).await?),
7
);
let status = get_json(&client, &format!("{base}/api/status")).await?;
assert_eq!(status["received_routes"], 40);
Ok(())
}
+315
View File
@@ -0,0 +1,315 @@
//! Connection collision detection (RFC 4271 §6.8) against a live server.
//!
//! The test server's BGP Identifier is 127.0.0.1. A test peer picks an
//! identifier above or below that to steer which connection must survive.
use std::net::{Ipv4Addr, SocketAddr};
use std::time::Duration;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::TestServer;
use bgp_packet::errors::{BgpError, CeaseSubcode, OpenMsgSubcode};
use bgp_packet::message::{
AsPathAttribute, Message, NextHopPathAttribute, NotificationMessage, OriginPathAttribute,
PathAttribute, UpdateMessage,
};
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::{LocalRoute, LocalRouteAttrs};
use bgp_server::collision::Direction;
use bgp_server::config::PeerConfig;
use bgp_server::family::FamilySet;
use eyre::Result;
use tokio::net::TcpListener;
const LOCAL_ASN: u32 = 65000;
const PEER_ASN: u32 = 65001;
const TIMEOUT: Duration = Duration::from_secs(5);
/// Above the server's 127.0.0.1.
const HIGH_ID: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 1);
/// Below the server's 127.0.0.1.
const LOW_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1);
async fn server_dialing(listener: &TcpListener) -> Result<TestServer> {
let endpoint: SocketAddr = listener.local_addr()?;
TestServer::start_with(LOCAL_ASN, |c| {
c.connect_retry = Duration::from_millis(200);
c.peers = vec![PeerConfig {
name: "r1".into(),
endpoint,
remote_asn: PEER_ASN,
hold_time_secs: 90,
local_address: None,
families: FamilySet::supported(),
add_path: None,
}];
})
.await
}
/// Exchanges OPENs only, leaving the server's side in OpenConfirm (it has
/// sent its KEEPALIVE and is waiting for ours).
async fn open_only(peer: &mut TestPeer, router_id: Ipv4Addr) -> Result<()> {
peer.send_open(PEER_ASN, router_id, 90).await?;
peer.recv_open().await?;
Ok(())
}
/// Finishes the handshake begun by [`open_only`].
async fn confirm(peer: &mut TestPeer) -> Result<()> {
match tokio::time::timeout(TIMEOUT, peer.recv()).await?? {
Message::KeepAlive => {}
other => eyre::bail!("expected KEEPALIVE, got {other:?}"),
}
peer.send(&Message::KeepAlive).await
}
/// Reads until a NOTIFICATION arrives, skipping KEEPALIVEs.
async fn expect_notification(peer: &mut TestPeer) -> Result<NotificationMessage> {
tokio::time::timeout(TIMEOUT, async {
loop {
match peer.recv().await? {
Message::Notification(n) => return Ok(n),
Message::KeepAlive => {}
other => eyre::bail!("expected NOTIFICATION, got {other:?}"),
}
}
})
.await?
}
fn is_collision_cease(n: &NotificationMessage) -> bool {
matches!(
n.error,
BgpError::Cease(CeaseSubcode::ConnCollisionResolution)
)
}
async fn wait_for_sessions(server: &TestServer, n: usize) -> Result<()> {
tokio::time::timeout(TIMEOUT, async {
while server.registry.list().len() != n {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
Ok(())
}
/// Peer has the higher identifier, so the connection the *peer* initiated
/// must survive: the server closes its own outbound connection.
#[tokio::test]
async fn test_simultaneous_open_peer_higher_keeps_inbound() -> Result<()> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let server = server_dialing(&listener).await?;
// Server's outbound connection reaches OpenConfirm.
let (stream, _) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
let mut dialled = TestPeer::from_stream(stream);
open_only(&mut dialled, HIGH_ID).await?;
// The peer now also dials the server.
let mut inbound = TestPeer::connect(server.addr).await?;
open_only(&mut inbound, HIGH_ID).await?;
let notif = expect_notification(&mut dialled).await?;
assert!(is_collision_cease(&notif), "{notif:?}");
assert!(dialled.is_closed().await);
confirm(&mut inbound).await?;
wait_for_sessions(&server, 1).await?;
let session = server.registry.list().remove(0);
assert_eq!(session.direction, Direction::Inbound);
assert_eq!(session.name.as_deref(), Some("r1"));
// The survivor is fully functional.
server
.local_rib
.announce(LocalRoute::new(
IpPrefix::try_from("10.100.1.0/24")?,
LocalRouteAttrs::default(),
))
.await?;
let update = tokio::time::timeout(TIMEOUT, inbound.recv_update()).await??;
assert_eq!(update.announced_routes[0].to_string(), "10.100.1.0/24");
// The dialer notices the surviving session and does not open another.
let extra = tokio::time::timeout(Duration::from_millis(700), listener.accept()).await;
assert!(
extra.is_err(),
"dialer opened a second connection despite an established session"
);
Ok(())
}
/// Peer has the lower identifier, so the connection the *server* initiated
/// must survive: the peer's inbound connection is refused.
#[tokio::test]
async fn test_simultaneous_open_peer_lower_keeps_outbound() -> Result<()> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let server = server_dialing(&listener).await?;
let (stream, _) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
let mut dialled = TestPeer::from_stream(stream);
open_only(&mut dialled, LOW_ID).await?;
let mut inbound = TestPeer::connect(server.addr).await?;
open_only(&mut inbound, LOW_ID).await?;
let notif = expect_notification(&mut inbound).await?;
assert!(is_collision_cease(&notif), "{notif:?}");
assert!(inbound.is_closed().await);
confirm(&mut dialled).await?;
wait_for_sessions(&server, 1).await?;
assert_eq!(server.registry.list()[0].direction, Direction::Outbound);
Ok(())
}
/// The order in which the two OPENs are processed must not change the
/// outcome: here the inbound connection reaches OpenConfirm first.
#[tokio::test]
async fn test_outcome_is_independent_of_arrival_order() -> Result<()> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let server = server_dialing(&listener).await?;
let (stream, _) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
let mut dialled = TestPeer::from_stream(stream);
// Inbound first this time; peer is lower so outbound must still win.
let mut inbound = TestPeer::connect(server.addr).await?;
open_only(&mut inbound, LOW_ID).await?;
open_only(&mut dialled, LOW_ID).await?;
let notif = expect_notification(&mut inbound).await?;
assert!(is_collision_cease(&notif), "{notif:?}");
confirm(&mut dialled).await?;
wait_for_sessions(&server, 1).await?;
assert_eq!(server.registry.list()[0].direction, Direction::Outbound);
Ok(())
}
/// RFC default: a collision with an Established session closes the new
/// connection, whatever the identifiers say.
#[tokio::test]
async fn test_established_session_wins_by_default() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut first = TestPeer::connect(server.addr).await?;
first.handshake(PEER_ASN, HIGH_ID, 90).await?;
wait_for_sessions(&server, 1).await?;
let mut second = TestPeer::connect(server.addr).await?;
open_only(&mut second, HIGH_ID).await?;
let notif = expect_notification(&mut second).await?;
assert!(is_collision_cease(&notif), "{notif:?}");
// The original is untouched.
assert_eq!(server.registry.list().len(), 1);
server
.local_rib
.announce(LocalRoute::new(
IpPrefix::try_from("10.100.1.0/24")?,
LocalRouteAttrs::default(),
))
.await?;
let update = tokio::time::timeout(TIMEOUT, first.recv_update()).await??;
assert_eq!(update.announced_routes[0].to_string(), "10.100.1.0/24");
Ok(())
}
/// With collision_detect_established_state, a peer that restarts and dials
/// again replaces the stale session at once, and the routes learned over the
/// old session are flushed.
#[tokio::test]
async fn test_restarted_peer_replaces_stale_session_when_enabled() -> Result<()> {
let server =
TestServer::start_with(LOCAL_ASN, |c| c.collision_detect_established_state = true).await?;
let mut stale = TestPeer::connect(server.addr).await?;
stale.handshake(PEER_ASN, HIGH_ID, 90).await?;
wait_for_sessions(&server, 1).await?;
stale
.send(&Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![PEER_ASN]),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(10, 0, 0, 9))),
],
announced_routes: vec![IpPrefix::try_from("203.0.113.0/24")?.into()],
}))
.await?;
tokio::time::timeout(TIMEOUT, async {
while server.received_routes().is_empty() {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
let stale_addr = server.registry.list()[0].peer.addr;
// "Restart": a fresh connection from the same router.
let mut fresh = TestPeer::connect(server.addr).await?;
fresh.handshake(PEER_ASN, HIGH_ID, 90).await?;
let notif = expect_notification(&mut stale).await?;
assert!(is_collision_cease(&notif), "{notif:?}");
tokio::time::timeout(TIMEOUT, async {
loop {
let sessions = server.registry.list();
if sessions.len() == 1 && sessions[0].peer.addr != stale_addr {
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
assert!(
server.received_routes().is_empty(),
"stale routes were not flushed"
);
Ok(())
}
/// Different routers (different identifiers) never collide.
#[tokio::test]
async fn test_distinct_identifiers_do_not_collide() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut a = TestPeer::connect(server.addr).await?;
a.handshake(PEER_ASN, HIGH_ID, 90).await?;
let mut b = TestPeer::connect(server.addr).await?;
b.handshake(PEER_ASN, LOW_ID, 90).await?;
wait_for_sessions(&server, 2).await?;
Ok(())
}
/// A failed connection frees its slot: the same router can come back.
#[tokio::test]
async fn test_identifier_is_released_when_connection_ends() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut first = TestPeer::connect(server.addr).await?;
first.handshake(PEER_ASN, HIGH_ID, 90).await?;
wait_for_sessions(&server, 1).await?;
drop(first);
wait_for_sessions(&server, 0).await?;
let mut second = TestPeer::connect(server.addr).await?;
second.handshake(PEER_ASN, HIGH_ID, 90).await?;
wait_for_sessions(&server, 1).await?;
Ok(())
}
/// A peer claiming our own identifier is refused outright.
#[tokio::test]
async fn test_peer_with_our_identifier_is_refused() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = TestPeer::connect(server.addr).await?;
open_only(&mut peer, Ipv4Addr::new(127, 0, 0, 1)).await?;
let notif = expect_notification(&mut peer).await?;
assert!(
matches!(notif.error, BgpError::OpenMsg(OpenMsgSubcode::BadBgpId)),
"{notif:?}"
);
Ok(())
}
+319
View File
@@ -0,0 +1,319 @@
//! The active side: the server dials its configured peers and keeps
//! retrying.
use std::net::{Ipv4Addr, SocketAddr};
use std::time::Duration;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::TestServer;
use bgp_packet::errors::{BgpError, OpenMsgSubcode};
use bgp_packet::message::Message;
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::{LocalRoute, LocalRouteAttrs};
use bgp_server::config::PeerConfig;
use bgp_server::family::FamilySet;
use bgp_server::session::DialState;
use eyre::Result;
use tokio::net::TcpListener;
const LOCAL_ASN: u32 = 65000;
const PEER_ASN: u32 = 65001;
const TIMEOUT: Duration = Duration::from_secs(5);
/// Starts a server configured to dial `listener`'s address as peer "r1".
async fn server_dialing(listener: &TcpListener, remote_asn: u32) -> Result<TestServer> {
let endpoint: SocketAddr = listener.local_addr()?;
TestServer::start_with(LOCAL_ASN, |c| {
c.connect_retry = Duration::from_millis(200);
c.peers = vec![PeerConfig {
name: "r1".into(),
endpoint,
remote_asn,
hold_time_secs: 90,
local_address: None,
families: FamilySet::supported(),
add_path: None,
}];
})
.await
}
async fn accept(listener: &TcpListener) -> Result<TestPeer> {
let (stream, _) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
Ok(TestPeer::from_stream(stream))
}
#[tokio::test]
async fn test_server_dials_configured_peer_and_establishes() -> Result<()> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let server = server_dialing(&listener, PEER_ASN).await?;
let mut peer = accept(&listener).await?;
// Both sides send OPEN on connect; the test peer's handshake order works
// whichever side opened TCP.
let open = peer
.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
.await?;
assert_eq!(open.asn, LOCAL_ASN as u16);
let mut events = server.registry.subscribe();
// Either the Up event is still queued or the session is already listed.
let listed = tokio::time::timeout(TIMEOUT, async {
loop {
if let Some(s) = server.registry.list().first().cloned() {
return s;
}
let _ = events.recv().await;
}
})
.await?;
assert_eq!(listed.name.as_deref(), Some("r1"));
assert_eq!(listed.peer.asn, PEER_ASN);
assert_eq!(listed.peer.addr, listener.local_addr()?);
// Configured view reflects it; the dialer is not in backoff.
let (cfg, state) = server
.registry
.configured_peers()
.into_iter()
.next()
.unwrap();
assert_eq!(cfg.name, "r1");
assert!(
matches!(state, DialState::Connecting | DialState::Idle),
"{state:?}"
);
// Local routes flow over the dialled session too.
server
.local_rib
.announce(LocalRoute::new(
IpPrefix::try_from("10.100.1.0/24")?,
LocalRouteAttrs::default(),
))
.await?;
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
assert_eq!(update.announced_routes[0].to_string(), "10.100.1.0/24");
Ok(())
}
#[tokio::test]
async fn test_server_reconnects_after_peer_closes() -> Result<()> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let server = server_dialing(&listener, PEER_ASN).await?;
let mut first = accept(&listener).await?;
first
.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
.await?;
tokio::time::timeout(TIMEOUT, async {
while server.registry.list().is_empty() {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
drop(first);
// Session goes away, then the dialer comes back.
tokio::time::timeout(TIMEOUT, async {
while !server.registry.list().is_empty() {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
let (_, state) = server
.registry
.configured_peers()
.into_iter()
.next()
.unwrap();
assert!(matches!(state, DialState::Backoff { .. }), "{state:?}");
let mut second = accept(&listener).await?;
second
.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
.await?;
tokio::time::timeout(TIMEOUT, async {
while server.registry.list().is_empty() {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
Ok(())
}
#[tokio::test]
async fn test_wrong_asn_is_refused_with_bad_peer_as() -> Result<()> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let _server = server_dialing(&listener, PEER_ASN).await?;
let mut peer = accept(&listener).await?;
peer.send_open(65099, Ipv4Addr::new(10, 0, 0, 2), 90)
.await?;
let _ = peer.recv_open().await?;
let notif = tokio::time::timeout(TIMEOUT, async {
loop {
match peer.recv().await? {
Message::Notification(n) => return Ok::<_, eyre::Error>(n),
Message::KeepAlive => {}
other => eyre::bail!("unexpected {other:?}"),
}
}
})
.await??;
assert!(
matches!(notif.error, BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs)),
"{notif:?}"
);
// The dialer retries rather than giving up.
let _again = accept(&listener).await?;
Ok(())
}
/// An inbound connection from a configured peer's address is also checked
/// against the configured ASN and gets the configured name.
#[tokio::test]
async fn test_inbound_from_configured_address_uses_config() -> Result<()> {
// Configure a peer at the loopback address on a port nobody listens on,
// so the dialer never succeeds; then connect inbound from loopback.
let server = TestServer::start_with(LOCAL_ASN, |c| {
c.connect_retry = Duration::from_secs(60);
c.peers = vec![PeerConfig {
name: "r1".into(),
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1),
remote_asn: PEER_ASN,
hold_time_secs: 90,
local_address: None,
families: FamilySet::supported(),
add_path: None,
}];
})
.await?;
// Wrong ASN inbound → refused.
let mut bad = TestPeer::connect(server.addr).await?;
bad.send_open(65099, Ipv4Addr::new(10, 0, 0, 2), 90).await?;
let _ = bad.recv_open().await?;
let msg = tokio::time::timeout(TIMEOUT, bad.recv()).await??;
assert!(
matches!(&msg, Message::Notification(n) if matches!(n.error, BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs))),
"{msg:?}"
);
// Right ASN inbound → established, named.
let mut good = TestPeer::connect(server.addr).await?;
good.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
.await?;
let listed = tokio::time::timeout(TIMEOUT, async {
loop {
if let Some(s) = server.registry.list().first().cloned() {
return s;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
assert_eq!(listed.name.as_deref(), Some("r1"));
Ok(())
}
/// With `local_address` set the connection must come from that address (the
/// peer may identify us by it), and an address the host does not have is
/// reported as the reason for backing off rather than silently ignored.
#[tokio::test]
async fn test_local_address_is_used_as_the_source() -> Result<()> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let endpoint: SocketAddr = listener.local_addr()?;
let _server = TestServer::start_with(LOCAL_ASN, |c| {
c.connect_retry = Duration::from_millis(200);
c.peers = vec![PeerConfig {
name: "r1".into(),
endpoint,
remote_asn: PEER_ASN,
hold_time_secs: 90,
local_address: Some(Ipv4Addr::LOCALHOST.into()),
families: FamilySet::supported(),
add_path: None,
}];
})
.await?;
let (_stream, from) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
assert_eq!(from.ip(), std::net::IpAddr::from(Ipv4Addr::LOCALHOST));
// 192.0.2.1 is documentation space; no host has it.
let server = TestServer::start_with(LOCAL_ASN, |c| {
c.connect_retry = Duration::from_millis(200);
c.peers = vec![PeerConfig {
name: "r1".into(),
endpoint,
remote_asn: PEER_ASN,
hold_time_secs: 90,
local_address: Some(Ipv4Addr::new(192, 0, 2, 1).into()),
families: FamilySet::supported(),
add_path: None,
}];
})
.await?;
let reason = tokio::time::timeout(TIMEOUT, async {
loop {
if let Some((_, DialState::Backoff { last_error, .. })) =
server.registry.configured_peers().into_iter().next()
{
return last_error;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
})
.await?;
assert!(reason.contains("local_address 192.0.2.1"), "{reason}");
Ok(())
}
/// A peer that takes the connection and then refuses the session, here
/// because it is not the AS we are configured to expect, will refuse it
/// again. It is dialled ever more slowly, like a peer that cannot be
/// reached: connecting used to count as success and reset the wait, so
/// such a peer was dialled every few seconds for ever.
#[tokio::test]
async fn test_a_peer_that_refuses_the_session_is_dialled_ever_more_slowly() -> Result<()> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let server = server_dialing(&listener, PEER_ASN).await?;
let mut waits = Vec::new();
for _ in 0..3 {
let mut peer = accept(&listener).await?;
peer.send_open(65099, Ipv4Addr::new(10, 0, 0, 2), 90)
.await?;
let _ = peer.recv_open().await?;
let refusal = tokio::time::timeout(TIMEOUT, peer.recv()).await??;
assert!(
matches!(&refusal, Message::Notification(n) if n.error == BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs)),
"{refusal:?}"
);
drop(peer);
// How long the dialer says it will wait before trying again.
let wait = tokio::time::timeout(TIMEOUT, async {
loop {
if let Some((_, DialState::Backoff { until, last_error })) =
server.registry.configured_peers().into_iter().next()
{
assert!(last_error.contains("AS65099"), "{last_error}");
return until
.duration_since(std::time::SystemTime::now())
.unwrap_or_default();
}
tokio::time::sleep(Duration::from_millis(5)).await;
}
})
.await?;
waits.push(wait);
}
// 200 ms, then 400, then 800, less what passed before each was read.
assert!(
waits[2] > Duration::from_millis(500),
"the waits were {waits:?}"
);
Ok(())
}
+87
View File
@@ -0,0 +1,87 @@
//! What the server does when a part of it that it cannot do without fails.
//!
//! The local RIB's actor stops only by panicking. Before, nothing noticed:
//! sessions stayed up, nothing was announced any more, and the status page
//! showed an empty table. Now the server tells its peers it is going and
//! stops with an error, for whatever supervises it to restart it.
use std::net::Ipv4Addr;
use std::time::Duration;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::TestServer;
use bgp_packet::errors::{BgpError, CeaseSubcode};
use bgp_packet::message::Message;
use bgp_rib::RibStopped;
use bgp_server::error::ServerError;
use eyre::{Result, bail};
const LOCAL_ASN: u32 = 65000;
const PEER_ASN: u32 = 65001;
const TIMEOUT: Duration = Duration::from_secs(5);
async fn established(server: &TestServer) -> Result<TestPeer> {
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 90)
.await?;
tokio::time::timeout(TIMEOUT, async {
while server.registry.list().is_empty() {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
Ok(peer)
}
/// Reads until the server's NOTIFICATION, skipping KEEPALIVEs.
async fn farewell(peer: &mut TestPeer) -> Result<BgpError> {
tokio::time::timeout(TIMEOUT, async {
loop {
match peer.recv().await? {
Message::KeepAlive => {}
Message::Notification(n) => return Ok(n.error),
other => bail!("expected NOTIFICATION, got {other:?}"),
}
}
})
.await?
}
#[tokio::test]
async fn test_losing_the_local_rib_stops_the_server_and_tells_the_peers() -> Result<()> {
let mut server = TestServer::start(LOCAL_ASN).await?;
let (base, _http) = server.serve_http().await?;
let mut peer = established(&server).await?;
server.local_rib.stop_for_testing();
let outcome = tokio::time::timeout(TIMEOUT, server.stopped()).await?;
assert!(
matches!(outcome, Err(ServerError::RibStopped(RibStopped { rib })) if rib == "local RIB"),
"{outcome:?}"
);
assert_eq!(
farewell(&mut peer).await?,
BgpError::Cease(CeaseSubcode::AdminShutdown)
);
// The status interface says the table cannot be read, where it used to
// show an empty one.
let client = reqwest::Client::new();
for path in ["/api/status", "/api/routes/local", "/metricz"] {
let response = client.get(format!("{base}{path}")).send().await?;
assert_eq!(response.status(), 503, "{path}");
assert_eq!(
response.text().await?,
"the local RIB has stopped",
"{path}"
);
}
// What does not need that table still answers.
let response = client
.get(format!("{base}/api/routes/received"))
.send()
.await?;
assert_eq!(response.status(), 200);
Ok(())
}
+303
View File
@@ -0,0 +1,303 @@
//! Drives the HTTP API against a real server with a real BGP peer attached.
use std::net::Ipv4Addr;
use std::time::Duration;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::TestServer;
use bgp_packet::message::{
AsPathAttribute, CommunitiesPathAttribute, LargeCommunity, Message, NextHopPathAttribute,
OriginPathAttribute, PathAttribute, UpdateMessage,
};
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::{LocalRoute, LocalRouteAttrs};
use eyre::Result;
use serde_json::{Value, json};
const LOCAL_ASN: u32 = 65000;
const PEER_ASN: u32 = 65001;
const TIMEOUT: Duration = Duration::from_secs(5);
fn prefix(s: &str) -> IpPrefix {
IpPrefix::try_from(s).unwrap()
}
async fn established_peer(server: &TestServer) -> Result<TestPeer> {
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 90)
.await?;
tokio::time::timeout(TIMEOUT, async {
while server.registry.list().is_empty() {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
Ok(peer)
}
async fn get_json(client: &reqwest::Client, url: &str) -> Result<Value> {
Ok(client
.get(url)
.send()
.await?
.error_for_status()?
.json()
.await?)
}
#[tokio::test]
async fn test_status_and_peers() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let (base, _http) = server.serve_http().await?;
let client = reqwest::Client::new();
let config = get_json(&client, &format!("{base}/api/config")).await?;
assert_eq!(config["path"], "test.textpb");
assert_eq!(config["text"], format!("local_asn: {LOCAL_ASN}\n"));
let status = get_json(&client, &format!("{base}/api/status")).await?;
assert_eq!(status["local_asn"], LOCAL_ASN);
assert_eq!(status["router_id"], "127.0.0.1");
assert_eq!(status["sessions"], 0);
assert!(status["started_at_unix"].as_u64().unwrap() > 1_700_000_000);
assert!(!status["hostname"].as_str().unwrap().is_empty());
let _peer = established_peer(&server).await?;
let status = get_json(&client, &format!("{base}/api/status")).await?;
assert_eq!(status["sessions"], 1);
let peers = get_json(&client, &format!("{base}/api/peers")).await?;
assert_eq!(peers.as_array().unwrap().len(), 1);
assert_eq!(peers[0]["peer_asn"], PEER_ASN);
assert_eq!(peers[0]["local_addr"], server.addr.to_string());
assert_eq!(peers[0]["four_octet_asn"], true);
assert_eq!(
peers[0]["address_families"],
serde_json::json!(["IPv4 unicast", "IPv6 unicast"])
);
Ok(())
}
/// Originated routes are listed with their attributes as text.
#[tokio::test]
async fn test_originated_routes_are_listed() -> Result<()> {
let attrs = LocalRouteAttrs {
prepend: 1,
med: Some(10),
communities: vec![(65000, 100), (65535, 65281)],
large_communities: vec![LargeCommunity {
global_admin: 65000,
data_1: 1,
data_2: 2,
}],
..Default::default()
};
let server = TestServer::start_with(LOCAL_ASN, |c| {
c.originate = vec![LocalRoute::new(prefix("10.100.1.0/24"), attrs)];
})
.await?;
let (base, _http) = server.serve_http().await?;
let client = reqwest::Client::new();
let local = get_json(&client, &format!("{base}/api/routes/local")).await?;
assert_eq!(local.as_array().unwrap().len(), 1);
assert_eq!(local[0]["prefix"], "10.100.1.0/24");
assert_eq!(local[0]["origin"], "IGP");
assert_eq!(local[0]["prepend"], 1);
assert_eq!(local[0]["med"], 10);
assert_eq!(local[0]["communities"], json!(["65000:100", "65535:65281"]));
assert_eq!(local[0]["large_communities"], json!(["65000:1:2"]));
assert_eq!(
get_json(&client, &format!("{base}/api/status")).await?["local_routes"],
1
);
Ok(())
}
/// The API only reports. Nothing reachable over HTTP changes what the
/// router announces.
#[tokio::test]
async fn test_api_is_read_only() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let (base, _http) = server.serve_http().await?;
let client = reqwest::Client::new();
let body = json!({ "prefix": "10.100.1.0/24" });
for path in [
"/api/routes/local",
"/api/routes/received",
"/api/peers",
"/api/status",
] {
let post = client
.post(format!("{base}{path}"))
.json(&body)
.send()
.await?;
assert_eq!(post.status(), 405, "POST {path}");
let delete = client
.delete(format!("{base}{path}"))
.json(&body)
.send()
.await?;
assert_eq!(delete.status(), 405, "DELETE {path}");
}
assert!(server.local_rib.snapshot().await.unwrap().is_empty());
Ok(())
}
/// Routes the peer sends us show up in the received table with a summary.
#[tokio::test]
async fn test_received_routes_summary() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let (base, _http) = server.serve_http().await?;
let client = reqwest::Client::new();
let mut peer = established_peer(&server).await?;
peer.send(&Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![PEER_ASN, 65002]),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(10, 0, 0, 1))),
PathAttribute::Communities(CommunitiesPathAttribute(vec![(65001, 7)])),
],
announced_routes: vec![prefix("203.0.113.0/24").into()],
}))
.await?;
let received = tokio::time::timeout(TIMEOUT, async {
loop {
let v = get_json(&client, &format!("{base}/api/routes/received")).await?;
if !v.as_array().unwrap().is_empty() {
return Ok::<_, eyre::Error>(v);
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
})
.await??;
assert_eq!(received[0]["prefix"], "203.0.113.0/24");
assert_eq!(received[0]["peer_asn"], PEER_ASN);
assert_eq!(received[0]["summary"]["as_path"], json!([PEER_ASN, 65002]));
assert_eq!(received[0]["summary"]["next_hop"], "10.0.0.1");
assert_eq!(received[0]["summary"]["communities"], json!(["65001:7"]));
Ok(())
}
/// The SSE stream reports session and route changes as they happen.
#[tokio::test]
async fn test_event_stream() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let (base, _http) = server.serve_http().await?;
let client = reqwest::Client::new();
let mut resp = client
.get(format!("{base}/api/events"))
.send()
.await?
.error_for_status()?;
assert!(
resp.headers()["content-type"]
.to_str()?
.starts_with("text/event-stream")
);
let _peer = established_peer(&server).await?;
server
.local_rib
.announce(LocalRoute::new(prefix("10.100.1.0/24"), Default::default()))
.await?;
// Collect SSE frames until we have seen both events.
let mut buf = String::new();
let mut seen_up = false;
let mut seen_announce = false;
tokio::time::timeout(TIMEOUT, async {
while !(seen_up && seen_announce) {
let chunk = resp
.chunk()
.await?
.ok_or_else(|| eyre::eyre!("stream ended"))?;
buf.push_str(std::str::from_utf8(&chunk)?);
for line in buf.lines().filter_map(|l| l.strip_prefix("data:")) {
let ev: Value = serde_json::from_str(line.trim())?;
match ev["type"].as_str() {
Some("session_up") if ev["peer"]["peer_asn"] == PEER_ASN => seen_up = true,
Some("local_announced") if ev["route"]["prefix"] == "10.100.1.0/24" => {
seen_announce = true
}
_ => {}
}
}
}
Ok::<_, eyre::Error>(())
})
.await??;
Ok(())
}
#[tokio::test]
async fn test_statusz_page_is_served() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let (base, _http) = server.serve_http().await?;
let client = reqwest::Client::new();
let resp = client.get(format!("{base}/statusz")).send().await?;
assert_eq!(resp.status(), 200);
assert!(
resp.headers()["content-type"]
.to_str()?
.starts_with("text/html")
);
let body = resp.text().await?;
assert!(body.contains("<title>Status for ferrix_bgp</title>"));
assert!(body.contains("/api/events"));
// The root redirects to the page.
let no_redirect = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()?;
let resp = no_redirect.get(format!("{base}/")).send().await?;
assert_eq!(resp.status(), 307);
assert_eq!(resp.headers()["location"], "/statusz");
Ok(())
}
#[tokio::test]
async fn test_metricz() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let (base, _http) = server.serve_http().await?;
let client = reqwest::Client::new();
let _peer = established_peer(&server).await?;
let text = client
.get(format!("{base}/metricz"))
.send()
.await?
.error_for_status()?
.text()
.await?;
// Metrics are process-global, so other tests in this binary contribute
// too; assert on presence and a lower bound rather than exact values.
let metric = |name: &str| -> Option<i64> {
text.lines()
.find(|l| l.starts_with(name) && l[name.len()..].starts_with(' '))
.and_then(|l| l.rsplit(' ').next()?.parse().ok())
};
assert!(
metric("bgp_sessions_established").unwrap_or(0) >= 1,
"{text}"
);
assert!(
metric("bgp_messages_sent_total{type=\"open\"}").unwrap_or(0) >= 1,
"{text}"
);
assert!(
metric("bgp_messages_received_total{type=\"keepalive\"}").unwrap_or(0) >= 1,
"{text}"
);
assert!(metric("bgp_local_routes").is_some(), "{text}");
assert!(metric("bgp_rib_routes").is_some(), "{text}");
Ok(())
}
+148
View File
@@ -0,0 +1,148 @@
//! Interoperability with what real routers put on the wire.
use std::net::Ipv4Addr;
use std::time::Duration;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::TestServer;
use bgp_packet::message::{Message, PathAttribute};
use bgp_packet::open::{Capability, OpenOption};
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::{LocalRoute, LocalRouteAttrs};
use eyre::Result;
const TIMEOUT: Duration = Duration::from_secs(5);
/// The OPEN a BIRD 2 router sends for an IPv6-only session from AS210036:
/// one optional parameter carrying MP(IPv6 unicast), route refresh,
/// extended message, graceful restart (aware only), 4-octet AS, enhanced
/// route refresh and long-lived graceful restart (aware only).
#[rustfmt::skip]
const BIRD_OPEN: &[u8] = &[
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x37, 0x01, // length 55, type OPEN
0x04, // version
0x5b, 0xa0, // AS_TRANS
0x00, 0xf0, // hold time 240
0x0a, 0xff, 0x00, 0x01, // router id 10.255.0.1
0x1a, // optional parameters length 26
0x02, 0x18, // capabilities, 24 bytes
0x01, 0x04, 0x00, 0x02, 0x00, 0x01, // MP: IPv6 unicast
0x02, 0x00, // route refresh
0x06, 0x00, // extended message
0x40, 0x02, 0x00, 0x00, // graceful restart, no address families
0x41, 0x04, 0x00, 0x03, 0x34, 0x74, // 4-octet AS 210036
0x46, 0x00, // enhanced route refresh
0x47, 0x00, // long-lived graceful restart, none
];
/// IPv6 End-of-RIB: an UPDATE whose only attribute is an empty
/// MP_UNREACH_NLRI for IPv6 unicast (RFC 4724 §2).
#[rustfmt::skip]
const IPV6_END_OF_RIB: &[u8] = &[
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x1e, 0x02, // length 30, type UPDATE
0x00, 0x00, // no withdrawn routes
0x00, 0x07, // 7 bytes of path attributes
0x90, 0x0f, 0x00, 0x03, // optional + extended length, MP_UNREACH_NLRI, len 3
0x00, 0x02, 0x01, // IPv6 unicast, no prefixes
];
#[tokio::test]
async fn test_server_advertises_ipv4_and_ipv6_unicast() -> Result<()> {
let server = TestServer::start(65000).await?;
let mut peer = TestPeer::connect(server.addr).await?;
let open = peer.recv_open().await?;
let caps: Vec<&Capability> = open
.options
.iter()
.flat_map(|o| match o {
OpenOption::Capabilities(c) => c.iter(),
})
.collect();
for afi in [Afi::IPV4, Afi::IPV6] {
assert!(
caps.iter().any(|c| matches!(c, Capability::MultiProtocol { afi: a, safi: Safi::UNICAST } if *a == afi)),
"missing MP capability for {afi:?}: {caps:?}"
);
}
Ok(())
}
#[tokio::test]
async fn test_bird_open_and_end_of_rib_are_accepted() -> Result<()> {
let server = TestServer::start(4200000001).await?;
let mut peer = TestPeer::connect(server.addr).await?;
peer.send_raw(BIRD_OPEN).await?;
peer.recv_open().await?;
match tokio::time::timeout(TIMEOUT, peer.recv()).await?? {
Message::KeepAlive => {}
other => eyre::bail!("server did not accept BIRD's OPEN: {other:?}"),
}
peer.send(&Message::KeepAlive).await?;
tokio::time::timeout(TIMEOUT, async {
while server.registry.list().is_empty() {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
let session = server.registry.list().remove(0);
assert_eq!(
session.peer.asn, 210036,
"4-octet ASN must come from the capability, not AS_TRANS"
);
assert!(session.four_octet_asn);
// End-of-RIB must neither break the session nor create a route.
peer.send_raw(IPV6_END_OF_RIB).await?;
peer.send(&Message::KeepAlive).await?;
tokio::time::sleep(Duration::from_millis(200)).await;
assert_eq!(
server.registry.list().len(),
1,
"session dropped after End-of-RIB"
);
assert!(server.received_routes().is_empty());
Ok(())
}
/// Over an IPv4 test transport an IPv6 route cannot be sent (no IPv6 next
/// hop), so this only checks the builder the advertiser uses: the UPDATE for
/// an IPv6 prefix carries MP_REACH_NLRI with an IPv6 next hop and no NEXT_HOP.
#[tokio::test]
async fn test_ipv6_prefix_update_shape() -> Result<()> {
let route = LocalRoute::new(
IpPrefix::try_from("2001:db8:1::/48")?,
LocalRouteAttrs::default(),
);
let nh: std::net::Ipv6Addr = "fd00:179::2".parse()?;
let update = bgp_server::advertise::build_announce(
&route,
&bgp_server::advertise::Towards {
local_asn: 4200000001,
next_hop: nh.into(),
four_octet_asn: true,
ibgp: false,
},
)
.expect("ipv6 over ipv6");
assert!(update.announced_routes.is_empty());
assert!(
!update
.path_attributes
.iter()
.any(|a| matches!(a, PathAttribute::NextHop(_)))
);
assert!(
update
.path_attributes
.iter()
.any(|a| matches!(a, PathAttribute::MpReachNlri(m)
if m.family == AfiSafi::IPV6_UNICAST && m.prefixes[0].to_string() == "2001:db8:1::/48"))
);
let _ = Ipv4Addr::LOCALHOST;
Ok(())
}
@@ -0,0 +1,321 @@
//! End-to-end tests for capability negotiation: what the two OPENs agree on
//! decides which routes are sent and accepted, and how they are encoded.
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
use std::time::Duration;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::TestServer;
use bgp_packet::errors::{BgpError, OpenMsgSubcode};
use bgp_packet::message::{
AsPathAttribute, Message, MpReachNlriPathAttribute, NextHopPathAttribute, NlriNextHop,
OriginPathAttribute, PathAttribute, UpdateMessage,
};
use bgp_packet::open::Capability;
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::{LocalRoute, LocalRouteAttrs};
use bgp_server::config::PeerConfig;
use bgp_server::family::{FamilySet, SUPPORTED};
use eyre::{Result, bail};
const LOCAL_ASN: u32 = 65000;
const PEER_ASN: u32 = 65001;
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1);
const TIMEOUT: Duration = Duration::from_secs(5);
fn prefix(s: &str) -> IpPrefix {
IpPrefix::try_from(s).unwrap()
}
fn only(family: AfiSafi) -> FamilySet {
[family].into_iter().collect()
}
fn four_octet() -> Capability {
Capability::FourByteAsn { asn: PEER_ASN }
}
async fn wait_for_session(server: &TestServer) -> Result<bgp_server::session::SessionHandle> {
Ok(tokio::time::timeout(TIMEOUT, async {
loop {
if let Some(session) = server.registry.list().into_iter().next() {
return session;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?)
}
/// An UPDATE announcing one IPv4 route in the body and one IPv6 route in
/// MP_REACH_NLRI.
fn dual_stack_update() -> UpdateMessage {
UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![PEER_ASN]),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
family: AfiSafi::IPV6_UNICAST,
next_hop: NlriNextHop::Ipv6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)),
prefixes: vec![prefix("2001:db8:1::/48").into()],
}),
],
announced_routes: vec![prefix("192.0.2.0/24").into()],
}
}
/// The session carries the families both OPENs offered, and the server's
/// OPEN offers every family by default.
#[tokio::test]
async fn test_families_are_the_ones_both_sides_offered() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = TestPeer::connect(server.addr).await?;
let open = peer
.handshake_with(
PEER_ASN,
PEER_ID,
90,
vec![
four_octet(),
Capability::multiprotocol(AfiSafi::IPV4_UNICAST),
],
)
.await?;
for family in SUPPORTED {
assert!(
open.capabilities()
.any(|c| *c == Capability::multiprotocol(family)),
"the server's OPEN offers {family}"
);
}
let session = wait_for_session(&server).await?;
assert_eq!(session.families, only(AfiSafi::IPV4_UNICAST));
Ok(())
}
/// A route of a family the peer did not offer is not accepted from it. The
/// rest of the UPDATE is, and the session stays up.
#[tokio::test]
async fn test_routes_of_a_family_not_negotiated_are_not_accepted() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake_with(
PEER_ASN,
PEER_ID,
90,
vec![
four_octet(),
Capability::multiprotocol(AfiSafi::IPV4_UNICAST),
],
)
.await?;
wait_for_session(&server).await?;
peer.send(&Message::Update(dual_stack_update())).await?;
let routes = tokio::time::timeout(TIMEOUT, async {
loop {
let routes = server.received_routes();
if !routes.is_empty() {
return routes;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
// One UPDATE is applied as a whole, so the IPv6 route is not on its way.
assert_eq!(routes.len(), 1);
assert_eq!(routes[0].prefix, prefix("192.0.2.0/24"));
assert_eq!(server.registry.list().len(), 1, "the session survives");
Ok(())
}
/// A route of a family the peer did not offer is not sent to it.
#[tokio::test]
async fn test_routes_of_a_family_not_negotiated_are_not_sent() -> Result<()> {
let server = TestServer::start_with(LOCAL_ASN, |c| {
c.originate = vec![LocalRoute::new(
prefix("10.100.0.0/16"),
LocalRouteAttrs::default(),
)];
})
.await?;
// This peer wants IPv6 only; the server has an IPv4 route.
let mut v6_only = TestPeer::connect(server.addr).await?;
v6_only
.handshake_with(
PEER_ASN,
PEER_ID,
90,
vec![
four_octet(),
Capability::multiprotocol(AfiSafi::IPV6_UNICAST),
],
)
.await?;
let session = wait_for_session(&server).await?;
assert_eq!(session.families, only(AfiSafi::IPV6_UNICAST));
// A second peer that takes IPv4 is sent the route. The advertiser
// handles sessions in the order they came up, so by the time this one
// has its route the first has been dealt with.
let mut dual = TestPeer::connect(server.addr).await?;
dual.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
.await?;
let update = tokio::time::timeout(TIMEOUT, dual.recv_update()).await??;
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
let nothing = tokio::time::timeout(Duration::from_millis(200), v6_only.recv_update()).await;
assert!(nothing.is_err(), "got {nothing:?}");
Ok(())
}
/// A configured peer is offered only the families configured for it, and a
/// peer that shares none of them is refused with the capability we needed.
#[tokio::test]
async fn test_no_family_in_common_refuses_the_session() -> Result<()> {
let server = TestServer::start_with(LOCAL_ASN, |c| {
// Inbound connections from this address inherit the peer's settings.
c.peers = vec![PeerConfig {
name: "v6-only".into(),
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1),
remote_asn: PEER_ASN,
hold_time_secs: 90,
local_address: None,
families: only(AfiSafi::IPV6_UNICAST),
add_path: None,
}];
// Keep the dialer out of the way.
c.connect_retry = Duration::from_secs(3600);
})
.await?;
let mut peer = TestPeer::connect(server.addr).await?;
peer.send_open_with(
PEER_ASN,
PEER_ID,
90,
vec![
four_octet(),
Capability::multiprotocol(AfiSafi::IPV4_UNICAST),
],
)
.await?;
let open = peer.recv_open().await?;
let offered: Vec<_> = open
.capabilities()
.filter(|c| matches!(c, Capability::MultiProtocol { .. }))
.collect();
assert_eq!(offered, [&Capability::multiprotocol(AfiSafi::IPV6_UNICAST)]);
match tokio::time::timeout(TIMEOUT, peer.recv()).await?? {
Message::Notification(n) => {
assert_eq!(
n.error,
BgpError::OpenMsg(OpenMsgSubcode::UnsupportedCapability)
);
// The Multiprotocol capability for IPv6 unicast.
assert_eq!(n.data, [1, 4, 0, 2, 0, 1]);
}
other => bail!("expected NOTIFICATION, got {other:?}"),
}
assert!(server.registry.list().is_empty());
Ok(())
}
/// Real routers offer families we do not support. That must not stop
/// the families we do share from working.
#[tokio::test]
async fn test_a_peer_offering_an_unsupported_family_is_still_accepted() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake_with(
PEER_ASN,
PEER_ID,
90,
vec![
four_octet(),
Capability::multiprotocol(AfiSafi::new(Afi::L2VPN, Safi::EVPN)),
Capability::multiprotocol(AfiSafi::IPV4_UNICAST),
],
)
.await?;
let session = wait_for_session(&server).await?;
assert_eq!(session.families, only(AfiSafi::IPV4_UNICAST));
assert!(
session.four_octet_asn,
"capabilities after it are still read"
);
Ok(())
}
/// A peer that offers no capabilities at all is a plain RFC 4271 speaker:
/// IPv4 unicast, two-octet AS numbers. Our four-octet AS number reaches it
/// as AS_TRANS in AS_PATH and in full in AS4_PATH (RFC 6793 §4.2.2).
#[tokio::test]
async fn test_a_peer_without_capabilities_gets_two_octet_as_paths() -> Result<()> {
const WIDE_ASN: u32 = 4_200_000_001;
let server = TestServer::start_with(WIDE_ASN, |c| {
c.originate = vec![LocalRoute::new(
prefix("10.100.0.0/16"),
LocalRouteAttrs::default(),
)];
})
.await?;
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake_with(PEER_ASN, PEER_ID, 90, vec![]).await?;
let session = wait_for_session(&server).await?;
assert!(!session.four_octet_asn);
assert_eq!(session.families, only(AfiSafi::IPV4_UNICAST));
// The test peer now decodes AS_PATH two octets wide, as such a speaker
// would; an UPDATE encoded four octets wide would not parse.
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
assert!(
update
.path_attributes
.contains(&AsPathAttribute::from_asns(vec![23456])),
"{update:?}"
);
let as4_path = update.path_attributes.iter().find_map(|a| match a {
PathAttribute::As4Path(path) => Some(path.segments[0].path.clone()),
_ => None,
});
assert_eq!(as4_path, Some(vec![WIDE_ASN]));
// And its own two-octet paths are read correctly.
peer.send(&Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![PEER_ASN, 64512]),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
],
announced_routes: vec![prefix("192.0.2.0/24").into()],
}))
.await?;
let routes = tokio::time::timeout(TIMEOUT, async {
loop {
let routes = server.received_routes();
if !routes.is_empty() {
return routes;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
assert!(
routes[0]
.attributes
.contains(&AsPathAttribute::from_asns(vec![PEER_ASN, 64512]))
);
Ok(())
}
@@ -0,0 +1,228 @@
//! End-to-end tests for errors in UPDATE messages (RFC 7606): most of them
//! cost the routes in the UPDATE, not the session.
use std::net::Ipv4Addr;
use std::time::Duration;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::{ReceivedRoute, TestServer};
use bgp_packet::errors::{BgpError, UpdateMsgSubcode};
use bgp_packet::message::{LocalPrefPathAttribute, Message, PathAttribute};
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_server::metrics::UPDATE_ERRORS;
use eyre::{Result, bail};
const LOCAL_ASN: u32 = 65000;
const PEER_ASN: u32 = 65001;
const TIMEOUT: Duration = Duration::from_secs(5);
const WELL_KNOWN: u8 = 0x40;
const OPTIONAL_TRANSITIVE: u8 = 0xc0;
fn attr(flags: u8, type_code: u8, value: &[u8]) -> Vec<u8> {
let mut wire = vec![flags, type_code, value.len() as u8];
wire.extend_from_slice(value);
wire
}
fn origin() -> Vec<u8> {
attr(WELL_KNOWN, 1, &[0])
}
/// AS_SEQUENCE of the peer's AS, four octets wide.
fn as_path() -> Vec<u8> {
attr(WELL_KNOWN, 2, &[2, 1, 0, 0, 0xfd, 0xe9])
}
fn next_hop() -> Vec<u8> {
attr(WELL_KNOWN, 3, &[192, 0, 2, 1])
}
/// A whole UPDATE message announcing `nlri` with `attributes`, as bytes, so
/// that it can be wrong in ways the encoder would not write.
fn update(attributes: &[Vec<u8>], nlri: &[u8]) -> Vec<u8> {
let attributes = attributes.concat();
let mut body = vec![0, 0];
body.extend_from_slice(&(attributes.len() as u16).to_be_bytes());
body.extend_from_slice(&attributes);
body.extend_from_slice(nlri);
let mut wire = vec![0xff; 16];
wire.extend_from_slice(&((19 + body.len()) as u16).to_be_bytes());
wire.push(2);
wire.extend_from_slice(&body);
wire
}
/// 10.1.0.0/16 and 10.2.0.0/16.
const ROUTE_A: [u8; 3] = [16, 10, 1];
const ROUTE_B: [u8; 3] = [16, 10, 2];
fn prefix(s: &str) -> IpPrefix {
IpPrefix::try_from(s).unwrap()
}
async fn established(server: &TestServer, asn: u32) -> Result<TestPeer> {
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(asn, Ipv4Addr::new(10, 0, 0, 1), 90).await?;
Ok(peer)
}
/// Waits until the server's Adj-RIB-In holds exactly `prefixes`.
async fn wait_for_routes(server: &TestServer, prefixes: &[&str]) -> Result<Vec<ReceivedRoute>> {
let mut expected: Vec<IpPrefix> = prefixes.iter().map(|p| prefix(p)).collect();
expected.sort_by_key(|p| p.to_string());
Ok(tokio::time::timeout(TIMEOUT, async {
loop {
let mut routes = server.received_routes();
routes.sort_by_key(|r| r.prefix.to_string());
if routes.iter().map(|r| &r.prefix).eq(expected.iter()) {
return routes;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?)
}
/// The situation RFC 7606 exists for: a route is in the table, and then an
/// UPDATE for it arrives with a broken attribute. The route goes; the
/// session, and the other routes learned over it, stay.
#[tokio::test]
async fn test_a_malformed_attribute_withdraws_the_route_and_keeps_the_session() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = established(&server, PEER_ASN).await?;
let handled_before = UPDATE_ERRORS
.with_label_values(&["treat-as-withdraw"])
.get();
let good = [origin(), as_path(), next_hop()];
peer.send_raw(&update(&good, &[ROUTE_A, ROUTE_B].concat()))
.await?;
wait_for_routes(&server, &["10.1.0.0/16", "10.2.0.0/16"]).await?;
// ORIGIN with a value that is not defined.
let bad = [attr(WELL_KNOWN, 1, &[7]), as_path(), next_hop()];
peer.send_raw(&update(&bad, &ROUTE_A)).await?;
wait_for_routes(&server, &["10.2.0.0/16"]).await?;
// The session goes on: the route can be announced again.
peer.send_raw(&update(&good, &ROUTE_A)).await?;
wait_for_routes(&server, &["10.1.0.0/16", "10.2.0.0/16"]).await?;
assert_eq!(server.registry.list().len(), 1);
assert!(
UPDATE_ERRORS
.with_label_values(&["treat-as-withdraw"])
.get()
> handled_before
);
Ok(())
}
/// An announcement without a mandatory attribute is not a route we can use.
#[tokio::test]
async fn test_a_route_without_as_path_is_not_accepted() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = established(&server, PEER_ASN).await?;
peer.send_raw(&update(&[origin(), next_hop()], &ROUTE_A))
.await?;
// A well-formed one after it shows that the first was dealt with.
peer.send_raw(&update(&[origin(), as_path(), next_hop()], &ROUTE_B))
.await?;
wait_for_routes(&server, &["10.2.0.0/16"]).await?;
assert_eq!(server.registry.list().len(), 1);
Ok(())
}
/// A broken attribute that plays no part in choosing a route is dropped,
/// and the route is kept.
#[tokio::test]
async fn test_a_malformed_aggregator_is_dropped_and_the_route_kept() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = established(&server, PEER_ASN).await?;
let attributes = [
origin(),
as_path(),
next_hop(),
// AGGREGATOR is eight octets on this session.
attr(OPTIONAL_TRANSITIVE, 7, &[0, 0, 0xfd, 0xe9, 192]),
];
peer.send_raw(&update(&attributes, &ROUTE_A)).await?;
let routes = wait_for_routes(&server, &["10.1.0.0/16"]).await?;
assert_eq!(routes[0].attributes.len(), 3);
assert!(
!routes[0]
.attributes
.iter()
.any(|a| matches!(a, PathAttribute::Aggregator(_)))
);
Ok(())
}
/// LOCAL_PREF is a matter between routers of one AS. From an external peer
/// it is dropped; from an internal one it is kept.
#[tokio::test]
async fn test_local_pref_is_kept_only_from_an_internal_peer() -> Result<()> {
let local_pref = attr(WELL_KNOWN, 5, &[0, 0, 0, 200]);
let has_local_pref = |routes: &[ReceivedRoute]| {
routes[0]
.attributes
.contains(&PathAttribute::LocalPref(LocalPrefPathAttribute(200)))
};
let server = TestServer::start(LOCAL_ASN).await?;
let mut external = established(&server, PEER_ASN).await?;
let attributes = [origin(), as_path(), next_hop(), local_pref.clone()];
external.send_raw(&update(&attributes, &ROUTE_A)).await?;
let routes = wait_for_routes(&server, &["10.1.0.0/16"]).await?;
assert!(!has_local_pref(&routes));
let server = TestServer::start(LOCAL_ASN).await?;
let mut internal = established(&server, LOCAL_ASN).await?;
// An internal peer has not added its AS to the path.
let attributes = [origin(), attr(WELL_KNOWN, 2, &[]), next_hop(), local_pref];
internal.send_raw(&update(&attributes, &ROUTE_A)).await?;
let routes = wait_for_routes(&server, &["10.1.0.0/16"]).await?;
assert!(has_local_pref(&routes));
Ok(())
}
/// When the prefixes themselves cannot be read there is nothing to
/// withdraw, and the session is reset as before.
#[tokio::test]
async fn test_an_unreadable_prefix_still_resets_the_session() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = established(&server, PEER_ASN).await?;
// An IPv4 prefix of length 33.
peer.send_raw(&update(
&[origin(), as_path(), next_hop()],
&[33, 10, 1, 2, 3, 4],
))
.await?;
loop {
match tokio::time::timeout(TIMEOUT, peer.recv()).await?? {
Message::KeepAlive => {}
Message::Notification(n) => {
assert_eq!(
n.error,
BgpError::UpdateMsg(UpdateMsgSubcode::InvalidNetworkField)
);
break;
}
other => bail!("expected NOTIFICATION, got {other:?}"),
}
}
tokio::time::timeout(TIMEOUT, async {
while !server.registry.list().is_empty() {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
Ok(())
}