Phase 1 of the BGP teaching demo: the daemon can now originate routes and
advertise them to every established peer.
- rib: add LocalRib actor holding locally originated routes with owner,
origin, AS-path prepend, MED, standard and large communities. Enforces a
LocalRibPolicy (prefix allowlist, per-owner limit, owner match on
replace/withdraw) and emits LocalRibEvents.
- server: add SessionRegistry with per-session outbound queues; sessions
register on Established and deregister via a drop guard. Emits
SessionEvent::{Up,Down}.
- server: advertiser task fans LocalRib changes out to all sessions and
replays the table to new sessions. UPDATE builders handle IPv4 (legacy
NLRI) and IPv6 (MP_REACH/MP_UNREACH).
- fsm: merge the timer and timer-less established loops into one select!
with an outbound branch; reset the hold timer on any received message
per RFC 4271 §8.2.2 (previously only on KEEPALIVE).
- config: new local_route_policy proto message.
- packet: fix Communitites typo, expose CommunitiesPathAttribute inner vec.
- primitives: IpPrefix::contains.
- integration tests: announce/withdraw/replay/fan-out/policy coverage.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
257 lines
9.4 KiB
Rust
257 lines
9.4 KiB
Rust
//! 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::TestServer;
|
|
use bgp_packet::message::{
|
|
AsPathAttribute, CommunitiesPathAttribute, LargeCommunitiesPathAttribute, LargeCommunity,
|
|
NextHopPathAttribute, OriginPathAttribute, PathAttribute, UpdateMessage,
|
|
};
|
|
use bgp_primitives::ip_prefix::IpPrefix;
|
|
use bgp_rib::{LocalRibError, LocalRibPolicy, 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), "alice", attrs)
|
|
}
|
|
|
|
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?;
|
|
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(&server).await?;
|
|
let mut b = established_peer(&server).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"), Some("alice")).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(())
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Policy
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[tokio::test]
|
|
async fn test_policy_rejects_prefix_outside_allowlist() -> Result<()> {
|
|
let server = TestServer::start_with_policy(
|
|
LOCAL_ASN,
|
|
LocalRibPolicy { allowed_prefixes: vec![prefix("10.100.0.0/16")], max_routes_per_owner: 2 },
|
|
)
|
|
.await?;
|
|
let mut peer = established_peer(&server).await?;
|
|
wait_for_sessions(&server, 1).await?;
|
|
|
|
let err = server
|
|
.local_rib
|
|
.announce(route("192.0.2.0/24", LocalRouteAttrs::default()))
|
|
.await
|
|
.unwrap_err();
|
|
assert_eq!(err, LocalRibError::NotAllowed(prefix("192.0.2.0/24")));
|
|
|
|
// An allowed prefix still goes through on the same session.
|
|
server.local_rib.announce(route("10.100.9.0/24", LocalRouteAttrs::default())).await?;
|
|
let update = next_update(&mut peer).await?;
|
|
assert_eq!(update.announced_routes, vec![prefix("10.100.9.0/24")]);
|
|
Ok(())
|
|
}
|