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:
1 parent
1ec60255ea
commit
630c175918
138 files changed
+21650
-2925
No files matched your search
@@ -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(())
|
||||
}
|
||||
@@ -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? {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(¬if), "{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(¬if), "{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(¬if), "{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(¬if), "{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(¬if), "{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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
Reference in new issue
Block a user