Format the workspace with rustfmt

Default settings. No change in behaviour; the tree was last formatted
before the lecture work and had drifted to a mix of line widths.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-10-01 20:58:12 +02:00
1 parent d8e8924b4e
commit 2ab6c1c38b
27 files changed
+959 -249

No files matched your search

+31 -7
View File
@@ -78,8 +78,13 @@ async fn main() -> Result<()> {
} }
async fn run(args: &Args, mut peer: TestPeer, addr: std::net::SocketAddr) -> Result<()> { 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?; let open = peer
println!("established with {} (AS{} router-id {})", addr, open.asn, open.identifier); .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 { for p in &args.announce {
let prefix = IpPrefix::try_from(p.as_str())?; let prefix = IpPrefix::try_from(p.as_str())?;
@@ -123,9 +128,15 @@ fn announce(prefix: IpPrefix, as_path: Vec<u32>, communities: Vec<(u16, u16)>) -
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(10, 0, 0, 2))), PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(10, 0, 0, 2))),
]; ];
if !communities.is_empty() { if !communities.is_empty() {
path_attributes.push(PathAttribute::Communities(CommunitiesPathAttribute(communities))); path_attributes.push(PathAttribute::Communities(CommunitiesPathAttribute(
communities,
)));
}
UpdateMessage {
withdrawn_routes: vec![],
path_attributes,
announced_routes: vec![prefix],
} }
UpdateMessage { withdrawn_routes: vec![], path_attributes, announced_routes: vec![prefix] }
} }
async fn echo_back(peer: &mut TestPeer, u: &UpdateMessage, asn: u32) -> Result<()> { async fn echo_back(peer: &mut TestPeer, u: &UpdateMessage, asn: u32) -> Result<()> {
@@ -133,7 +144,9 @@ async fn echo_back(peer: &mut TestPeer, u: &UpdateMessage, asn: u32) -> Result<(
let mut communities = vec![]; let mut communities = vec![];
for attr in &u.path_attributes { for attr in &u.path_attributes {
match attr { match attr {
PathAttribute::AsPath(p) => as_path.extend(p.segments.iter().flat_map(|s| s.path.iter().copied())), PathAttribute::AsPath(p) => {
as_path.extend(p.segments.iter().flat_map(|s| s.path.iter().copied()))
}
PathAttribute::Communities(c) => communities = c.0.clone(), PathAttribute::Communities(c) => communities = c.0.clone(),
_ => {} _ => {}
} }
@@ -143,7 +156,12 @@ async fn echo_back(peer: &mut TestPeer, u: &UpdateMessage, asn: u32) -> Result<(
return Ok(()); return Ok(());
} }
for prefix in &u.announced_routes { for prefix in &u.announced_routes {
peer.send(&Message::Update(announce(prefix.clone(), as_path.clone(), communities.clone()))).await?; peer.send(&Message::Update(announce(
prefix.clone(),
as_path.clone(),
communities.clone(),
)))
.await?;
println!(" echoed {prefix} back with path {as_path:?}"); println!(" echoed {prefix} back with path {as_path:?}");
} }
if !u.withdrawn_routes.is_empty() { if !u.withdrawn_routes.is_empty() {
@@ -153,7 +171,13 @@ async fn echo_back(peer: &mut TestPeer, u: &UpdateMessage, asn: u32) -> Result<(
announced_routes: vec![], announced_routes: vec![],
})) }))
.await?; .await?;
println!(" echoed withdraw of {:?}", u.withdrawn_routes.iter().map(|p| p.to_string()).collect::<Vec<_>>()); println!(
" echoed withdraw of {:?}",
u.withdrawn_routes
.iter()
.map(|p| p.to_string())
.collect::<Vec<_>>()
);
} }
Ok(()) Ok(())
} }
+12 -3
View File
@@ -25,7 +25,10 @@ impl TestPeer {
/// Wraps an already-connected stream, e.g. one accepted from a server /// Wraps an already-connected stream, e.g. one accepted from a server
/// that dialled us. /// that dialled us.
pub fn from_stream(stream: TcpStream) -> Self { pub fn from_stream(stream: TcpStream) -> Self {
Self { stream, ctx: ParserContext::default() } Self {
stream,
ctx: ParserContext::default(),
}
} }
/// Send a BGP message. /// Send a BGP message.
@@ -48,7 +51,9 @@ impl TestPeer {
let mut buf = vec![0u8; total_len]; let mut buf = vec![0u8; total_len];
buf[..BGP_HEADER_LEN as usize].copy_from_slice(&header); 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) let (_, msg) = Message::from_wire(&self.ctx, &buf)
.map_err(|e| eyre!("Failed to parse BGP message: {e:?}"))?; .map_err(|e| eyre!("Failed to parse BGP message: {e:?}"))?;
@@ -90,7 +95,11 @@ impl TestPeer {
router_id: Ipv4Addr, router_id: Ipv4Addr,
hold_time: u16, hold_time: u16,
) -> Result<()> { ) -> Result<()> {
let asn_field = if local_asn > u16::MAX as u32 { 23456 } else { local_asn as u16 }; let asn_field = if local_asn > u16::MAX as u32 {
23456
} else {
local_asn as u16
};
let open = Message::Open(OpenMessage { let open = Message::Open(OpenMessage {
version: 4, version: 4,
asn: asn_field, asn: asn_field,
+8 -1
View File
@@ -62,7 +62,14 @@ impl TestServer {
let registry = bound.registry.clone(); let registry = bound.registry.clone();
let handle = tokio::spawn(bound.run()); let handle = tokio::spawn(bound.run());
Ok(Self { handle, addr, adj_rib_in, local_rib, registry, local_asn }) Ok(Self {
handle,
addr,
adj_rib_in,
local_rib,
registry,
local_asn,
})
} }
} }
+110 -35
View File
@@ -67,7 +67,9 @@ async fn test_registry_tracks_session_lifecycle() -> Result<()> {
let peer = established_peer(&server).await?; let peer = established_peer(&server).await?;
let up = tokio::time::timeout(TIMEOUT, events.recv()).await??; let up = tokio::time::timeout(TIMEOUT, events.recv()).await??;
let SessionEvent::Up(handle) = up else { panic!("expected Up, got {up:?}") }; let SessionEvent::Up(handle) = up else {
panic!("expected Up, got {up:?}")
};
assert_eq!(handle.peer.asn, PEER_ASN); assert_eq!(handle.peer.asn, PEER_ASN);
assert_eq!(handle.local_addr, server.addr); assert_eq!(handle.local_addr, server.addr);
assert!(handle.four_octet_asn); assert!(handle.four_octet_asn);
@@ -76,7 +78,10 @@ async fn test_registry_tracks_session_lifecycle() -> Result<()> {
drop(peer); drop(peer);
let down = tokio::time::timeout(TIMEOUT, events.recv()).await??; let down = tokio::time::timeout(TIMEOUT, events.recv()).await??;
assert!(matches!(down, SessionEvent::Down(p) if p == handle.peer), "got {down:?}"); assert!(
matches!(down, SessionEvent::Down(p) if p == handle.peer),
"got {down:?}"
);
assert!(server.registry.list().is_empty()); assert!(server.registry.list().is_empty());
Ok(()) Ok(())
} }
@@ -102,7 +107,11 @@ async fn test_announce_after_session_up() -> Result<()> {
prepend: 0, prepend: 0,
med: None, med: None,
communities: vec![(65000, 100), (65535, 65281)], communities: vec![(65000, 100), (65535, 65281)],
large_communities: vec![LargeCommunity { global_admin: 65000, data_1: 7, data_2: 8 }], large_communities: vec![LargeCommunity {
global_admin: 65000,
data_1: 7,
data_2: 8,
}],
}, },
)) ))
.await?; .await?;
@@ -116,9 +125,16 @@ async fn test_announce_after_session_up() -> Result<()> {
PathAttribute::Origin(OriginPathAttribute::IGP), PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![LOCAL_ASN]), AsPathAttribute::from_asns(vec![LOCAL_ASN]),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::LOCALHOST)), PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::LOCALHOST)),
PathAttribute::Communities(CommunitiesPathAttribute(vec![(65000, 100), (65535, 65281)])), PathAttribute::Communities(CommunitiesPathAttribute(vec![
(65000, 100),
(65535, 65281)
])),
PathAttribute::LargeCommunities(LargeCommunitiesPathAttribute { PathAttribute::LargeCommunities(LargeCommunitiesPathAttribute {
communities: vec![LargeCommunity { global_admin: 65000, data_1: 7, data_2: 8 }], communities: vec![LargeCommunity {
global_admin: 65000,
data_1: 7,
data_2: 8
}],
}), }),
] ]
); );
@@ -130,8 +146,14 @@ async fn test_announce_after_session_up() -> Result<()> {
#[tokio::test] #[tokio::test]
async fn test_routes_replayed_on_session_up() -> Result<()> { async fn test_routes_replayed_on_session_up() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?; let server = TestServer::start(LOCAL_ASN).await?;
server.local_rib.announce(route("10.100.1.0/24", LocalRouteAttrs::default())).await?; server
server.local_rib.announce(route("10.100.2.0/24", LocalRouteAttrs::default())).await?; .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 peer = established_peer(&server).await?;
@@ -153,7 +175,10 @@ async fn test_announce_reaches_all_sessions() -> Result<()> {
let mut b = established_peer_with_id(&server, Ipv4Addr::new(10, 0, 0, 2)).await?; let mut b = established_peer_with_id(&server, Ipv4Addr::new(10, 0, 0, 2)).await?;
wait_for_sessions(&server, 2).await?; wait_for_sessions(&server, 2).await?;
server.local_rib.announce(route("10.100.1.0/24", LocalRouteAttrs::default())).await?; server
.local_rib
.announce(route("10.100.1.0/24", LocalRouteAttrs::default()))
.await?;
for peer in [&mut a, &mut b] { for peer in [&mut a, &mut b] {
let update = next_update(peer).await?; let update = next_update(peer).await?;
@@ -168,7 +193,10 @@ async fn test_withdraw_sends_withdrawn_route() -> Result<()> {
let mut peer = established_peer(&server).await?; let mut peer = established_peer(&server).await?;
wait_for_sessions(&server, 1).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.1.0/24", LocalRouteAttrs::default()))
.await?;
let _ = next_update(&mut peer).await?; let _ = next_update(&mut peer).await?;
server.local_rib.withdraw(prefix("10.100.1.0/24")).await?; server.local_rib.withdraw(prefix("10.100.1.0/24")).await?;
@@ -186,24 +214,39 @@ async fn test_reannounce_with_prepend_and_med() -> Result<()> {
let mut peer = established_peer(&server).await?; let mut peer = established_peer(&server).await?;
wait_for_sessions(&server, 1).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.1.0/24", LocalRouteAttrs::default()))
.await?;
let first = next_update(&mut peer).await?; let first = next_update(&mut peer).await?;
assert_eq!(first.path_attributes[1], AsPathAttribute::from_asns(vec![LOCAL_ASN])); assert_eq!(
first.path_attributes[1],
AsPathAttribute::from_asns(vec![LOCAL_ASN])
);
server server
.local_rib .local_rib
.announce(route( .announce(route(
"10.100.1.0/24", "10.100.1.0/24",
LocalRouteAttrs { prepend: 3, med: Some(42), ..LocalRouteAttrs::default() }, LocalRouteAttrs {
prepend: 3,
med: Some(42),
..LocalRouteAttrs::default()
},
)) ))
.await?; .await?;
let second = next_update(&mut peer).await?; let second = next_update(&mut peer).await?;
assert_eq!(second.announced_routes, vec![prefix("10.100.1.0/24")]); 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_eq!(
assert!(second second.path_attributes[1],
.path_attributes AsPathAttribute::from_asns(vec![LOCAL_ASN; 4])
.iter() );
.any(|a| matches!(a, PathAttribute::MultiExitDisc(m) if m.0 == 42))); assert!(
second
.path_attributes
.iter()
.any(|a| matches!(a, PathAttribute::MultiExitDisc(m) if m.0 == 42))
);
Ok(()) Ok(())
} }
@@ -212,10 +255,14 @@ async fn test_reannounce_with_prepend_and_med() -> Result<()> {
async fn test_keepalives_continue_alongside_updates() -> Result<()> { async fn test_keepalives_continue_alongside_updates() -> Result<()> {
let server = TestServer::start_with_hold_time(LOCAL_ASN, 3).await?; let server = TestServer::start_with_hold_time(LOCAL_ASN, 3).await?;
let mut peer = TestPeer::connect(server.addr).await?; let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 3).await?; peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 3)
.await?;
wait_for_sessions(&server, 1).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.1.0/24", LocalRouteAttrs::default()))
.await?;
let mut saw_update = false; let mut saw_update = false;
let mut saw_keepalive = false; let mut saw_keepalive = false;
@@ -243,15 +290,17 @@ async fn test_received_routes_removed_when_session_closes() -> Result<()> {
let mut peer = established_peer(&server).await?; let mut peer = established_peer(&server).await?;
wait_for_sessions(&server, 1).await?; wait_for_sessions(&server, 1).await?;
peer.send(&bgp_packet::message::Message::Update(bgp_packet::message::UpdateMessage { peer.send(&bgp_packet::message::Message::Update(
withdrawn_routes: vec![], bgp_packet::message::UpdateMessage {
path_attributes: vec![ withdrawn_routes: vec![],
PathAttribute::Origin(OriginPathAttribute::IGP), path_attributes: vec![
AsPathAttribute::from_asns(vec![PEER_ASN]), PathAttribute::Origin(OriginPathAttribute::IGP),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(10, 0, 0, 1))), AsPathAttribute::from_asns(vec![PEER_ASN]),
], PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(10, 0, 0, 1))),
announced_routes: vec![prefix("203.0.113.0/24"), prefix("198.51.100.0/24")], ],
})) announced_routes: vec![prefix("203.0.113.0/24"), prefix("198.51.100.0/24")],
},
))
.await?; .await?;
for _ in 0..2 { for _ in 0..2 {
let ev = tokio::time::timeout(TIMEOUT, rib_events.recv()).await??; let ev = tokio::time::timeout(TIMEOUT, rib_events.recv()).await??;
@@ -300,8 +349,14 @@ async fn test_route_refresh_resends_local_routes() -> Result<()> {
let mut peer = established_peer(&server).await?; let mut peer = established_peer(&server).await?;
wait_for_sessions(&server, 1).await?; wait_for_sessions(&server, 1).await?;
server.local_rib.announce(route("10.100.1.0/24", LocalRouteAttrs::default())).await?; server
server.local_rib.announce(route("10.100.2.0/24", LocalRouteAttrs::default())).await?; .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?;
let _ = next_update(&mut peer).await?; let _ = next_update(&mut peer).await?;
@@ -314,7 +369,11 @@ async fn test_route_refresh_resends_local_routes() -> Result<()> {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
resent.sort(); resent.sort();
assert_eq!(resent, vec!["10.100.1.0/24", "10.100.2.0/24"]); 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"); assert_eq!(
server.registry.list().len(),
1,
"session must survive a route refresh"
);
Ok(()) Ok(())
} }
@@ -345,7 +404,10 @@ async fn test_shutdown_sends_cease_to_every_peer() -> Result<()> {
} }
}) })
.await??; .await??;
assert!(matches!(notif.error, BgpError::Cease(CeaseSubcode::AdminShutdown)), "{notif:?}"); assert!(
matches!(notif.error, BgpError::Cease(CeaseSubcode::AdminShutdown)),
"{notif:?}"
);
assert!(peer.is_closed().await); assert!(peer.is_closed().await);
} }
Ok(()) Ok(())
@@ -359,8 +421,15 @@ async fn test_shutdown_sends_cease_to_every_peer() -> Result<()> {
/// connects, and each session is sent them when it comes up. /// connects, and each session is sent them when it comes up.
#[tokio::test] #[tokio::test]
async fn test_configured_routes_are_announced_to_every_new_session() -> Result<()> { 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 attrs = LocalRouteAttrs {
let configured = vec![route("10.100.1.0/24", attrs), route("10.100.2.0/24", LocalRouteAttrs::default())]; 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?; let server = TestServer::start_with(LOCAL_ASN, |c| c.originate = configured).await?;
assert_eq!(server.local_rib.snapshot().await.len(), 2); assert_eq!(server.local_rib.snapshot().await.len(), 2);
@@ -369,7 +438,13 @@ async fn test_configured_routes_are_announced_to_every_new_session() -> Result<(
let mut seen = vec![next_update(&mut peer).await?, next_update(&mut peer).await?]; let mut seen = vec![next_update(&mut peer).await?, next_update(&mut peer).await?];
seen.sort_by_key(|u| u.announced_routes[0].to_string()); 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_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!(
seen[0]
.path_attributes
.contains(&PathAttribute::Communities(CommunitiesPathAttribute(vec![
(65000, 100)
])))
);
assert_eq!(seen[1].announced_routes, vec![prefix("10.100.2.0/24")]); assert_eq!(seen[1].announced_routes, vec![prefix("10.100.2.0/24")]);
} }
Ok(()) Ok(())
@@ -222,7 +222,8 @@ async fn test_rib_records_ipv4_announce() -> Result<()> {
let mut events = server.adj_rib_in.subscribe(); let mut events = server.adj_rib_in.subscribe();
let mut peer = TestPeer::connect(server.addr).await?; 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(AddressFamilyId::Ipv4, vec![10], 8)?;
peer.send(&Message::Update(UpdateMessage { peer.send(&Message::Update(UpdateMessage {
@@ -253,7 +254,8 @@ async fn test_rib_records_ipv4_withdraw() -> Result<()> {
let mut events = server.adj_rib_in.subscribe(); let mut events = server.adj_rib_in.subscribe();
let mut peer = TestPeer::connect(server.addr).await?; 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(AddressFamilyId::Ipv4, vec![10], 8)?;
@@ -295,7 +297,8 @@ async fn test_rib_records_ipv6_announce() -> Result<()> {
let mut events = server.adj_rib_in.subscribe(); let mut events = server.adj_rib_in.subscribe();
let mut peer = TestPeer::connect(server.addr).await?; 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(AddressFamilyId::Ipv6, vec![0x20, 0x01, 0x0d, 0xb8], 32)?;
peer.send(&Message::Update(UpdateMessage { peer.send(&Message::Update(UpdateMessage {
@@ -331,7 +334,8 @@ async fn test_rib_records_ipv6_withdraw() -> Result<()> {
let mut events = server.adj_rib_in.subscribe(); let mut events = server.adj_rib_in.subscribe();
let mut peer = TestPeer::connect(server.addr).await?; 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(AddressFamilyId::Ipv6, vec![0x20, 0x01, 0x0d, 0xb8], 32)?;
+33 -8
View File
@@ -33,7 +33,13 @@ async fn server_dialing(listener: &TcpListener) -> Result<TestServer> {
let endpoint: SocketAddr = listener.local_addr()?; let endpoint: SocketAddr = listener.local_addr()?;
TestServer::start_with(LOCAL_ASN, |c| { TestServer::start_with(LOCAL_ASN, |c| {
c.connect_retry = Duration::from_millis(200); 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 }]; c.peers = vec![PeerConfig {
name: "r1".into(),
endpoint,
remote_asn: PEER_ASN,
hold_time_secs: 90,
local_address: None,
}];
}) })
.await .await
} }
@@ -70,7 +76,10 @@ async fn expect_notification(peer: &mut TestPeer) -> Result<NotificationMessage>
} }
fn is_collision_cease(n: &NotificationMessage) -> bool { fn is_collision_cease(n: &NotificationMessage) -> bool {
matches!(n.error, BgpError::Cease(CeaseSubcode::ConnCollisionResolution)) matches!(
n.error,
BgpError::Cease(CeaseSubcode::ConnCollisionResolution)
)
} }
async fn wait_for_sessions(server: &TestServer, n: usize) -> Result<()> { async fn wait_for_sessions(server: &TestServer, n: usize) -> Result<()> {
@@ -112,14 +121,20 @@ async fn test_simultaneous_open_peer_higher_keeps_inbound() -> Result<()> {
// The survivor is fully functional. // The survivor is fully functional.
server server
.local_rib .local_rib
.announce(LocalRoute::new(IpPrefix::try_from("10.100.1.0/24")?, LocalRouteAttrs::default())) .announce(LocalRoute::new(
IpPrefix::try_from("10.100.1.0/24")?,
LocalRouteAttrs::default(),
))
.await?; .await?;
let update = tokio::time::timeout(TIMEOUT, inbound.recv_update()).await??; let update = tokio::time::timeout(TIMEOUT, inbound.recv_update()).await??;
assert_eq!(update.announced_routes[0].to_string(), "10.100.1.0/24"); assert_eq!(update.announced_routes[0].to_string(), "10.100.1.0/24");
// The dialer notices the surviving session and does not open another. // The dialer notices the surviving session and does not open another.
let extra = tokio::time::timeout(Duration::from_millis(700), listener.accept()).await; 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"); assert!(
extra.is_err(),
"dialer opened a second connection despite an established session"
);
Ok(()) Ok(())
} }
@@ -190,7 +205,10 @@ async fn test_established_session_wins_by_default() -> Result<()> {
assert_eq!(server.registry.list().len(), 1); assert_eq!(server.registry.list().len(), 1);
server server
.local_rib .local_rib
.announce(LocalRoute::new(IpPrefix::try_from("10.100.1.0/24")?, LocalRouteAttrs::default())) .announce(LocalRoute::new(
IpPrefix::try_from("10.100.1.0/24")?,
LocalRouteAttrs::default(),
))
.await?; .await?;
let update = tokio::time::timeout(TIMEOUT, first.recv_update()).await??; let update = tokio::time::timeout(TIMEOUT, first.recv_update()).await??;
assert_eq!(update.announced_routes[0].to_string(), "10.100.1.0/24"); assert_eq!(update.announced_routes[0].to_string(), "10.100.1.0/24");
@@ -202,7 +220,8 @@ async fn test_established_session_wins_by_default() -> Result<()> {
/// old session are flushed. /// old session are flushed.
#[tokio::test] #[tokio::test]
async fn test_restarted_peer_replaces_stale_session_when_enabled() -> Result<()> { 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 server =
TestServer::start_with(LOCAL_ASN, |c| c.collision_detect_established_state = true).await?;
let mut stale = TestPeer::connect(server.addr).await?; let mut stale = TestPeer::connect(server.addr).await?;
stale.handshake(PEER_ASN, HIGH_ID, 90).await?; stale.handshake(PEER_ASN, HIGH_ID, 90).await?;
@@ -243,7 +262,10 @@ async fn test_restarted_peer_replaces_stale_session_when_enabled() -> Result<()>
} }
}) })
.await?; .await?;
assert!(server.adj_rib_in.snapshot().await.is_empty(), "stale routes were not flushed"); assert!(
server.adj_rib_in.snapshot().await.is_empty(),
"stale routes were not flushed"
);
Ok(()) Ok(())
} }
@@ -282,6 +304,9 @@ async fn test_peer_with_our_identifier_is_refused() -> Result<()> {
let mut peer = TestPeer::connect(server.addr).await?; let mut peer = TestPeer::connect(server.addr).await?;
open_only(&mut peer, Ipv4Addr::new(127, 0, 0, 1)).await?; open_only(&mut peer, Ipv4Addr::new(127, 0, 0, 1)).await?;
let notif = expect_notification(&mut peer).await?; let notif = expect_notification(&mut peer).await?;
assert!(matches!(notif.error, BgpError::OpenMsg(OpenMsgSubcode::BadBgpId)), "{notif:?}"); assert!(
matches!(notif.error, BgpError::OpenMsg(OpenMsgSubcode::BadBgpId)),
"{notif:?}"
);
Ok(()) Ok(())
} }
+51 -13
View File
@@ -24,7 +24,13 @@ async fn server_dialing(listener: &TcpListener, remote_asn: u32) -> Result<TestS
let endpoint: SocketAddr = listener.local_addr()?; let endpoint: SocketAddr = listener.local_addr()?;
TestServer::start_with(LOCAL_ASN, |c| { TestServer::start_with(LOCAL_ASN, |c| {
c.connect_retry = Duration::from_millis(200); c.connect_retry = Duration::from_millis(200);
c.peers = vec![PeerConfig { name: "r1".into(), endpoint, remote_asn, hold_time_secs: 90, local_address: None }]; c.peers = vec![PeerConfig {
name: "r1".into(),
endpoint,
remote_asn,
hold_time_secs: 90,
local_address: None,
}];
}) })
.await .await
} }
@@ -42,7 +48,9 @@ async fn test_server_dials_configured_peer_and_establishes() -> Result<()> {
let mut peer = accept(&listener).await?; let mut peer = accept(&listener).await?;
// Both sides send OPEN on connect; the test peer's handshake order works // Both sides send OPEN on connect; the test peer's handshake order works
// whichever side opened TCP. // whichever side opened TCP.
let open = peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90).await?; let open = peer
.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
.await?;
assert_eq!(open.asn, LOCAL_ASN as u16); assert_eq!(open.asn, LOCAL_ASN as u16);
let mut events = server.registry.subscribe(); let mut events = server.registry.subscribe();
@@ -61,14 +69,25 @@ async fn test_server_dials_configured_peer_and_establishes() -> Result<()> {
assert_eq!(listed.peer.addr, listener.local_addr()?); assert_eq!(listed.peer.addr, listener.local_addr()?);
// Configured view reflects it; the dialer is not in backoff. // Configured view reflects it; the dialer is not in backoff.
let (cfg, state) = server.registry.configured_peers().into_iter().next().unwrap(); let (cfg, state) = server
.registry
.configured_peers()
.into_iter()
.next()
.unwrap();
assert_eq!(cfg.name, "r1"); assert_eq!(cfg.name, "r1");
assert!(matches!(state, DialState::Connecting | DialState::Idle), "{state:?}"); assert!(
matches!(state, DialState::Connecting | DialState::Idle),
"{state:?}"
);
// Local routes flow over the dialled session too. // Local routes flow over the dialled session too.
server server
.local_rib .local_rib
.announce(LocalRoute::new(IpPrefix::try_from("10.100.1.0/24")?, LocalRouteAttrs::default())) .announce(LocalRoute::new(
IpPrefix::try_from("10.100.1.0/24")?,
LocalRouteAttrs::default(),
))
.await?; .await?;
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??; let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
assert_eq!(update.announced_routes[0].to_string(), "10.100.1.0/24"); assert_eq!(update.announced_routes[0].to_string(), "10.100.1.0/24");
@@ -81,7 +100,9 @@ async fn test_server_reconnects_after_peer_closes() -> Result<()> {
let server = server_dialing(&listener, PEER_ASN).await?; let server = server_dialing(&listener, PEER_ASN).await?;
let mut first = accept(&listener).await?; let mut first = accept(&listener).await?;
first.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90).await?; first
.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
.await?;
tokio::time::timeout(TIMEOUT, async { tokio::time::timeout(TIMEOUT, async {
while server.registry.list().is_empty() { while server.registry.list().is_empty() {
tokio::time::sleep(Duration::from_millis(10)).await; tokio::time::sleep(Duration::from_millis(10)).await;
@@ -97,11 +118,18 @@ async fn test_server_reconnects_after_peer_closes() -> Result<()> {
} }
}) })
.await?; .await?;
let (_, state) = server.registry.configured_peers().into_iter().next().unwrap(); let (_, state) = server
.registry
.configured_peers()
.into_iter()
.next()
.unwrap();
assert!(matches!(state, DialState::Backoff { .. }), "{state:?}"); assert!(matches!(state, DialState::Backoff { .. }), "{state:?}");
let mut second = accept(&listener).await?; let mut second = accept(&listener).await?;
second.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90).await?; second
.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
.await?;
tokio::time::timeout(TIMEOUT, async { tokio::time::timeout(TIMEOUT, async {
while server.registry.list().is_empty() { while server.registry.list().is_empty() {
tokio::time::sleep(Duration::from_millis(10)).await; tokio::time::sleep(Duration::from_millis(10)).await;
@@ -117,7 +145,8 @@ async fn test_wrong_asn_is_refused_with_bad_peer_as() -> Result<()> {
let _server = server_dialing(&listener, PEER_ASN).await?; let _server = server_dialing(&listener, PEER_ASN).await?;
let mut peer = accept(&listener).await?; let mut peer = accept(&listener).await?;
peer.send_open(65099, Ipv4Addr::new(10, 0, 0, 2), 90).await?; peer.send_open(65099, Ipv4Addr::new(10, 0, 0, 2), 90)
.await?;
let _ = peer.recv_open().await?; let _ = peer.recv_open().await?;
let notif = tokio::time::timeout(TIMEOUT, async { let notif = tokio::time::timeout(TIMEOUT, async {
loop { loop {
@@ -129,7 +158,10 @@ async fn test_wrong_asn_is_refused_with_bad_peer_as() -> Result<()> {
} }
}) })
.await??; .await??;
assert!(matches!(notif.error, BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs)), "{notif:?}"); assert!(
matches!(notif.error, BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs)),
"{notif:?}"
);
// The dialer retries rather than giving up. // The dialer retries rather than giving up.
let _again = accept(&listener).await?; let _again = accept(&listener).await?;
@@ -159,11 +191,15 @@ async fn test_inbound_from_configured_address_uses_config() -> Result<()> {
bad.send_open(65099, Ipv4Addr::new(10, 0, 0, 2), 90).await?; bad.send_open(65099, Ipv4Addr::new(10, 0, 0, 2), 90).await?;
let _ = bad.recv_open().await?; let _ = bad.recv_open().await?;
let msg = tokio::time::timeout(TIMEOUT, bad.recv()).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:?}"); assert!(
matches!(&msg, Message::Notification(n) if matches!(n.error, BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs))),
"{msg:?}"
);
// Right ASN inbound → established, named. // Right ASN inbound → established, named.
let mut good = TestPeer::connect(server.addr).await?; let mut good = TestPeer::connect(server.addr).await?;
good.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90).await?; good.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
.await?;
let listed = tokio::time::timeout(TIMEOUT, async { let listed = tokio::time::timeout(TIMEOUT, async {
loop { loop {
if let Some(s) = server.registry.list().first().cloned() { if let Some(s) = server.registry.list().first().cloned() {
@@ -212,7 +248,9 @@ async fn test_local_address_is_used_as_the_source() -> Result<()> {
.await?; .await?;
let reason = tokio::time::timeout(TIMEOUT, async { let reason = tokio::time::timeout(TIMEOUT, async {
loop { loop {
if let Some((_, DialState::Backoff { last_error, .. })) = server.registry.configured_peers().into_iter().next() { if let Some((_, DialState::Backoff { last_error, .. })) =
server.registry.configured_peers().into_iter().next()
{
return last_error; return last_error;
} }
tokio::time::sleep(Duration::from_millis(20)).await; tokio::time::sleep(Duration::from_millis(20)).await;
+78 -17
View File
@@ -24,7 +24,8 @@ fn prefix(s: &str) -> IpPrefix {
async fn established_peer(server: &TestServer) -> Result<TestPeer> { async fn established_peer(server: &TestServer) -> Result<TestPeer> {
let mut peer = TestPeer::connect(server.addr).await?; let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 90).await?; peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 90)
.await?;
tokio::time::timeout(TIMEOUT, async { tokio::time::timeout(TIMEOUT, async {
while server.registry.list().is_empty() { while server.registry.list().is_empty() {
tokio::time::sleep(Duration::from_millis(10)).await; tokio::time::sleep(Duration::from_millis(10)).await;
@@ -35,7 +36,13 @@ async fn established_peer(server: &TestServer) -> Result<TestPeer> {
} }
async fn get_json(client: &reqwest::Client, url: &str) -> Result<Value> { async fn get_json(client: &reqwest::Client, url: &str) -> Result<Value> {
Ok(client.get(url).send().await?.error_for_status()?.json().await?) Ok(client
.get(url)
.send()
.await?
.error_for_status()?
.json()
.await?)
} }
#[tokio::test] #[tokio::test]
@@ -75,7 +82,11 @@ async fn test_originated_routes_are_listed() -> Result<()> {
prepend: 1, prepend: 1,
med: Some(10), med: Some(10),
communities: vec![(65000, 100), (65535, 65281)], communities: vec![(65000, 100), (65535, 65281)],
large_communities: vec![LargeCommunity { global_admin: 65000, data_1: 1, data_2: 2 }], large_communities: vec![LargeCommunity {
global_admin: 65000,
data_1: 1,
data_2: 2,
}],
..Default::default() ..Default::default()
}; };
let server = TestServer::start_with(LOCAL_ASN, |c| { let server = TestServer::start_with(LOCAL_ASN, |c| {
@@ -93,7 +104,10 @@ async fn test_originated_routes_are_listed() -> Result<()> {
assert_eq!(local[0]["med"], 10); assert_eq!(local[0]["med"], 10);
assert_eq!(local[0]["communities"], json!(["65000:100", "65535:65281"])); assert_eq!(local[0]["communities"], json!(["65000:100", "65535:65281"]));
assert_eq!(local[0]["large_communities"], json!(["65000:1:2"])); assert_eq!(local[0]["large_communities"], json!(["65000:1:2"]));
assert_eq!(get_json(&client, &format!("{base}/api/status")).await?["local_routes"], 1); assert_eq!(
get_json(&client, &format!("{base}/api/status")).await?["local_routes"],
1
);
Ok(()) Ok(())
} }
@@ -106,10 +120,23 @@ async fn test_api_is_read_only() -> Result<()> {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let body = json!({ "prefix": "10.100.1.0/24" }); let body = json!({ "prefix": "10.100.1.0/24" });
for path in ["/api/routes/local", "/api/routes/received", "/api/peers", "/api/status"] { for path in [
let post = client.post(format!("{base}{path}")).json(&body).send().await?; "/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}"); assert_eq!(post.status(), 405, "POST {path}");
let delete = client.delete(format!("{base}{path}")).json(&body).send().await?; let delete = client
.delete(format!("{base}{path}"))
.json(&body)
.send()
.await?;
assert_eq!(delete.status(), 405, "DELETE {path}"); assert_eq!(delete.status(), 405, "DELETE {path}");
} }
assert!(server.local_rib.snapshot().await.is_empty()); assert!(server.local_rib.snapshot().await.is_empty());
@@ -161,8 +188,16 @@ async fn test_event_stream() -> Result<()> {
let (base, _http) = server.serve_http().await?; let (base, _http) = server.serve_http().await?;
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let mut resp = client.get(format!("{base}/api/events")).send().await?.error_for_status()?; let mut resp = client
assert!(resp.headers()["content-type"].to_str()?.starts_with("text/event-stream")); .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?; let _peer = established_peer(&server).await?;
server server
@@ -176,13 +211,18 @@ async fn test_event_stream() -> Result<()> {
let mut seen_announce = false; let mut seen_announce = false;
tokio::time::timeout(TIMEOUT, async { tokio::time::timeout(TIMEOUT, async {
while !(seen_up && seen_announce) { while !(seen_up && seen_announce) {
let chunk = resp.chunk().await?.ok_or_else(|| eyre::eyre!("stream ended"))?; let chunk = resp
.chunk()
.await?
.ok_or_else(|| eyre::eyre!("stream ended"))?;
buf.push_str(std::str::from_utf8(&chunk)?); buf.push_str(std::str::from_utf8(&chunk)?);
for line in buf.lines().filter_map(|l| l.strip_prefix("data:")) { for line in buf.lines().filter_map(|l| l.strip_prefix("data:")) {
let ev: Value = serde_json::from_str(line.trim())?; let ev: Value = serde_json::from_str(line.trim())?;
match ev["type"].as_str() { match ev["type"].as_str() {
Some("session_up") if ev["peer"]["peer_asn"] == PEER_ASN => seen_up = true, 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, Some("local_announced") if ev["route"]["prefix"] == "10.100.1.0/24" => {
seen_announce = true
}
_ => {} _ => {}
} }
} }
@@ -201,13 +241,19 @@ async fn test_statusz_page_is_served() -> Result<()> {
let resp = client.get(format!("{base}/statusz")).send().await?; let resp = client.get(format!("{base}/statusz")).send().await?;
assert_eq!(resp.status(), 200); assert_eq!(resp.status(), 200);
assert!(resp.headers()["content-type"].to_str()?.starts_with("text/html")); assert!(
resp.headers()["content-type"]
.to_str()?
.starts_with("text/html")
);
let body = resp.text().await?; let body = resp.text().await?;
assert!(body.contains("<title>Status for ferrix_bgp</title>")); assert!(body.contains("<title>Status for ferrix_bgp</title>"));
assert!(body.contains("/api/events")); assert!(body.contains("/api/events"));
// The root redirects to the page. // The root redirects to the page.
let no_redirect = reqwest::Client::builder().redirect(reqwest::redirect::Policy::none()).build()?; let no_redirect = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()?;
let resp = no_redirect.get(format!("{base}/")).send().await?; let resp = no_redirect.get(format!("{base}/")).send().await?;
assert_eq!(resp.status(), 307); assert_eq!(resp.status(), 307);
assert_eq!(resp.headers()["location"], "/statusz"); assert_eq!(resp.headers()["location"], "/statusz");
@@ -221,7 +267,13 @@ async fn test_metricz() -> Result<()> {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let _peer = established_peer(&server).await?; let _peer = established_peer(&server).await?;
let text = client.get(format!("{base}/metricz")).send().await?.error_for_status()?.text().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 // Metrics are process-global, so other tests in this binary contribute
// too; assert on presence and a lower bound rather than exact values. // too; assert on presence and a lower bound rather than exact values.
let metric = |name: &str| -> Option<i64> { let metric = |name: &str| -> Option<i64> {
@@ -229,9 +281,18 @@ async fn test_metricz() -> Result<()> {
.find(|l| l.starts_with(name) && l[name.len()..].starts_with(' ')) .find(|l| l.starts_with(name) && l[name.len()..].starts_with(' '))
.and_then(|l| l.rsplit(' ').next()?.parse().ok()) .and_then(|l| l.rsplit(' ').next()?.parse().ok())
}; };
assert!(metric("bgp_sessions_established").unwrap_or(0) >= 1, "{text}"); assert!(
assert!(metric("bgp_messages_sent_total{type=\"open\"}").unwrap_or(0) >= 1, "{text}"); metric("bgp_sessions_established").unwrap_or(0) >= 1,
assert!(metric("bgp_messages_received_total{type=\"keepalive\"}").unwrap_or(0) >= 1, "{text}"); "{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_local_routes").is_some(), "{text}");
assert!(metric("bgp_adj_rib_in_routes").is_some(), "{text}"); assert!(metric("bgp_adj_rib_in_routes").is_some(), "{text}");
Ok(()) Ok(())
+28 -7
View File
@@ -90,14 +90,21 @@ async fn test_bird_open_and_end_of_rib_are_accepted() -> Result<()> {
}) })
.await?; .await?;
let session = server.registry.list().remove(0); 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_eq!(
session.peer.asn, 210036,
"4-octet ASN must come from the capability, not AS_TRANS"
);
assert!(session.four_octet_asn); assert!(session.four_octet_asn);
// End-of-RIB must neither break the session nor create a route. // End-of-RIB must neither break the session nor create a route.
peer.send_raw(IPV6_END_OF_RIB).await?; peer.send_raw(IPV6_END_OF_RIB).await?;
peer.send(&Message::KeepAlive).await?; peer.send(&Message::KeepAlive).await?;
tokio::time::sleep(Duration::from_millis(200)).await; tokio::time::sleep(Duration::from_millis(200)).await;
assert_eq!(server.registry.list().len(), 1, "session dropped after End-of-RIB"); assert_eq!(
server.registry.list().len(),
1,
"session dropped after End-of-RIB"
);
assert!(server.adj_rib_in.snapshot().await.is_empty()); assert!(server.adj_rib_in.snapshot().await.is_empty());
Ok(()) Ok(())
} }
@@ -107,13 +114,27 @@ async fn test_bird_open_and_end_of_rib_are_accepted() -> Result<()> {
/// an IPv6 prefix carries MP_REACH_NLRI with an IPv6 next hop and no NEXT_HOP. /// an IPv6 prefix carries MP_REACH_NLRI with an IPv6 next hop and no NEXT_HOP.
#[tokio::test] #[tokio::test]
async fn test_ipv6_prefix_update_shape() -> Result<()> { async fn test_ipv6_prefix_update_shape() -> Result<()> {
let route = LocalRoute::new(IpPrefix::try_from("2001:db8:1::/48")?, LocalRouteAttrs::default()); let route = LocalRoute::new(
IpPrefix::try_from("2001:db8:1::/48")?,
LocalRouteAttrs::default(),
);
let nh: std::net::Ipv6Addr = "fd00:179::2".parse()?; let nh: std::net::Ipv6Addr = "fd00:179::2".parse()?;
let update = bgp_server::advertise::build_announce(&route, 4200000001, nh.into()).expect("ipv6 over ipv6"); let update = bgp_server::advertise::build_announce(&route, 4200000001, nh.into())
.expect("ipv6 over ipv6");
assert!(update.announced_routes.is_empty()); assert!(update.announced_routes.is_empty());
assert!(!update.path_attributes.iter().any(|a| matches!(a, PathAttribute::NextHop(_)))); assert!(
assert!(update.path_attributes.iter().any(|a| matches!(a, PathAttribute::MpReachNlri(m) !update
if m.afi == AddressFamilyId::Ipv6 && m.prefixes[0].to_string() == "2001:db8:1::/48"))); .path_attributes
.iter()
.any(|a| matches!(a, PathAttribute::NextHop(_)))
);
assert!(
update
.path_attributes
.iter()
.any(|a| matches!(a, PathAttribute::MpReachNlri(m)
if m.afi == AddressFamilyId::Ipv6 && m.prefixes[0].to_string() == "2001:db8:1::/48"))
);
let _ = Ipv4Addr::LOCALHOST; let _ = Ipv4Addr::LOCALHOST;
Ok(()) Ok(())
} }
+20 -4
View File
@@ -19,7 +19,10 @@ pub const WELL_KNOWN: &[(&str, (u16, u16))] = &[
pub fn parse_standard(s: &str) -> Result<(u16, u16), String> { pub fn parse_standard(s: &str) -> Result<(u16, u16), String> {
let s = s.trim(); let s = s.trim();
if let Some((_, value)) = WELL_KNOWN.iter().find(|(name, _)| name.eq_ignore_ascii_case(s)) { if let Some((_, value)) = WELL_KNOWN
.iter()
.find(|(name, _)| name.eq_ignore_ascii_case(s))
{
return Ok(*value); return Ok(*value);
} }
let (high, low) = s let (high, low) = s
@@ -41,14 +44,20 @@ pub fn parse_large(s: &str) -> Result<LargeCommunity, String> {
let s = s.trim(); let s = s.trim();
let parts: Vec<&str> = s.split(':').collect(); let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 3 { if parts.len() != 3 {
return Err(format!("large community {s:?} must be GLOBAL:LOCAL1:LOCAL2")); return Err(format!(
"large community {s:?} must be GLOBAL:LOCAL1:LOCAL2"
));
} }
let parse = |part: &str| { let parse = |part: &str| {
part.trim() part.trim()
.parse::<u32>() .parse::<u32>()
.map_err(|_| format!("large community {s:?}: {part:?} is not a 32-bit number")) .map_err(|_| format!("large community {s:?}: {part:?} is not a 32-bit number"))
}; };
Ok(LargeCommunity { global_admin: parse(parts[0])?, data_1: parse(parts[1])?, data_2: parse(parts[2])? }) Ok(LargeCommunity {
global_admin: parse(parts[0])?,
data_1: parse(parts[1])?,
data_2: parse(parts[2])?,
})
} }
pub fn format_large(c: &LargeCommunity) -> String { pub fn format_large(c: &LargeCommunity) -> String {
@@ -73,7 +82,14 @@ mod tests {
#[test] #[test]
fn large_roundtrip() { fn large_roundtrip() {
let c = parse_large("65000:1:2").unwrap(); let c = parse_large("65000:1:2").unwrap();
assert_eq!(c, LargeCommunity { global_admin: 65000, data_1: 1, data_2: 2 }); assert_eq!(
c,
LargeCommunity {
global_admin: 65000,
data_1: 1,
data_2: 2
}
);
assert_eq!(format_large(&c), "65000:1:2"); assert_eq!(format_large(&c), "65000:1:2");
assert!(parse_large("65000:1").is_err()); assert!(parse_large("65000:1").is_err());
assert!(parse_large("a:b:c").is_err()); assert!(parse_large("a:b:c").is_err());
+12 -4
View File
@@ -993,11 +993,17 @@ mod tests {
let samples: &[(&str, &[u8])] = &[ let samples: &[(&str, &[u8])] = &[
("multiprotocol", &[0x01, 0x04, 0x00, 0x02, 0x00, 0x01]), ("multiprotocol", &[0x01, 0x04, 0x00, 0x02, 0x00, 0x01]),
("route refresh", &[0x02, 0x00]), ("route refresh", &[0x02, 0x00]),
("extended next hop", &[0x05, 0x06, 0x00, 0x01, 0x00, 0x01, 0x00, 0x02]), (
"extended next hop",
&[0x05, 0x06, 0x00, 0x01, 0x00, 0x01, 0x00, 0x02],
),
("extended message", &[0x06, 0x00]), ("extended message", &[0x06, 0x00]),
("role", &[0x09, 0x01, 0x00]), ("role", &[0x09, 0x01, 0x00]),
("graceful restart, aware only", &[0x40, 0x02, 0x00, 0x00]), ("graceful restart, aware only", &[0x40, 0x02, 0x00, 0x00]),
("graceful restart, one family", &[0x40, 0x06, 0x00, 0x78, 0x00, 0x02, 0x01, 0x80]), (
"graceful restart, one family",
&[0x40, 0x06, 0x00, 0x78, 0x00, 0x02, 0x01, 0x80],
),
("4-octet asn", &[0x41, 0x04, 0x00, 0x00, 0xfd, 0xe8]), ("4-octet asn", &[0x41, 0x04, 0x00, 0x00, 0xfd, 0xe8]),
("add-path", &[0x45, 0x04, 0x00, 0x02, 0x01, 0x03]), ("add-path", &[0x45, 0x04, 0x00, 0x02, 0x01, 0x03]),
("enhanced route refresh", &[0x46, 0x00]), ("enhanced route refresh", &[0x46, 0x00]),
@@ -1009,7 +1015,10 @@ mod tests {
input.extend_from_slice(trailer); input.extend_from_slice(trailer);
let (rest, _) = Capability::from_wire(&ctx, &input) let (rest, _) = Capability::from_wire(&ctx, &input)
.unwrap_or_else(|e| panic!("{name}: failed to parse: {e:?}")); .unwrap_or_else(|e| panic!("{name}: failed to parse: {e:?}"));
assert_eq!(rest, trailer, "{name}: did not return the bytes following the capability"); assert_eq!(
rest, trailer,
"{name}: did not return the bytes following the capability"
);
} }
} }
@@ -1033,5 +1042,4 @@ mod tests {
assert_eq!(caps.len(), 7, "{caps:?}"); assert_eq!(caps.len(), 7, "{caps:?}");
assert!(caps.contains(&Capability::FourByteAsn { asn: 210036 })); assert!(caps.contains(&Capability::FourByteAsn { asn: 210036 }));
} }
} }
+50 -11
View File
@@ -11,11 +11,23 @@ const EVENT_CHANNEL_CAPACITY: usize = 1024;
const CMD_CHANNEL_CAPACITY: usize = 256; const CMD_CHANNEL_CAPACITY: usize = 256;
enum RibCommand { enum RibCommand {
Announce { peer: PeerId, prefix: IpPrefix, entry: RibEntry }, Announce {
Withdraw { peer: PeerId, prefix: IpPrefix }, peer: PeerId,
prefix: IpPrefix,
entry: RibEntry,
},
Withdraw {
peer: PeerId,
prefix: IpPrefix,
},
/// Drop everything learned from `peer`, e.g. because its session ended. /// Drop everything learned from `peer`, e.g. because its session ended.
RemovePeer { peer: PeerId, reply: oneshot::Sender<usize> }, RemovePeer {
Snapshot { reply: oneshot::Sender<Vec<SnapshotEntry>> }, peer: PeerId,
reply: oneshot::Sender<usize>,
},
Snapshot {
reply: oneshot::Sender<Vec<SnapshotEntry>>,
},
} }
/// Handle to the Adj-RIB-In actor. Cheap to clone — all clones share the same /// Handle to the Adj-RIB-In actor. Cheap to clone — all clones share the same
@@ -37,7 +49,14 @@ impl AdjRibIn {
} }
pub async fn announce(&self, peer: PeerId, prefix: IpPrefix, entry: RibEntry) { pub async fn announce(&self, peer: PeerId, prefix: IpPrefix, entry: RibEntry) {
let _ = self.tx.send(RibCommand::Announce { peer, prefix, entry }).await; let _ = self
.tx
.send(RibCommand::Announce {
peer,
prefix,
entry,
})
.await;
} }
pub async fn withdraw(&self, peer: PeerId, prefix: IpPrefix) { pub async fn withdraw(&self, peer: PeerId, prefix: IpPrefix) {
@@ -49,7 +68,13 @@ impl AdjRibIn {
/// terminated session are deleted). Returns how many were removed. /// terminated session are deleted). Returns how many were removed.
pub async fn remove_peer(&self, peer: PeerId) -> usize { pub async fn remove_peer(&self, peer: PeerId) -> usize {
let (reply_tx, reply_rx) = oneshot::channel(); let (reply_tx, reply_rx) = oneshot::channel();
let _ = self.tx.send(RibCommand::RemovePeer { peer, reply: reply_tx }).await; let _ = self
.tx
.send(RibCommand::RemovePeer {
peer,
reply: reply_tx,
})
.await;
reply_rx.await.unwrap_or(0) reply_rx.await.unwrap_or(0)
} }
@@ -76,9 +101,17 @@ async fn run_actor(mut rx: mpsc::Receiver<RibCommand>, events: broadcast::Sender
while let Some(cmd) = rx.recv().await { while let Some(cmd) = rx.recv().await {
match cmd { match cmd {
// Send errors below mean there are no subscribers, which is fine. // Send errors below mean there are no subscribers, which is fine.
RibCommand::Announce { peer, prefix, entry } => { RibCommand::Announce {
peer,
prefix,
entry,
} => {
table.insert((peer, prefix.clone()), entry.clone()); table.insert((peer, prefix.clone()), entry.clone());
let _ = events.send(RibEvent::Announced { peer, prefix, entry }); let _ = events.send(RibEvent::Announced {
peer,
prefix,
entry,
});
} }
RibCommand::Withdraw { peer, prefix } => { RibCommand::Withdraw { peer, prefix } => {
if table.remove(&(peer, prefix.clone())).is_some() { if table.remove(&(peer, prefix.clone())).is_some() {
@@ -86,11 +119,17 @@ async fn run_actor(mut rx: mpsc::Receiver<RibCommand>, events: broadcast::Sender
} }
} }
RibCommand::RemovePeer { peer, reply } => { RibCommand::RemovePeer { peer, reply } => {
let prefixes: Vec<IpPrefix> = let prefixes: Vec<IpPrefix> = table
table.keys().filter(|(p, _)| *p == peer).map(|(_, prefix)| prefix.clone()).collect(); .keys()
.filter(|(p, _)| *p == peer)
.map(|(_, prefix)| prefix.clone())
.collect();
for prefix in &prefixes { for prefix in &prefixes {
table.remove(&(peer, prefix.clone())); table.remove(&(peer, prefix.clone()));
let _ = events.send(RibEvent::Withdrawn { peer, prefix: prefix.clone() }); let _ = events.send(RibEvent::Withdrawn {
peer,
prefix: prefix.clone(),
});
} }
let _ = reply.send(prefixes.len()); let _ = reply.send(prefixes.len());
} }
+4 -1
View File
@@ -26,7 +26,10 @@ pub struct RibEntry {
impl RibEntry { impl RibEntry {
pub fn new(path_attributes: Vec<PathAttribute>) -> Self { pub fn new(path_attributes: Vec<PathAttribute>) -> Self {
Self { path_attributes, received_at: SystemTime::now() } Self {
path_attributes,
received_at: SystemTime::now(),
}
} }
} }
+9 -2
View File
@@ -4,6 +4,13 @@ use crate::entry::{PeerId, RibEntry};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum RibEvent { pub enum RibEvent {
Announced { peer: PeerId, prefix: IpPrefix, entry: RibEntry }, Announced {
Withdrawn { peer: PeerId, prefix: IpPrefix }, peer: PeerId,
prefix: IpPrefix,
entry: RibEntry,
},
Withdrawn {
peer: PeerId,
prefix: IpPrefix,
},
} }
+32 -9
View File
@@ -53,7 +53,11 @@ pub struct LocalRoute {
impl LocalRoute { impl LocalRoute {
pub fn new(prefix: IpPrefix, attrs: LocalRouteAttrs) -> Self { pub fn new(prefix: IpPrefix, attrs: LocalRouteAttrs) -> Self {
Self { prefix, attrs, announced_at: SystemTime::now() } Self {
prefix,
attrs,
announced_at: SystemTime::now(),
}
} }
} }
@@ -67,13 +71,23 @@ pub enum LocalRibError {
pub enum LocalRibEvent { pub enum LocalRibEvent {
/// A route was added or its attributes replaced. /// A route was added or its attributes replaced.
Announced(LocalRoute), Announced(LocalRoute),
Withdrawn { prefix: IpPrefix }, Withdrawn {
prefix: IpPrefix,
},
} }
enum Command { enum Command {
Announce { route: LocalRoute, reply: oneshot::Sender<()> }, Announce {
Withdraw { prefix: IpPrefix, reply: oneshot::Sender<Option<LocalRoute>> }, route: LocalRoute,
Snapshot { reply: oneshot::Sender<Vec<LocalRoute>> }, reply: oneshot::Sender<()>,
},
Withdraw {
prefix: IpPrefix,
reply: oneshot::Sender<Option<LocalRoute>>,
},
Snapshot {
reply: oneshot::Sender<Vec<LocalRoute>>,
},
} }
/// Handle to the local RIB actor. Cheap to clone; all clones share one table. /// Handle to the local RIB actor. Cheap to clone; all clones share one table.
@@ -170,12 +184,16 @@ mod tests {
let mut events = rib.subscribe(); let mut events = rib.subscribe();
rib.announce(route("10.0.0.0/24")).await.unwrap(); rib.announce(route("10.0.0.0/24")).await.unwrap();
assert!(matches!(events.recv().await.unwrap(), LocalRibEvent::Announced(r) if r.prefix == prefix("10.0.0.0/24"))); assert!(
matches!(events.recv().await.unwrap(), LocalRibEvent::Announced(r) if r.prefix == prefix("10.0.0.0/24"))
);
assert_eq!(rib.snapshot().await.len(), 1); assert_eq!(rib.snapshot().await.len(), 1);
let removed = rib.withdraw(prefix("10.0.0.0/24")).await.unwrap(); let removed = rib.withdraw(prefix("10.0.0.0/24")).await.unwrap();
assert_eq!(removed.unwrap().prefix, prefix("10.0.0.0/24")); assert_eq!(removed.unwrap().prefix, prefix("10.0.0.0/24"));
assert!(matches!(events.recv().await.unwrap(), LocalRibEvent::Withdrawn { prefix: p } if p == prefix("10.0.0.0/24"))); assert!(
matches!(events.recv().await.unwrap(), LocalRibEvent::Withdrawn { prefix: p } if p == prefix("10.0.0.0/24"))
);
assert!(rib.snapshot().await.is_empty()); assert!(rib.snapshot().await.is_empty());
// Withdrawing an absent prefix is a no-op, not an error. // Withdrawing an absent prefix is a no-op, not an error.
@@ -186,8 +204,13 @@ mod tests {
async fn announcing_a_prefix_again_replaces_its_attributes() { async fn announcing_a_prefix_again_replaces_its_attributes() {
let rib = LocalRib::spawn(); let rib = LocalRib::spawn();
rib.announce(route("10.0.0.0/24")).await.unwrap(); rib.announce(route("10.0.0.0/24")).await.unwrap();
let attrs = LocalRouteAttrs { med: Some(50), ..Default::default() }; let attrs = LocalRouteAttrs {
rib.announce(LocalRoute::new(prefix("10.0.0.0/24"), attrs.clone())).await.unwrap(); med: Some(50),
..Default::default()
};
rib.announce(LocalRoute::new(prefix("10.0.0.0/24"), attrs.clone()))
.await
.unwrap();
let table = rib.snapshot().await; let table = rib.snapshot().await;
assert_eq!(table.len(), 1); assert_eq!(table.len(), 1);
+50 -14
View File
@@ -117,7 +117,11 @@ async fn send_withdraw(session: &SessionHandle, prefix: &IpPrefix) {
/// Returns `None` when the prefix family does not match the next-hop family: /// Returns `None` when the prefix family does not match the next-hop family:
/// carrying IPv4 NLRI over an IPv6 next hop (RFC 8950) or vice versa is not /// carrying IPv4 NLRI over an IPv6 next hop (RFC 8950) or vice versa is not
/// supported yet. /// supported yet.
pub fn build_announce(route: &LocalRoute, local_asn: u32, next_hop: IpAddr) -> Option<UpdateMessage> { pub fn build_announce(
route: &LocalRoute,
local_asn: u32,
next_hop: IpAddr,
) -> Option<UpdateMessage> {
let attrs = &route.attrs; let attrs = &route.attrs;
// Path attributes in ascending type-code order, as most implementations // Path attributes in ascending type-code order, as most implementations
@@ -138,7 +142,9 @@ pub fn build_announce(route: &LocalRoute, local_asn: u32, next_hop: IpAddr) -> O
} }
if let Some(med) = attrs.med { if let Some(med) = attrs.med {
path_attributes.push(PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(med))); path_attributes.push(PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(
med,
)));
} }
if !attrs.communities.is_empty() { if !attrs.communities.is_empty() {
path_attributes.push(PathAttribute::Communities(CommunitiesPathAttribute( path_attributes.push(PathAttribute::Communities(CommunitiesPathAttribute(
@@ -154,12 +160,18 @@ pub fn build_announce(route: &LocalRoute, local_asn: u32, next_hop: IpAddr) -> O
})); }));
} }
if !attrs.large_communities.is_empty() { if !attrs.large_communities.is_empty() {
path_attributes.push(PathAttribute::LargeCommunities(LargeCommunitiesPathAttribute { path_attributes.push(PathAttribute::LargeCommunities(
communities: attrs.large_communities.clone(), LargeCommunitiesPathAttribute {
})); communities: attrs.large_communities.clone(),
},
));
} }
Some(UpdateMessage { withdrawn_routes: vec![], path_attributes, announced_routes }) Some(UpdateMessage {
withdrawn_routes: vec![],
path_attributes,
announced_routes,
})
} }
/// Builds the UPDATE withdrawing `prefix`. /// Builds the UPDATE withdrawing `prefix`.
@@ -206,10 +218,15 @@ mod tests {
prepend: 2, prepend: 2,
med: Some(50), med: Some(50),
communities: vec![(65000, 100), (65535, 65281)], communities: vec![(65000, 100), (65535, 65281)],
large_communities: vec![LargeCommunity { global_admin: 65000, data_1: 1, data_2: 2 }], large_communities: vec![LargeCommunity {
global_admin: 65000,
data_1: 1,
data_2: 2,
}],
}, },
); );
let update = build_announce(&route, 65000, IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1))).unwrap(); let update =
build_announce(&route, 65000, IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1))).unwrap();
assert_eq!(update.announced_routes, vec![prefix("10.100.1.0/24")]); assert_eq!(update.announced_routes, vec![prefix("10.100.1.0/24")]);
assert!(update.withdrawn_routes.is_empty()); assert!(update.withdrawn_routes.is_empty());
@@ -220,20 +237,32 @@ mod tests {
AsPathAttribute::from_asns(vec![65000, 65000, 65000]), AsPathAttribute::from_asns(vec![65000, 65000, 65000]),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))), PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(50)), PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(50)),
PathAttribute::Communities(CommunitiesPathAttribute(vec![(65000, 100), (65535, 65281)])), PathAttribute::Communities(CommunitiesPathAttribute(vec![
(65000, 100),
(65535, 65281)
])),
PathAttribute::LargeCommunities(LargeCommunitiesPathAttribute { PathAttribute::LargeCommunities(LargeCommunitiesPathAttribute {
communities: vec![LargeCommunity { global_admin: 65000, data_1: 1, data_2: 2 }], communities: vec![LargeCommunity {
global_admin: 65000,
data_1: 1,
data_2: 2
}],
}), }),
] ]
); );
// Serialise and parse back to make sure the wire encoding is sound. // Serialise and parse back to make sure the wire encoding is sound.
let ctx = ParserContext { four_octet_asn: Some(true), address_family: None }; let ctx = ParserContext {
four_octet_asn: Some(true),
address_family: None,
};
let msg = Message::Update(update.clone()); let msg = Message::Update(update.clone());
let mut buf = BytesMut::new(); let mut buf = BytesMut::new();
msg.to_wire(&ctx, &mut buf).unwrap(); msg.to_wire(&ctx, &mut buf).unwrap();
let (_, parsed) = Message::from_wire(&ctx, &buf).unwrap(); let (_, parsed) = Message::from_wire(&ctx, &buf).unwrap();
let Message::Update(parsed) = parsed else { panic!("expected UPDATE") }; let Message::Update(parsed) = parsed else {
panic!("expected UPDATE")
};
assert_eq!(parsed, update); assert_eq!(parsed, update);
} }
@@ -244,7 +273,12 @@ mod tests {
let update = build_announce(&route, 65000, IpAddr::V6(nh)).unwrap(); let update = build_announce(&route, 65000, IpAddr::V6(nh)).unwrap();
assert!(update.announced_routes.is_empty()); 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::NextHop(_)))
);
assert!(update.path_attributes.iter().any(|a| matches!( assert!(update.path_attributes.iter().any(|a| matches!(
a, a,
PathAttribute::MpReachNlri(MpReachNlriPathAttribute { next_hop: NlriNextHop::Ipv6(h), prefixes, .. }) PathAttribute::MpReachNlri(MpReachNlriPathAttribute { next_hop: NlriNextHop::Ipv6(h), prefixes, .. })
@@ -268,6 +302,8 @@ mod tests {
let v6 = build_withdraw(&prefix("2001:db8::/32")); let v6 = build_withdraw(&prefix("2001:db8::/32"));
assert!(v6.withdrawn_routes.is_empty()); assert!(v6.withdrawn_routes.is_empty());
assert!(matches!(&v6.path_attributes[..], [PathAttribute::MpUnreachNlri(m)] if m.prefixes == vec![prefix("2001:db8::/32")])); assert!(
matches!(&v6.path_attributes[..], [PathAttribute::MpUnreachNlri(m)] if m.prefixes == vec![prefix("2001:db8::/32")])
);
} }
} }
+65 -11
View File
@@ -67,7 +67,11 @@ fn new_loses(
return false; return false;
} }
// Identifiers compare as unsigned 32-bit integers. // Identifiers compare as unsigned 32-bit integers.
let keep = if u32::from(local_id) > u32::from(remote_id) { Direction::Outbound } else { Direction::Inbound }; let keep = if u32::from(local_id) > u32::from(remote_id) {
Direction::Outbound
} else {
Direction::Inbound
};
new != keep new != keep
} }
@@ -81,17 +85,32 @@ pub struct CollisionTable {
impl CollisionTable { impl CollisionTable {
pub fn new(detect_established: bool) -> Self { pub fn new(detect_established: bool) -> Self {
Self { entries: Default::default(), next_id: Default::default(), detect_established } Self {
entries: Default::default(),
next_id: Default::default(),
detect_established,
}
} }
/// Call when a connection receives the peer's OPEN. `None` means this /// Call when a connection receives the peer's OPEN. `None` means this
/// connection lost and must be closed with Cease / Connection Collision /// connection lost and must be closed with Cease / Connection Collision
/// Resolution. Otherwise any rival has been told to close and this /// Resolution. Otherwise any rival has been told to close and this
/// connection holds the slot until the guard drops. /// connection holds the slot until the guard drops.
pub fn admit(&self, local_id: Ipv4Addr, remote_id: Ipv4Addr, direction: Direction) -> Option<ConnectionGuard> { pub fn admit(
&self,
local_id: Ipv4Addr,
remote_id: Ipv4Addr,
direction: Direction,
) -> Option<ConnectionGuard> {
let mut entries = self.entries.lock().expect("collision table poisoned"); let mut entries = self.entries.lock().expect("collision table poisoned");
if let Some(existing) = entries.get_mut(&remote_id) { if let Some(existing) = entries.get_mut(&remote_id) {
if new_loses(local_id, remote_id, existing, direction, self.detect_established) { if new_loses(
local_id,
remote_id,
existing,
direction,
self.detect_established,
) {
COLLISIONS.with_label_values(&["new"]).inc(); COLLISIONS.with_label_values(&["new"]).inc();
return None; return None;
} }
@@ -102,8 +121,21 @@ impl CollisionTable {
} }
let id = self.next_id.fetch_add(1, Ordering::Relaxed); let id = self.next_id.fetch_add(1, Ordering::Relaxed);
let (kill, killed) = oneshot::channel(); let (kill, killed) = oneshot::channel();
entries.insert(remote_id, Entry { id, direction, established: false, kill: Some(kill) }); entries.insert(
Some(ConnectionGuard { table: self.clone(), remote_id, id, killed }) remote_id,
Entry {
id,
direction,
established: false,
kill: Some(kill),
},
);
Some(ConnectionGuard {
table: self.clone(),
remote_id,
id,
killed,
})
} }
} }
@@ -131,7 +163,10 @@ impl Drop for ConnectionGuard {
// A connection that lost has already been replaced in the map; only // A connection that lost has already been replaced in the map; only
// remove the slot if it is still ours. // remove the slot if it is still ours.
let mut entries = self.table.entries.lock().expect("collision table poisoned"); let mut entries = self.table.entries.lock().expect("collision table poisoned");
if entries.get(&self.remote_id).is_some_and(|e| e.id == self.id) { if entries
.get(&self.remote_id)
.is_some_and(|e| e.id == self.id)
{
entries.remove(&self.remote_id); entries.remove(&self.remote_id);
} }
} }
@@ -145,8 +180,20 @@ mod tests {
const LOW: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1); const LOW: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1);
const HIGH: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 1); const HIGH: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 1);
fn loses(local: Ipv4Addr, remote: Ipv4Addr, existing: Direction, established: bool, new: Direction, detect: bool) -> bool { fn loses(
let e = Entry { id: 0, direction: existing, established, kill: None }; local: Ipv4Addr,
remote: Ipv4Addr,
existing: Direction,
established: bool,
new: Direction,
detect: bool,
) -> bool {
let e = Entry {
id: 0,
direction: existing,
established,
kill: None,
};
new_loses(local, remote, &e, new, detect) new_loses(local, remote, &e, new, detect)
} }
@@ -169,7 +216,10 @@ mod tests {
for (a, b) in [(LOW, HIGH), (HIGH, LOW)] { for (a, b) in [(LOW, HIGH), (HIGH, LOW)] {
let a_closes_dial_from_b = loses(a, b, Outbound, false, Inbound, false); let a_closes_dial_from_b = loses(a, b, Outbound, false, Inbound, false);
let b_closes_dial_from_a = loses(b, a, Outbound, false, Inbound, false); let b_closes_dial_from_a = loses(b, a, Outbound, false, Inbound, false);
assert_ne!(a_closes_dial_from_b, b_closes_dial_from_a, "ends disagree for a={a} b={b}"); assert_ne!(
a_closes_dial_from_b, b_closes_dial_from_a,
"ends disagree for a={a} b={b}"
);
} }
} }
@@ -206,7 +256,11 @@ mod tests {
assert!(table.admit(LOW, HIGH, Outbound).is_none()); assert!(table.admit(LOW, HIGH, Outbound).is_none());
// A different identifier never collides. // A different identifier never collides.
assert!(table.admit(LOW, Ipv4Addr::new(192, 0, 2, 2), Inbound).is_some()); assert!(
table
.admit(LOW, Ipv4Addr::new(192, 0, 2, 2), Inbound)
.is_some()
);
// Once the survivor goes away the identifier is free again. // Once the survivor goes away the identifier is free again.
drop(inb); drop(inb);
+45 -16
View File
@@ -2,9 +2,9 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::str::FromStr; use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_packet::community; use bgp_packet::community;
use bgp_packet::message::OriginPathAttribute; use bgp_packet::message::OriginPathAttribute;
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::{LocalRoute, LocalRouteAttrs}; use bgp_rib::{LocalRoute, LocalRouteAttrs};
use eyre::{Result, WrapErr, bail, eyre}; use eyre::{Result, WrapErr, bail, eyre};
@@ -49,8 +49,12 @@ pub struct PeerConfig {
impl Config { impl Config {
pub fn from_proto(proto: BgpConfig) -> Result<Self> { pub fn from_proto(proto: BgpConfig) -> Result<Self> {
let router_id = Ipv4Addr::from_str(&proto.router_id) let router_id = Ipv4Addr::from_str(&proto.router_id).map_err(|_| {
.map_err(|_| eyre!("Invalid router_id {:?}: must be a dotted-quad IPv4 address", proto.router_id))?; eyre!(
"Invalid router_id {:?}: must be a dotted-quad IPv4 address",
proto.router_id
)
})?;
if proto.local_asn == 0 { if proto.local_asn == 0 {
bail!("local_asn must be non-zero"); bail!("local_asn must be non-zero");
@@ -59,10 +63,14 @@ impl Config {
let listen_endpoints = if proto.listen_endpoints.is_empty() { let listen_endpoints = if proto.listen_endpoints.is_empty() {
vec![ vec![
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), DEFAULT_BGP_PORT), SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), DEFAULT_BGP_PORT),
SocketAddr::new(IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED), DEFAULT_BGP_PORT), SocketAddr::new(
IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED),
DEFAULT_BGP_PORT,
),
] ]
} else { } else {
proto.listen_endpoints proto
.listen_endpoints
.into_iter() .into_iter()
.map(|ep| parse_endpoint(&ep.address, ep.port)) .map(|ep| parse_endpoint(&ep.address, ep.port))
.collect::<Result<_>>()? .collect::<Result<_>>()?
@@ -108,10 +116,16 @@ impl Config {
}) })
.collect::<Result<_>>()?; .collect::<Result<_>>()?;
let originate: Vec<LocalRoute> = let originate: Vec<LocalRoute> = proto
proto.originate.iter().map(parse_originated_route).collect::<Result<_>>()?; .originate
.iter()
.map(parse_originated_route)
.collect::<Result<_>>()?;
for (i, route) in originate.iter().enumerate() { for (i, route) in originate.iter().enumerate() {
if originate[..i].iter().any(|earlier| earlier.prefix == route.prefix) { if originate[..i]
.iter()
.any(|earlier| earlier.prefix == route.prefix)
{
bail!("originate lists {} more than once", route.prefix); bail!("originate lists {} more than once", route.prefix);
} }
} }
@@ -155,8 +169,7 @@ fn parse_endpoint(address: &str, port: u32) -> Result<SocketAddr> {
port as u16 port as u16
}; };
let ip = IpAddr::from_str(address) let ip = IpAddr::from_str(address).map_err(|_| eyre!("Invalid IP address {address:?}"))?;
.map_err(|_| eyre!("Invalid IP address {address:?}"))?;
Ok(SocketAddr::new(ip, port)) Ok(SocketAddr::new(ip, port))
} }
@@ -202,7 +215,8 @@ mod tests {
use super::*; use super::*;
fn parse(text: &str) -> Result<Config> { fn parse(text: &str) -> Result<Config> {
let proto = protobuf::text_format::parse_from_str::<BgpConfig>(text).map_err(|e| eyre!("{e}"))?; let proto =
protobuf::text_format::parse_from_str::<BgpConfig>(text).map_err(|e| eyre!("{e}"))?;
Config::from_proto(proto) Config::from_proto(proto)
} }
@@ -228,7 +242,11 @@ mod tests {
let full = &config.originate[1].attrs; let full = &config.originate[1].attrs;
assert_eq!(full.origin, OriginPathAttribute::INCOMPLETE); assert_eq!(full.origin, OriginPathAttribute::INCOMPLETE);
assert_eq!(full.prepend, 2); assert_eq!(full.prepend, 2);
assert_eq!(full.med, Some(0), "an explicit MED of 0 is not the same as no MED"); assert_eq!(
full.med,
Some(0),
"an explicit MED of 0 is not the same as no MED"
);
assert_eq!(full.communities, vec![(65000, 100), (65535, 65281)]); assert_eq!(full.communities, vec![(65000, 100), (65535, 65281)]);
assert_eq!(full.large_communities.len(), 1); assert_eq!(full.large_communities.len(), 1);
} }
@@ -237,12 +255,23 @@ mod tests {
fn bad_originated_routes_are_rejected_with_the_prefix_named() { fn bad_originated_routes_are_rejected_with_the_prefix_named() {
for (body, expected) in [ for (body, expected) in [
("prefix: \"not-a-prefix\"", "Invalid originate prefix"), ("prefix: \"not-a-prefix\"", "Invalid originate prefix"),
("prefix: \"192.0.2.0/24\" origin: \"static\"", "must be IGP, EGP or INCOMPLETE"), (
"prefix: \"192.0.2.0/24\" origin: \"static\"",
"must be IGP, EGP or INCOMPLETE",
),
("prefix: \"192.0.2.0/24\" as_path_prepend: 17", "at most 16"), ("prefix: \"192.0.2.0/24\" as_path_prepend: 17", "at most 16"),
("prefix: \"192.0.2.0/24\" communities: \"70000:1\"", "originate 192.0.2.0/24"), (
("prefix: \"192.0.2.0/24\" large_communities: \"1:2\"", "originate 192.0.2.0/24"), "prefix: \"192.0.2.0/24\" communities: \"70000:1\"",
"originate 192.0.2.0/24",
),
(
"prefix: \"192.0.2.0/24\" large_communities: \"1:2\"",
"originate 192.0.2.0/24",
),
] { ] {
let err = parse(&format!("{BASE}originate {{ {body} }}")).err().expect(body); let err = parse(&format!("{BASE}originate {{ {body} }}"))
.err()
.expect(body);
assert!(format!("{err:#}").contains(expected), "{body}: {err:#}"); assert!(format!("{err:#}").contains(expected), "{body}: {err:#}");
} }
let err = parse(&format!( let err = parse(&format!(
+15 -4
View File
@@ -49,10 +49,16 @@ async fn connect(peer: &PeerConfig) -> std::io::Result<TcpStream> {
let Some(local) = peer.local_address else { let Some(local) = peer.local_address else {
return TcpStream::connect(peer.endpoint).await; return TcpStream::connect(peer.endpoint).await;
}; };
let socket = if peer.endpoint.is_ipv4() { TcpSocket::new_v4()? } else { TcpSocket::new_v6()? }; let socket = if peer.endpoint.is_ipv4() {
TcpSocket::new_v4()?
} else {
TcpSocket::new_v6()?
};
socket socket
.bind(std::net::SocketAddr::new(local, 0)) .bind(std::net::SocketAddr::new(local, 0))
.map_err(|e| std::io::Error::new(e.kind(), format!("cannot use local_address {local}: {e}")))?; .map_err(|e| {
std::io::Error::new(e.kind(), format!("cannot use local_address {local}: {e}"))
})?;
socket.connect(peer.endpoint).await socket.connect(peer.endpoint).await
} }
@@ -78,7 +84,9 @@ async fn dial_loop(peer: Arc<PeerConfig>, config: SessionConfig, connect_retry:
Ok(Ok(stream)) => { Ok(Ok(stream)) => {
info!(peer = %peer.name, endpoint = %peer.endpoint, "TCP connected, starting session"); info!(peer = %peer.name, endpoint = %peer.endpoint, "TCP connected, starting session");
backoff = connect_retry; backoff = connect_retry;
run_session(stream, peer.endpoint, config.clone()).await.map(|()| "session closed by peer".to_string()) run_session(stream, peer.endpoint, config.clone())
.await
.map(|()| "session closed by peer".to_string())
} }
Ok(Err(e)) => Err(eyre::eyre!("connect failed: {e}")), Ok(Err(e)) => Err(eyre::eyre!("connect failed: {e}")),
Err(_) => Err(eyre::eyre!("connect timed out after {CONNECT_TIMEOUT:?}")), Err(_) => Err(eyre::eyre!("connect timed out after {CONNECT_TIMEOUT:?}")),
@@ -94,7 +102,10 @@ async fn dial_loop(peer: Arc<PeerConfig>, config: SessionConfig, connect_retry:
registry.set_dial_state( registry.set_dial_state(
&peer.name, &peer.name,
DialState::Backoff { until: SystemTime::now() + backoff, last_error: reason }, DialState::Backoff {
until: SystemTime::now() + backoff,
last_error: reason,
},
); );
tokio::time::sleep(backoff).await; tokio::time::sleep(backoff).await;
backoff = (backoff * 2).min(MAX_BACKOFF); backoff = (backoff * 2).min(MAX_BACKOFF);
+60 -13
View File
@@ -19,7 +19,9 @@ use tracing::{info, warn};
use bgp_packet::message::{AS_TRANS, BGP4_VERSION}; use bgp_packet::message::{AS_TRANS, BGP4_VERSION};
use crate::collision::{CollisionTable, Direction}; use crate::collision::{CollisionTable, Direction};
use crate::metrics::{MESSAGES_RECEIVED, MESSAGES_SENT, SESSIONS_ESTABLISHED_TOTAL, message_type_name}; use crate::metrics::{
MESSAGES_RECEIVED, MESSAGES_SENT, SESSIONS_ESTABLISHED_TOTAL, message_type_name,
};
use crate::session::{SessionHandle, SessionRegistry}; use crate::session::{SessionHandle, SessionRegistry};
/// Number of outbound messages that may be queued for a session before /// Number of outbound messages that may be queued for a session before
@@ -125,7 +127,12 @@ pub async fn run_session(
if let Some(expected) = config.expected_asn if let Some(expected) = config.expected_asn
&& peer_asn != expected && peer_asn != expected
{ {
send_notification(&mut writer, BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs), &ctx).await; send_notification(
&mut writer,
BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs),
&ctx,
)
.await;
bail!("Peer {peer_addr} announced AS{peer_asn} but AS{expected} was configured"); bail!("Peer {peer_addr} announced AS{peer_asn} but AS{expected} was configured");
} }
@@ -133,7 +140,9 @@ pub async fn run_session(
// guard keeps this connection visible to later ones, and `killed` fires // guard keeps this connection visible to later ones, and `killed` fires
// if one of them wins. // if one of them wins.
let Some(mut collision_guard) = let Some(mut collision_guard) =
config.collisions.admit(config.router_id, peer_open.identifier, config.direction) config
.collisions
.admit(config.router_id, peer_open.identifier, config.direction)
else { else {
return close_for_collision(&mut writer, &ctx, peer_addr).await; return close_for_collision(&mut writer, &ctx, peer_addr).await;
}; };
@@ -152,7 +161,11 @@ pub async fn run_session(
// With a negotiated hold time of zero the hold timer does not run // With a negotiated hold time of zero the hold timer does not run
// (RFC 4271 §4.2); fall back to the large pre-negotiation value so a // (RFC 4271 §4.2); fall back to the large pre-negotiation value so a
// silent peer is still cleaned up eventually. // silent peer is still cleaned up eventually.
let hold_duration = Duration::from_secs(if negotiated_hold_time == 0 { 240 } else { negotiated_hold_time as u64 }); let hold_duration = Duration::from_secs(if negotiated_hold_time == 0 {
240
} else {
negotiated_hold_time as u64
});
let confirm = tokio::select! { let confirm = tokio::select! {
msg = recv_with_timeout(&mut reader, &ctx, hold_duration) => msg, msg = recv_with_timeout(&mut reader, &ctx, hold_duration) => msg,
@@ -181,7 +194,10 @@ pub async fn run_session(
info!(%peer_addr, peer_asn, "Session established"); info!(%peer_addr, peer_asn, "Session established");
SESSIONS_ESTABLISHED_TOTAL.inc(); SESSIONS_ESTABLISHED_TOTAL.inc();
let peer_id = PeerId { addr: peer_addr, asn: peer_asn }; let peer_id = PeerId {
addr: peer_addr,
asn: peer_asn,
};
// --- Established -------------------------------------------------------- // --- Established --------------------------------------------------------
// Register so the rest of the server can see this session and queue // Register so the rest of the server can see this session and queue
@@ -204,7 +220,11 @@ pub async fn run_session(
negotiated_hold_time, negotiated_hold_time,
peer_id, peer_id,
&config.adj_rib_in, &config.adj_rib_in,
SessionInputs { outbound: &mut outbound_rx, killed: &mut collision_guard.killed, registry: &config.registry }, SessionInputs {
outbound: &mut outbound_rx,
killed: &mut collision_guard.killed,
registry: &config.registry,
},
) )
.await; .await;
@@ -235,12 +255,20 @@ async fn established_loop(
adj_rib_in: &AdjRibIn, adj_rib_in: &AdjRibIn,
inputs: SessionInputs<'_>, inputs: SessionInputs<'_>,
) -> Result<()> { ) -> Result<()> {
let SessionInputs { outbound, killed, registry } = inputs; let SessionInputs {
outbound,
killed,
registry,
} = inputs;
// When hold_time is 0, keepalives and the hold timer are disabled // When hold_time is 0, keepalives and the hold timer are disabled
// (RFC 4271 §4.4). The timer branches below are gated off in that case; // (RFC 4271 §4.4). The timer branches below are gated off in that case;
// the placeholder durations are never observed. // the placeholder durations are never observed.
let timers_enabled = hold_time_secs != 0; let timers_enabled = hold_time_secs != 0;
let secs = if timers_enabled { hold_time_secs as u64 } else { 3 }; let secs = if timers_enabled {
hold_time_secs as u64
} else {
3
};
let hold_duration = Duration::from_secs(secs); let hold_duration = Duration::from_secs(secs);
let keepalive_interval = Duration::from_secs(secs / 3); let keepalive_interval = Duration::from_secs(secs / 3);
@@ -328,7 +356,9 @@ async fn read_message(reader: &mut OwnedReadHalf, ctx: &ParserContext) -> Result
let (_, msg) = let (_, msg) =
Message::from_wire(ctx, &buf).map_err(|e| eyre!("Failed to parse BGP message: {e:?}"))?; Message::from_wire(ctx, &buf).map_err(|e| eyre!("Failed to parse BGP message: {e:?}"))?;
MESSAGES_RECEIVED.with_label_values(&[message_type_name(&msg)]).inc(); MESSAGES_RECEIVED
.with_label_values(&[message_type_name(&msg)])
.inc();
Ok(msg) Ok(msg)
} }
@@ -350,7 +380,9 @@ async fn send_message(
let mut buf = BytesMut::new(); let mut buf = BytesMut::new();
msg.to_wire(ctx, &mut buf)?; msg.to_wire(ctx, &mut buf)?;
writer.write_all(&buf).await?; writer.write_all(&buf).await?;
MESSAGES_SENT.with_label_values(&[message_type_name(msg)]).inc(); MESSAGES_SENT
.with_label_values(&[message_type_name(msg)])
.inc();
Ok(()) Ok(())
} }
@@ -374,7 +406,12 @@ async fn close_for_collision(
peer_addr: SocketAddr, peer_addr: SocketAddr,
) -> Result<()> { ) -> Result<()> {
info!(%peer_addr, "Closing connection: lost collision resolution"); info!(%peer_addr, "Closing connection: lost collision resolution");
send_notification(writer, BgpError::Cease(CeaseSubcode::ConnCollisionResolution), ctx).await; send_notification(
writer,
BgpError::Cease(CeaseSubcode::ConnCollisionResolution),
ctx,
)
.await;
bail!("Connection to {peer_addr} closed by collision resolution"); bail!("Connection to {peer_addr} closed by collision resolution");
} }
@@ -413,7 +450,12 @@ async fn apply_update(
rib.withdraw(peer, prefix.clone()).await; rib.withdraw(peer, prefix.clone()).await;
} }
for prefix in announced { for prefix in announced {
rib.announce(peer, prefix.clone(), RibEntry::new(path_attributes.to_vec())).await; rib.announce(
peer,
prefix.clone(),
RibEntry::new(path_attributes.to_vec()),
)
.await;
} }
// Multiprotocol attributes carry NLRI for all other AFIs. // Multiprotocol attributes carry NLRI for all other AFIs.
@@ -421,7 +463,12 @@ async fn apply_update(
match attr { match attr {
PathAttribute::MpReachNlri(mp) => { PathAttribute::MpReachNlri(mp) => {
for prefix in &mp.prefixes { for prefix in &mp.prefixes {
rib.announce(peer, prefix.clone(), RibEntry::new(path_attributes.to_vec())).await; rib.announce(
peer,
prefix.clone(),
RibEntry::new(path_attributes.to_vec()),
)
.await;
} }
} }
PathAttribute::MpUnreachNlri(mp) => { PathAttribute::MpUnreachNlri(mp) => {
+29 -11
View File
@@ -9,23 +9,37 @@ use prometheus::{
}; };
pub static MESSAGES_SENT: LazyLock<IntCounterVec> = LazyLock::new(|| { pub static MESSAGES_SENT: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!("bgp_messages_sent_total", "BGP messages sent, by type", &["type"]) register_int_counter_vec!(
.expect("metric registration") "bgp_messages_sent_total",
"BGP messages sent, by type",
&["type"]
)
.expect("metric registration")
}); });
pub static MESSAGES_RECEIVED: LazyLock<IntCounterVec> = LazyLock::new(|| { pub static MESSAGES_RECEIVED: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!("bgp_messages_received_total", "BGP messages received, by type", &["type"]) register_int_counter_vec!(
.expect("metric registration") "bgp_messages_received_total",
"BGP messages received, by type",
&["type"]
)
.expect("metric registration")
}); });
pub static SESSIONS_ESTABLISHED_TOTAL: LazyLock<IntCounter> = LazyLock::new(|| { pub static SESSIONS_ESTABLISHED_TOTAL: LazyLock<IntCounter> = LazyLock::new(|| {
register_int_counter!("bgp_sessions_established_total", "Sessions that reached Established") register_int_counter!(
.expect("metric registration") "bgp_sessions_established_total",
"Sessions that reached Established"
)
.expect("metric registration")
}); });
pub static SESSIONS_ESTABLISHED: LazyLock<IntGauge> = LazyLock::new(|| { pub static SESSIONS_ESTABLISHED: LazyLock<IntGauge> = LazyLock::new(|| {
register_int_gauge!("bgp_sessions_established", "Sessions currently in Established") register_int_gauge!(
.expect("metric registration") "bgp_sessions_established",
"Sessions currently in Established"
)
.expect("metric registration")
}); });
pub static COLLISIONS: LazyLock<IntCounterVec> = LazyLock::new(|| { pub static COLLISIONS: LazyLock<IntCounterVec> = LazyLock::new(|| {
@@ -38,12 +52,16 @@ pub static COLLISIONS: LazyLock<IntCounterVec> = LazyLock::new(|| {
}); });
pub static LOCAL_ROUTES: LazyLock<IntGauge> = LazyLock::new(|| { pub static LOCAL_ROUTES: LazyLock<IntGauge> = LazyLock::new(|| {
register_int_gauge!("bgp_local_routes", "Locally originated routes").expect("metric registration") register_int_gauge!("bgp_local_routes", "Locally originated routes")
.expect("metric registration")
}); });
pub static ADJ_RIB_IN_ROUTES: LazyLock<IntGauge> = LazyLock::new(|| { pub static ADJ_RIB_IN_ROUTES: LazyLock<IntGauge> = LazyLock::new(|| {
register_int_gauge!("bgp_adj_rib_in_routes", "Routes held in Adj-RIB-In across all peers") register_int_gauge!(
.expect("metric registration") "bgp_adj_rib_in_routes",
"Routes held in Adj-RIB-In across all peers"
)
.expect("metric registration")
}); });
/// Forces registration of every metric so they appear in scrapes before /// Forces registration of every metric so they appear in scrapes before
+20 -4
View File
@@ -57,7 +57,10 @@ impl BgpServerBuilder {
// each session is sent the whole table when it comes up. // each session is sent the whole table when it comes up.
let local_rib = LocalRib::spawn(); let local_rib = LocalRib::spawn();
for route in &self.config.originate { for route in &self.config.originate {
local_rib.announce(route.clone()).await.wrap_err("Local RIB stopped during startup")?; local_rib
.announce(route.clone())
.await
.wrap_err("Local RIB stopped during startup")?;
} }
Ok(BgpServer { Ok(BgpServer {
listeners, listeners,
@@ -67,7 +70,11 @@ impl BgpServerBuilder {
self.config self.config
.peers .peers
.iter() .iter()
.map(|p| ConfiguredPeer { name: p.name.clone(), endpoint: p.endpoint, remote_asn: p.remote_asn }) .map(|p| ConfiguredPeer {
name: p.name.clone(),
endpoint: p.endpoint,
remote_asn: p.remote_asn,
})
.collect(), .collect(),
), ),
config: self.config, config: self.config,
@@ -116,7 +123,11 @@ impl BgpServer {
self.registry.clone(), self.registry.clone(),
self.config.local_asn, self.config.local_asn,
); );
let _dialers = spawn_dialers(&self.config.peers, &session_config, self.config.connect_retry); let _dialers = spawn_dialers(
&self.config.peers,
&session_config,
self.config.connect_retry,
);
let mut accepting: FuturesUnordered<_> = self let mut accepting: FuturesUnordered<_> = self
.listeners .listeners
@@ -130,7 +141,12 @@ impl BgpServer {
// Inbound connections from a configured peer inherit its // Inbound connections from a configured peer inherit its
// name, expected ASN and hold time. // name, expected ASN and hold time.
let mut config = session_config.clone(); let mut config = session_config.clone();
if let Some(peer) = self.config.peers.iter().find(|p| p.endpoint.ip() == peer_addr.ip()) { if let Some(peer) = self
.config
.peers
.iter()
.find(|p| p.endpoint.ip() == peer_addr.ip())
{
config.peer_name = Some(peer.name.clone()); config.peer_name = Some(peer.name.clone());
config.expected_asn = Some(peer.remote_asn); config.expected_asn = Some(peer.remote_asn);
config.hold_time_secs = peer.hold_time_secs; config.hold_time_secs = peer.hold_time_secs;
+55 -11
View File
@@ -30,7 +30,10 @@ pub enum DialState {
Idle, Idle,
Connecting, Connecting,
/// The last attempt failed or the session ended; waiting to retry. /// The last attempt failed or the session ended; waiting to retry.
Backoff { until: SystemTime, last_error: String }, Backoff {
until: SystemTime,
last_error: String,
},
} }
/// A peer from the configuration file, whether or not it is currently up. /// A peer from the configuration file, whether or not it is currently up.
@@ -72,13 +75,24 @@ impl SessionHandle {
four_octet_asn: bool, four_octet_asn: bool,
outbound: mpsc::Sender<Message>, outbound: mpsc::Sender<Message>,
) -> Self { ) -> Self {
Self { peer, name, direction, local_addr, established_at: SystemTime::now(), four_octet_asn, outbound } Self {
peer,
name,
direction,
local_addr,
established_at: SystemTime::now(),
four_octet_asn,
outbound,
}
} }
/// Queues a message for transmission on this session. Waits for queue /// Queues a message for transmission on this session. Waits for queue
/// space if the session's writer is behind. /// space if the session's writer is behind.
pub async fn send(&self, msg: Message) -> Result<(), SessionClosed> { pub async fn send(&self, msg: Message) -> Result<(), SessionClosed> {
self.outbound.send(msg).await.map_err(|_| SessionClosed(self.peer)) self.outbound
.send(msg)
.await
.map_err(|_| SessionClosed(self.peer))
} }
} }
@@ -114,7 +128,10 @@ impl SessionRegistry {
pub fn with_configured(configured: Vec<ConfiguredPeer>) -> Self { pub fn with_configured(configured: Vec<ConfiguredPeer>) -> Self {
let (events, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY); let (events, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
let dial_states = configured.iter().map(|p| (p.name.clone(), DialState::Idle)).collect(); let dial_states = configured
.iter()
.map(|p| (p.name.clone(), DialState::Idle))
.collect();
Self { Self {
inner: Arc::new(RwLock::new(HashMap::new())), inner: Arc::new(RwLock::new(HashMap::new())),
configured: Arc::new(configured), configured: Arc::new(configured),
@@ -130,7 +147,12 @@ impl SessionRegistry {
let states = self.dial_states.read().expect("dial states poisoned"); let states = self.dial_states.read().expect("dial states poisoned");
self.configured self.configured
.iter() .iter()
.map(|p| (p.clone(), states.get(&p.name).cloned().unwrap_or(DialState::Idle))) .map(|p| {
(
p.clone(),
states.get(&p.name).cloned().unwrap_or(DialState::Idle),
)
})
.collect() .collect()
} }
@@ -140,7 +162,10 @@ impl SessionRegistry {
} }
pub fn set_dial_state(&self, name: &str, state: DialState) { pub fn set_dial_state(&self, name: &str, state: DialState) {
self.dial_states.write().expect("dial states poisoned").insert(name.to_string(), state); self.dial_states
.write()
.expect("dial states poisoned")
.insert(name.to_string(), state);
} }
/// An established session with this remote IP and ASN, regardless of /// An established session with this remote IP and ASN, regardless of
@@ -158,19 +183,34 @@ impl SessionRegistry {
/// removes the session again when dropped. /// removes the session again when dropped.
pub fn register(&self, handle: SessionHandle) -> SessionGuard { pub fn register(&self, handle: SessionHandle) -> SessionGuard {
let peer = handle.peer; let peer = handle.peer;
self.inner.write().expect("session registry poisoned").insert(peer, handle.clone()); self.inner
.write()
.expect("session registry poisoned")
.insert(peer, handle.clone());
SESSIONS_ESTABLISHED.inc(); SESSIONS_ESTABLISHED.inc();
let _ = self.events.send(SessionEvent::Up(handle)); let _ = self.events.send(SessionEvent::Up(handle));
SessionGuard { registry: self.clone(), peer } SessionGuard {
registry: self.clone(),
peer,
}
} }
pub fn get(&self, peer: &PeerId) -> Option<SessionHandle> { pub fn get(&self, peer: &PeerId) -> Option<SessionHandle> {
self.inner.read().expect("session registry poisoned").get(peer).cloned() self.inner
.read()
.expect("session registry poisoned")
.get(peer)
.cloned()
} }
/// All currently established sessions, in no particular order. /// All currently established sessions, in no particular order.
pub fn list(&self) -> Vec<SessionHandle> { pub fn list(&self) -> Vec<SessionHandle> {
self.inner.read().expect("session registry poisoned").values().cloned().collect() self.inner
.read()
.expect("session registry poisoned")
.values()
.cloned()
.collect()
} }
/// Called by a session when its peer asks for a route refresh. /// Called by a session when its peer asks for a route refresh.
@@ -212,7 +252,11 @@ impl SessionRegistry {
} }
fn remove(&self, peer: &PeerId) { fn remove(&self, peer: &PeerId) {
let removed = self.inner.write().expect("session registry poisoned").remove(peer); let removed = self
.inner
.write()
.expect("session registry poisoned")
.remove(peer);
if removed.is_some() { if removed.is_some() {
SESSIONS_ESTABLISHED.dec(); SESSIONS_ESTABLISHED.dec();
let _ = self.events.send(SessionEvent::Down(*peer)); let _ = self.events.send(SessionEvent::Down(*peer));
+56 -14
View File
@@ -18,7 +18,9 @@ use crate::AppState;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
fn unix_secs(t: SystemTime) -> u64 { fn unix_secs(t: SystemTime) -> u64 {
t.duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0) t.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@@ -76,7 +78,9 @@ impl PeerView {
let (state_name, last_error, retry_at_unix) = match state { let (state_name, last_error, retry_at_unix) = match state {
DialState::Idle => ("Idle", None, None), DialState::Idle => ("Idle", None, None),
DialState::Connecting => ("Connecting", None, None), DialState::Connecting => ("Connecting", None, None),
DialState::Backoff { until, last_error } => ("Backoff", Some(last_error.clone()), Some(unix_secs(*until))), DialState::Backoff { until, last_error } => {
("Backoff", Some(last_error.clone()), Some(unix_secs(*until)))
}
}; };
Self { Self {
name: Some(peer.name.clone()), name: Some(peer.name.clone()),
@@ -103,17 +107,26 @@ pub fn peer_views(registry: &SessionRegistry) -> Vec<PeerView> {
.configured_peers() .configured_peers()
.iter() .iter()
.map(|(peer, state)| { .map(|(peer, state)| {
match sessions.iter().find(|s| s.peer.addr.ip() == peer.endpoint.ip() && s.peer.asn == peer.remote_asn) { match sessions
.iter()
.find(|s| s.peer.addr.ip() == peer.endpoint.ip() && s.peer.asn == peer.remote_asn)
{
Some(session) => { Some(session) => {
claimed.insert(session.peer); claimed.insert(session.peer);
PeerView { name: Some(peer.name.clone()), ..PeerView::from(session) } PeerView {
name: Some(peer.name.clone()),
..PeerView::from(session)
}
} }
None => PeerView::from_configured(peer, state), None => PeerView::from_configured(peer, state),
} }
}) })
.collect(); .collect();
let mut rest: Vec<PeerView> = let mut rest: Vec<PeerView> = sessions
sessions.iter().filter(|s| !claimed.contains(&s.peer)).map(PeerView::from).collect(); .iter()
.filter(|s| !claimed.contains(&s.peer))
.map(PeerView::from)
.collect();
rest.sort_by(|a, b| a.peer_addr.cmp(&b.peer_addr)); rest.sort_by(|a, b| a.peer_addr.cmp(&b.peer_addr));
views.extend(rest); views.extend(rest);
views views
@@ -137,8 +150,19 @@ impl From<&LocalRoute> for LocalRouteView {
origin: origin_name(r.attrs.origin).to_string(), origin: origin_name(r.attrs.origin).to_string(),
prepend: r.attrs.prepend, prepend: r.attrs.prepend,
med: r.attrs.med, med: r.attrs.med,
communities: r.attrs.communities.iter().copied().map(community::format_standard).collect(), communities: r
large_communities: r.attrs.large_communities.iter().map(community::format_large).collect(), .attrs
.communities
.iter()
.copied()
.map(community::format_standard)
.collect(),
large_communities: r
.attrs
.large_communities
.iter()
.map(community::format_large)
.collect(),
announced_at_unix: unix_secs(r.announced_at), announced_at_unix: unix_secs(r.announced_at),
} }
} }
@@ -163,14 +187,22 @@ pub fn summarize(attrs: &[PathAttribute]) -> AttrSummary {
match attr { match attr {
PathAttribute::Origin(o) => s.origin = Some(origin_name(*o).to_string()), PathAttribute::Origin(o) => s.origin = Some(origin_name(*o).to_string()),
PathAttribute::AsPath(p) => { PathAttribute::AsPath(p) => {
s.as_path = p.segments.iter().flat_map(|seg| seg.path.iter().copied()).collect() s.as_path = p
.segments
.iter()
.flat_map(|seg| seg.path.iter().copied())
.collect()
} }
PathAttribute::NextHop(nh) => s.next_hop = Some(nh.0.to_string()), PathAttribute::NextHop(nh) => s.next_hop = Some(nh.0.to_string()),
PathAttribute::MpReachNlri(mp) => s.next_hop = Some(format!("{:?}", mp.next_hop)), PathAttribute::MpReachNlri(mp) => s.next_hop = Some(format!("{:?}", mp.next_hop)),
PathAttribute::MultiExitDisc(m) => s.med = Some(m.0), PathAttribute::MultiExitDisc(m) => s.med = Some(m.0),
PathAttribute::LocalPref(l) => s.local_pref = Some(l.0), PathAttribute::LocalPref(l) => s.local_pref = Some(l.0),
PathAttribute::Communities(c) => { PathAttribute::Communities(c) => {
s.communities = c.0.iter().copied().map(community::format_standard).collect() s.communities =
c.0.iter()
.copied()
.map(community::format_standard)
.collect()
} }
PathAttribute::LargeCommunities(l) => { PathAttribute::LargeCommunities(l) => {
s.large_communities = l.communities.iter().map(community::format_large).collect() s.large_communities = l.communities.iter().map(community::format_large).collect()
@@ -250,15 +282,25 @@ pub async fn peers(State(state): State<AppState>) -> Json<Vec<PeerView>> {
} }
pub async fn local_routes(State(state): State<AppState>) -> Json<Vec<LocalRouteView>> { pub async fn local_routes(State(state): State<AppState>) -> Json<Vec<LocalRouteView>> {
let mut routes: Vec<LocalRouteView> = let mut routes: Vec<LocalRouteView> = state
state.local_rib.snapshot().await.iter().map(LocalRouteView::from).collect(); .local_rib
.snapshot()
.await
.iter()
.map(LocalRouteView::from)
.collect();
routes.sort_by(|a, b| a.prefix.cmp(&b.prefix)); routes.sort_by(|a, b| a.prefix.cmp(&b.prefix));
Json(routes) Json(routes)
} }
pub async fn received_routes(State(state): State<AppState>) -> Json<Vec<ReceivedRouteView>> { pub async fn received_routes(State(state): State<AppState>) -> Json<Vec<ReceivedRouteView>> {
let mut routes: Vec<ReceivedRouteView> = let mut routes: Vec<ReceivedRouteView> = state
state.adj_rib_in.snapshot().await.iter().map(ReceivedRouteView::from).collect(); .adj_rib_in
.snapshot()
.await
.iter()
.map(ReceivedRouteView::from)
.collect();
routes.sort_by(|a, b| (&a.prefix, &a.peer_addr).cmp(&(&b.prefix, &b.peer_addr))); routes.sort_by(|a, b| (&a.prefix, &a.peer_addr).cmp(&(&b.prefix, &b.peer_addr)));
Json(routes) Json(routes)
} }
+68 -23
View File
@@ -18,40 +18,81 @@ use crate::api::{LocalRouteView, PeerView, ReceivedRouteView, summarize};
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")] #[serde(tag = "type", rename_all = "snake_case")]
pub enum WireEvent { pub enum WireEvent {
SessionUp { peer: PeerView }, SessionUp {
SessionDown { peer_addr: String, peer_asn: u32 }, peer: PeerView,
},
SessionDown {
peer_addr: String,
peer_asn: u32,
},
/// The peer asked us to send all our routes again (ROUTE-REFRESH). /// The peer asked us to send all our routes again (ROUTE-REFRESH).
RouteRefreshRequested { peer: PeerView }, RouteRefreshRequested {
LocalAnnounced { route: LocalRouteView }, peer: PeerView,
LocalWithdrawn { prefix: String }, },
ReceivedAnnounced { route: ReceivedRouteView }, LocalAnnounced {
ReceivedWithdrawn { peer_addr: String, peer_asn: u32, prefix: String }, route: LocalRouteView,
},
LocalWithdrawn {
prefix: String,
},
ReceivedAnnounced {
route: ReceivedRouteView,
},
ReceivedWithdrawn {
peer_addr: String,
peer_asn: u32,
prefix: String,
},
/// The client fell behind and missed `missed` events on `stream`; it /// The client fell behind and missed `missed` events on `stream`; it
/// should refetch the corresponding table. /// should refetch the corresponding table.
Lagged { stream: &'static str, missed: u64 }, Lagged {
stream: &'static str,
missed: u64,
},
} }
impl WireEvent { impl WireEvent {
fn from_session(ev: Result<SessionEvent, BroadcastStreamRecvError>) -> Self { fn from_session(ev: Result<SessionEvent, BroadcastStreamRecvError>) -> Self {
match ev { match ev {
Ok(SessionEvent::Up(h)) => Self::SessionUp { peer: PeerView::from(&h) }, Ok(SessionEvent::Up(h)) => Self::SessionUp {
Ok(SessionEvent::Down(p)) => Self::SessionDown { peer_addr: p.addr.to_string(), peer_asn: p.asn }, peer: PeerView::from(&h),
Ok(SessionEvent::RefreshRequested(h)) => Self::RouteRefreshRequested { peer: PeerView::from(&h) }, },
Err(BroadcastStreamRecvError::Lagged(n)) => Self::Lagged { stream: "sessions", missed: n }, Ok(SessionEvent::Down(p)) => Self::SessionDown {
peer_addr: p.addr.to_string(),
peer_asn: p.asn,
},
Ok(SessionEvent::RefreshRequested(h)) => Self::RouteRefreshRequested {
peer: PeerView::from(&h),
},
Err(BroadcastStreamRecvError::Lagged(n)) => Self::Lagged {
stream: "sessions",
missed: n,
},
} }
} }
fn from_local(ev: Result<LocalRibEvent, BroadcastStreamRecvError>) -> Self { fn from_local(ev: Result<LocalRibEvent, BroadcastStreamRecvError>) -> Self {
match ev { match ev {
Ok(LocalRibEvent::Announced(r)) => Self::LocalAnnounced { route: LocalRouteView::from(&r) }, Ok(LocalRibEvent::Announced(r)) => Self::LocalAnnounced {
Ok(LocalRibEvent::Withdrawn { prefix }) => Self::LocalWithdrawn { prefix: prefix.to_string() }, route: LocalRouteView::from(&r),
Err(BroadcastStreamRecvError::Lagged(n)) => Self::Lagged { stream: "local_routes", missed: n }, },
Ok(LocalRibEvent::Withdrawn { prefix }) => Self::LocalWithdrawn {
prefix: prefix.to_string(),
},
Err(BroadcastStreamRecvError::Lagged(n)) => Self::Lagged {
stream: "local_routes",
missed: n,
},
} }
} }
fn from_received(ev: Result<RibEvent, BroadcastStreamRecvError>) -> Self { fn from_received(ev: Result<RibEvent, BroadcastStreamRecvError>) -> Self {
match ev { match ev {
Ok(RibEvent::Announced { peer, prefix, entry }) => Self::ReceivedAnnounced { Ok(RibEvent::Announced {
peer,
prefix,
entry,
}) => Self::ReceivedAnnounced {
route: ReceivedRouteView { route: ReceivedRouteView {
peer_addr: peer.addr.to_string(), peer_addr: peer.addr.to_string(),
peer_asn: peer.asn, peer_asn: peer.asn,
@@ -70,7 +111,10 @@ impl WireEvent {
peer_asn: peer.asn, peer_asn: peer.asn,
prefix: prefix.to_string(), prefix: prefix.to_string(),
}, },
Err(BroadcastStreamRecvError::Lagged(n)) => Self::Lagged { stream: "received_routes", missed: n }, Err(BroadcastStreamRecvError::Lagged(n)) => Self::Lagged {
stream: "received_routes",
missed: n,
},
} }
} }
} }
@@ -82,12 +126,13 @@ pub async fn stream(
let local = BroadcastStream::new(state.local_rib.subscribe()).map(WireEvent::from_local); let local = BroadcastStream::new(state.local_rib.subscribe()).map(WireEvent::from_local);
let received = BroadcastStream::new(state.adj_rib_in.subscribe()).map(WireEvent::from_received); let received = BroadcastStream::new(state.adj_rib_in.subscribe()).map(WireEvent::from_received);
let merged = futures::stream::select(sessions, futures::stream::select(local, received)).map(|ev| { let merged =
Ok(Event::default() futures::stream::select(sessions, futures::stream::select(local, received)).map(|ev| {
.event("bgp") Ok(Event::default()
.json_data(&ev) .event("bgp")
.unwrap_or_else(|e| Event::default().event("error").data(e.to_string()))) .json_data(&ev)
}); .unwrap_or_else(|e| Event::default().event("error").data(e.to_string())))
});
Sse::new(merged).keep_alive(KeepAlive::default()) Sse::new(merged).keep_alive(KeepAlive::default())
} }
+4 -1
View File
@@ -86,7 +86,10 @@ pub struct AppState {
const STATUSZ_HTML: &str = include_str!("../static/statusz.html"); const STATUSZ_HTML: &str = include_str!("../static/statusz.html");
async fn statusz() -> impl IntoResponse { async fn statusz() -> impl IntoResponse {
([(header::CONTENT_TYPE, "text/html; charset=utf-8")], Html(STATUSZ_HTML)) (
[(header::CONTENT_TYPE, "text/html; charset=utf-8")],
Html(STATUSZ_HTML),
)
} }
/// Builds the router. Exposed so tests can drive it without a socket. /// Builds the router. Exposed so tests can drive it without a socket.
+6 -1
View File
@@ -15,7 +15,12 @@ pub async fn metricz(State(state): State<AppState>) -> impl IntoResponse {
let mut buf = Vec::new(); let mut buf = Vec::new();
match TextEncoder::new().encode(&prometheus::gather(), &mut buf) { match TextEncoder::new().encode(&prometheus::gather(), &mut buf) {
Ok(()) => (StatusCode::OK, [(header::CONTENT_TYPE, "text/plain; version=0.0.4")], buf).into_response(), Ok(()) => (
StatusCode::OK,
[(header::CONTENT_TYPE, "text/plain; version=0.0.4")],
buf,
)
.into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }
} }