feature: BGP server, FSM, Adj-RIB-In actor, and integration tests
- Implement passive-side BGP FSM (OpenSent → OpenConfirm → Established) with hold timer, keepalive timer, and NOTIFICATION on expiry - Add bgp-server crate: config validation, TCP listener accept loop (BgpServerBuilder → BgpServer), connection dispatch - Add bgp-rib crate: Adj-RIB-In actor owning route state with event broadcast for streaming consumers and snapshot for introspection - Wire Adj-RIB-In into FSM: announce/withdraw on UPDATE for both legacy IPv4 NLRI fields and MP_REACH/MP_UNREACH path attributes - Add bgp-integration-tests crate with 15 end-to-end tests covering session establishment, timers, UPDATE handling, and RIB introspection - Fix require_empty! macro bug in open.rs (checked wrong buffer) - Fix FourByteAsn capability parser returning consumed buffer as remainder - Fix tracing subscriber not being installed in TracingArgs::install - Expand docs/RFCs.md with full implementation status across 15 RFCs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
4ba644827d
commit
1ec60255ea
24 files changed
+1685
-26
No files matched your search
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "bgp-integration-tests"
|
||||
edition.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
bgp-packet.workspace = true
|
||||
bgp-primitives.workspace = true
|
||||
bgp-rib.workspace = true
|
||||
bgp-server.workspace = true
|
||||
bytes.workspace = true
|
||||
eyre.workspace = true
|
||||
tokio.workspace = true
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod test_peer;
|
||||
pub mod test_server;
|
||||
@@ -0,0 +1,127 @@
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
|
||||
use bgp_packet::message::{BGP_HEADER_LEN, Message};
|
||||
use bgp_packet::open::{Capability, OpenMessage, OpenOption};
|
||||
use bgp_primitives::constants::{AddressFamilyId, SubsequentAfi};
|
||||
use bgp_primitives::parser::ParserContext;
|
||||
use bytes::BytesMut;
|
||||
use eyre::{Result, bail, eyre};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
/// A minimal BGP peer for use in tests. Connects over TCP and can exchange
|
||||
/// framed BGP messages using the `bgp-packet` wire format.
|
||||
pub struct TestPeer {
|
||||
stream: TcpStream,
|
||||
pub ctx: ParserContext,
|
||||
}
|
||||
|
||||
impl TestPeer {
|
||||
pub async fn connect(addr: SocketAddr) -> Result<Self> {
|
||||
let stream = TcpStream::connect(addr).await?;
|
||||
Ok(Self { stream, ctx: ParserContext::default() })
|
||||
}
|
||||
|
||||
/// Send a BGP message.
|
||||
pub async fn send(&mut self, msg: &Message) -> Result<()> {
|
||||
let mut buf = BytesMut::new();
|
||||
msg.to_wire(&self.ctx, &mut buf)?;
|
||||
self.stream.write_all(&buf).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read exactly one BGP message.
|
||||
pub async fn recv(&mut self) -> Result<Message> {
|
||||
let mut header = [0u8; BGP_HEADER_LEN as usize];
|
||||
self.stream.read_exact(&mut header).await?;
|
||||
|
||||
let total_len = u16::from_be_bytes([header[16], header[17]]) as usize;
|
||||
if total_len < BGP_HEADER_LEN as usize {
|
||||
bail!("Received BGP message with invalid length {total_len}");
|
||||
}
|
||||
|
||||
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?;
|
||||
|
||||
let (_, msg) = Message::from_wire(&self.ctx, &buf)
|
||||
.map_err(|e| eyre!("Failed to parse BGP message: {e:?}"))?;
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
/// Receive and unwrap as `OpenMessage`, failing if a different type arrives.
|
||||
pub async fn recv_open(&mut self) -> Result<OpenMessage> {
|
||||
match self.recv().await? {
|
||||
Message::Open(o) => {
|
||||
// Enable 4-octet ASN parsing if the peer advertised it.
|
||||
let four_byte = o.options.iter().any(|opt| {
|
||||
matches!(opt, OpenOption::Capabilities(caps)
|
||||
if caps.iter().any(|c| matches!(c, Capability::FourByteAsn { .. })))
|
||||
});
|
||||
self.ctx.four_octet_asn = Some(four_byte);
|
||||
Ok(o)
|
||||
}
|
||||
other => bail!("Expected OPEN, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
/// Send an OPEN message with the given parameters and standard capabilities.
|
||||
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 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,
|
||||
])],
|
||||
});
|
||||
self.send(&open).await
|
||||
}
|
||||
|
||||
/// Complete a full OPEN/KEEPALIVE handshake as the peer side.
|
||||
/// Returns the server's OPEN message.
|
||||
pub async fn handshake(
|
||||
&mut self,
|
||||
local_asn: u32,
|
||||
router_id: Ipv4Addr,
|
||||
hold_time: u16,
|
||||
) -> Result<OpenMessage> {
|
||||
self.send_open(local_asn, router_id, hold_time).await?;
|
||||
let server_open = self.recv_open().await?;
|
||||
// Expect server KEEPALIVE
|
||||
match self.recv().await? {
|
||||
Message::KeepAlive => {}
|
||||
other => bail!("Expected KEEPALIVE from server, got {:?}", other),
|
||||
}
|
||||
// Send our KEEPALIVE
|
||||
self.send(&Message::KeepAlive).await?;
|
||||
Ok(server_open)
|
||||
}
|
||||
|
||||
/// Send raw bytes without framing — useful for testing invalid input.
|
||||
pub async fn send_raw(&mut self, bytes: &[u8]) -> Result<()> {
|
||||
self.stream.write_all(bytes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns true if the remote end has closed the connection.
|
||||
pub async fn is_closed(&mut self) -> bool {
|
||||
let mut buf = [0u8; 1];
|
||||
match self.stream.read(&mut buf).await {
|
||||
Ok(0) | Err(_) => true,
|
||||
Ok(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
|
||||
use bgp_rib::AdjRibIn;
|
||||
use bgp_server::config::Config;
|
||||
use bgp_server::server::BgpServerBuilder;
|
||||
use eyre::Result;
|
||||
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<()>>,
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl TestServer {
|
||||
/// Start a server with `local_asn` and the default 90 s hold time.
|
||||
pub async fn start(local_asn: u32) -> Result<Self> {
|
||||
Self::start_with_hold_time(local_asn, 90).await
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
async fn start_inner(local_asn: u32, hold_time_secs: u16) -> Result<Self> {
|
||||
let config = Config {
|
||||
router_id: Ipv4Addr::new(127, 0, 0, 1),
|
||||
local_asn,
|
||||
hold_time_secs,
|
||||
listen_endpoints: vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)],
|
||||
peers: vec![],
|
||||
};
|
||||
|
||||
let bound = BgpServerBuilder::new(config).bind().await?;
|
||||
let addr = bound
|
||||
.local_addrs()
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("at least one listener");
|
||||
let adj_rib_in = bound.adj_rib_in.clone();
|
||||
|
||||
let handle = tokio::spawn(bound.run());
|
||||
Ok(Self { handle, addr, adj_rib_in })
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestServer {
|
||||
fn drop(&mut self) {
|
||||
self.handle.abort();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
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, MpReachNlriPathAttribute, MpUnreachNlriPathAttribute, NlriNextHop,
|
||||
NotificationMessage, PathAttribute, UpdateMessage,
|
||||
};
|
||||
use bgp_primitives::constants::{AddressFamilyId, SubsequentAfi};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::RibEvent;
|
||||
use eyre::Result;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Connectivity
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_server_starts() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
assert_ne!(server.addr.port(), 0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_server_accepts_tcp_connection() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let _peer = TestPeer::connect(server.addr).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_server_accepts_multiple_connections() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let mut peers = Vec::new();
|
||||
for _ in 0..5 {
|
||||
peers.push(TestPeer::connect(server.addr).await?);
|
||||
}
|
||||
assert_eq!(peers.len(), 5);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OpenSent state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Server sends an OPEN immediately after TCP is established.
|
||||
#[tokio::test]
|
||||
async fn test_server_sends_open() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
|
||||
let open = peer.recv_open().await?;
|
||||
assert_eq!(open.version, 4);
|
||||
assert_eq!(open.asn, 65000);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Server sends a NOTIFICATION and closes if we send garbage instead of OPEN.
|
||||
#[tokio::test]
|
||||
async fn test_bad_marker_does_not_crash_server() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
|
||||
let mut bad = TestPeer::connect(server.addr).await?;
|
||||
// Discard the server's OPEN, then send garbage.
|
||||
let _ = bad.recv_open().await?;
|
||||
bad.send_raw(&[0x00; 19]).await?;
|
||||
drop(bad);
|
||||
|
||||
// New connections are still accepted.
|
||||
let _good = TestPeer::connect(server.addr).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OpenConfirm → Established
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Full OPEN/KEEPALIVE handshake reaches Established.
|
||||
#[tokio::test]
|
||||
async fn test_session_established() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
|
||||
let server_open = peer
|
||||
.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
|
||||
.await?;
|
||||
|
||||
assert_eq!(server_open.version, 4);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// After Established, the server sends periodic KEEPALIVEs.
|
||||
#[tokio::test]
|
||||
async fn test_server_sends_keepalives() -> Result<()> {
|
||||
// Use a short hold time so the keepalive interval (hold/3 = 1 s) fires quickly.
|
||||
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?;
|
||||
|
||||
// We should receive a KEEPALIVE within ~1 second (hold/3).
|
||||
let msg = tokio::time::timeout(Duration::from_secs(3), peer.recv()).await??;
|
||||
assert!(
|
||||
matches!(msg, Message::KeepAlive),
|
||||
"Expected KEEPALIVE, got {:?}",
|
||||
msg
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Server closes with a NOTIFICATION when the hold timer expires.
|
||||
#[tokio::test]
|
||||
async fn test_hold_timer_expires() -> Result<()> {
|
||||
// hold_time = 3 s → server expects a KEEPALIVE within 3 s.
|
||||
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?;
|
||||
|
||||
// Don't send any keepalives. The server will send keepalives until its own
|
||||
// hold timer fires and it sends a NOTIFICATION. Drain messages until we
|
||||
// see it (or the outer timeout fires).
|
||||
let notif = tokio::time::timeout(Duration::from_secs(6), async {
|
||||
loop {
|
||||
match peer.recv().await? {
|
||||
Message::Notification(n) => return Ok::<_, eyre::Error>(n),
|
||||
Message::KeepAlive => {} // server keepalive — keep waiting
|
||||
other => eyre::bail!("Unexpected message: {:?}", other),
|
||||
}
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
assert!(
|
||||
matches!(notif.error, BgpError::HoldTimer),
|
||||
"Expected HoldTimer NOTIFICATION, got {:?}",
|
||||
notif
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A peer-sent NOTIFICATION is handled gracefully (session closes cleanly).
|
||||
#[tokio::test]
|
||||
async fn test_peer_sends_notification() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
|
||||
.await?;
|
||||
|
||||
peer.send(&Message::Notification(NotificationMessage {
|
||||
error: BgpError::Cease(CeaseSubcode::AdminShutdown),
|
||||
data: vec![],
|
||||
}))
|
||||
.await?;
|
||||
|
||||
// Server should close the connection cleanly.
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
assert!(peer.is_closed().await);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// UPDATE
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// An UPDATE message is accepted without crashing the server.
|
||||
#[tokio::test]
|
||||
async fn test_server_accepts_update() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
|
||||
.await?;
|
||||
|
||||
let update = Message::Update(UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: vec![],
|
||||
// 10.0.0.0/8 — one byte of prefix for a /8.
|
||||
announced_routes: vec![IpPrefix::new(AddressFamilyId::Ipv4, vec![10], 8)?],
|
||||
});
|
||||
peer.send(&update).await?;
|
||||
|
||||
// Server should stay up — we can still exchange a keepalive.
|
||||
peer.send(&Message::KeepAlive).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// An IPv6 UPDATE via MP_REACH_NLRI is accepted without crashing the server.
|
||||
#[tokio::test]
|
||||
async fn test_server_accepts_ipv6_update() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
|
||||
.await?;
|
||||
|
||||
// 2001:db8::/32
|
||||
let prefix = IpPrefix::new(AddressFamilyId::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],
|
||||
})],
|
||||
announced_routes: vec![],
|
||||
});
|
||||
peer.send(&update).await?;
|
||||
|
||||
// Server should stay up — we can still exchange a keepalive.
|
||||
peer.send(&Message::KeepAlive).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Adj-RIB-In introspection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// IPv4 prefix announced via the legacy NLRI field appears in the RIB.
|
||||
#[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 peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90).await?;
|
||||
|
||||
let prefix = IpPrefix::new(AddressFamilyId::Ipv4, vec![10], 8)?;
|
||||
peer.send(&Message::Update(UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: vec![],
|
||||
announced_routes: vec![prefix.clone()],
|
||||
}))
|
||||
.await?;
|
||||
|
||||
let event = tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
|
||||
assert!(
|
||||
matches!(&event, RibEvent::Announced { prefix: p, .. } if p == &prefix),
|
||||
"Expected Announced for 10.0.0.0/8, got {event:?}",
|
||||
);
|
||||
|
||||
let snapshot = server.adj_rib_in.snapshot().await;
|
||||
assert!(
|
||||
snapshot.iter().any(|e| e.prefix == prefix),
|
||||
"10.0.0.0/8 missing from RIB snapshot",
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Withdrawn IPv4 prefix is removed from the RIB.
|
||||
#[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 peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90).await?;
|
||||
|
||||
let prefix = IpPrefix::new(AddressFamilyId::Ipv4, vec![10], 8)?;
|
||||
|
||||
// Announce first.
|
||||
peer.send(&Message::Update(UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: vec![],
|
||||
announced_routes: vec![prefix.clone()],
|
||||
}))
|
||||
.await?;
|
||||
tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
|
||||
|
||||
// Now withdraw.
|
||||
peer.send(&Message::Update(UpdateMessage {
|
||||
withdrawn_routes: vec![prefix.clone()],
|
||||
path_attributes: vec![],
|
||||
announced_routes: vec![],
|
||||
}))
|
||||
.await?;
|
||||
|
||||
let event = tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
|
||||
assert!(
|
||||
matches!(&event, RibEvent::Withdrawn { prefix: p, .. } if p == &prefix),
|
||||
"Expected Withdrawn for 10.0.0.0/8, got {event:?}",
|
||||
);
|
||||
|
||||
let snapshot = server.adj_rib_in.snapshot().await;
|
||||
assert!(
|
||||
!snapshot.iter().any(|e| e.prefix == prefix),
|
||||
"10.0.0.0/8 should have been removed from RIB snapshot",
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// IPv6 prefix announced via MP_REACH_NLRI appears in the RIB.
|
||||
#[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 peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90).await?;
|
||||
|
||||
let prefix = IpPrefix::new(AddressFamilyId::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()],
|
||||
})],
|
||||
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),
|
||||
"Expected Announced for 2001:db8::/32, got {event:?}",
|
||||
);
|
||||
|
||||
let snapshot = server.adj_rib_in.snapshot().await;
|
||||
assert!(
|
||||
snapshot.iter().any(|e| e.prefix == prefix),
|
||||
"2001:db8::/32 missing from RIB snapshot",
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// IPv6 prefix withdrawn via MP_UNREACH_NLRI is removed from the RIB.
|
||||
#[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 peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90).await?;
|
||||
|
||||
let prefix = IpPrefix::new(AddressFamilyId::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()],
|
||||
})],
|
||||
announced_routes: vec![],
|
||||
}))
|
||||
.await?;
|
||||
tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
|
||||
|
||||
// Now withdraw via MP_UNREACH_NLRI.
|
||||
peer.send(&Message::Update(UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: vec![PathAttribute::MpUnreachNlri(MpUnreachNlriPathAttribute {
|
||||
afi: AddressFamilyId::Ipv6,
|
||||
safi: SubsequentAfi::Unicast,
|
||||
prefixes: vec![prefix.clone()],
|
||||
})],
|
||||
announced_routes: vec![],
|
||||
}))
|
||||
.await?;
|
||||
|
||||
let event = tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
|
||||
assert!(
|
||||
matches!(&event, RibEvent::Withdrawn { prefix: p, .. } if p == &prefix),
|
||||
"Expected Withdrawn for 2001:db8::/32, got {event:?}",
|
||||
);
|
||||
|
||||
let snapshot = server.adj_rib_in.snapshot().await;
|
||||
assert!(
|
||||
!snapshot.iter().any(|e| e.prefix == prefix),
|
||||
"2001:db8::/32 should have been removed from RIB snapshot",
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user