Use the sharded table in the server: sessions choose best routes
The server now keeps what it learns in table::RibTable. The Adj-RIB-In actor is gone, and with it the queue every UPDATE from every session went through. - A session takes a number in the table when it is established and writes its UPDATEs into the table itself. Its routes are removed when it ends, on a blocking thread since that looks at every prefix; a session torn down without cleaning up still has them removed. - For every prefix the best route is chosen (RFC 4271 §9.1.2.2) as routes come and go. Nothing acts on the choice yet beyond showing it. - The UPDATE decoder hands over the bytes of the attributes it kept, so the table finds the attribute set from them without writing the attributes out again. On a session with two-octet AS numbers the bytes are not in the table's form, and the parsed attributes are used. - /api/routes/received marks the best route of each prefix and returns at most `limit` routes (default 1000): the table can hold hundreds of millions. /api/status has the totals, and the status page says how many it is showing. The time a route was received is no longer kept or shown. - The event stream reports changes of best route (best_changed, best_withdrawn) in place of every route received. Each client follows the table's journal with a reader of its own, from the moment it connects: table::RibTable::reader_from_now. - Metrics: bgp_rib_routes, bgp_rib_prefixes, bgp_rib_attribute_sets and bgp_rib_journal_entries replace bgp_adj_rib_in_routes. - A line is no longer logged at INFO for every UPDATE sent or received; a full table is a million of them. They are at DEBUG. - A table with no room for another peer ends the session with Cease / Out of Resources. Four peers sending a synthetic full table each over TCP, 4.9 million routes: 0.80 s, against 4.76 s before. One peer: 0.45 s against 0.69 s. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
3b20576971
commit
31aa3a4563
43 files changed
+1213
-1212
No files matched your search
Generated
+1
@@ -274,6 +274,7 @@ version = "0.0.2"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"bgp-packet",
|
||||
"bgp-primitives",
|
||||
"bgp-rib",
|
||||
"bgp-server",
|
||||
"futures",
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ async fn main() -> Result<()> {
|
||||
),
|
||||
registry: server.registry.clone(),
|
||||
local_rib: server.local_rib.clone(),
|
||||
adj_rib_in: server.adj_rib_in.clone(),
|
||||
rib: Arc::clone(&server.rib),
|
||||
};
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = bgp_web::serve(state, http_endpoint).await {
|
||||
|
||||
+127
-255
@@ -1,25 +1,28 @@
|
||||
//! What it costs to take in the Internet routing table.
|
||||
//! What it costs to take in the Internet routing table, from the bytes on
|
||||
//! the wire to a chosen best route.
|
||||
//!
|
||||
//! Run with `cargo bench -p bgp-bench`. Set `BENCH_PEERS` (default 4) and
|
||||
//! `BENCH_SCALE_DOWN` (default 1; 10 makes the table ten times smaller) to
|
||||
//! change the size.
|
||||
//! Run with `cargo bench -p bgp-bench --bench full_table`. Set
|
||||
//! `BENCH_PEERS` (default 4) and `BENCH_SCALE_DOWN` (default 1; 10 makes the
|
||||
//! table ten times smaller) to change the size. The table is synthetic (see
|
||||
//! the crate documentation); `real_table` runs on a real one.
|
||||
//!
|
||||
//! This is a stopwatch and an allocation counter around whole operations at
|
||||
//! full size, not a statistical micro-benchmark: the questions are "how
|
||||
//! long until a full table is in" and "how much memory does it take", and
|
||||
//! their answers are seconds and gigabytes.
|
||||
//! This is a stopwatch around whole operations at full size, not a
|
||||
//! statistical micro-benchmark: the questions are "how long until a full
|
||||
//! table is in" and "how much memory does it take", and their answers are
|
||||
//! seconds and gigabytes.
|
||||
|
||||
use std::hint::black_box;
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::net::Ipv4Addr;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use bgp_bench::memory::Region;
|
||||
use bgp_bench::memory::{Region, describe};
|
||||
use bgp_bench::{Prefixes, Shape, announcements, encoded, wire_context};
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_packet::message::{Message, PathAttribute, UpdateMessage};
|
||||
use bgp_packet::message::{Message, PathAttribute};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::{AdjRibIn, PeerId, ReceivedUpdate, RibEntry, SnapshotEntry};
|
||||
use bgp_rib::table::{PeerIndex, PeerInfo, RibTable};
|
||||
use bytes::BytesMut;
|
||||
|
||||
fn env(name: &str, default: usize) -> usize {
|
||||
@@ -29,13 +32,7 @@ fn env(name: &str, default: usize) -> usize {
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
/// Bytes allocated and not freed since `region` began; zero when memory
|
||||
/// is not being counted (see `bgp_bench::memory`).
|
||||
fn retained(region: &Region) -> i64 {
|
||||
region.held().unwrap_or(0)
|
||||
}
|
||||
|
||||
fn mib(bytes: i64) -> f64 {
|
||||
fn mib(bytes: usize) -> f64 {
|
||||
bytes as f64 / (1024.0 * 1024.0)
|
||||
}
|
||||
|
||||
@@ -52,66 +49,39 @@ fn row(what: &str, elapsed: Duration, detail: String) {
|
||||
println!("{what:<44} {:>9.3} s {detail}", elapsed.as_secs_f64());
|
||||
}
|
||||
|
||||
fn peer_id(peer: u32) -> PeerId {
|
||||
PeerId {
|
||||
addr: SocketAddr::new(Ipv4Addr::new(192, 0, 2, peer as u8).into(), 179),
|
||||
asn: 64512 + peer,
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if Region::begin().held().is_none() {
|
||||
println!(
|
||||
"Memory is not counted in this run: sizes print as 0. For sizes (and slower\ntimings) add --features count-memory.\n"
|
||||
);
|
||||
}
|
||||
let peers = env("BENCH_PEERS", 4) as u32;
|
||||
let shape = Shape::INTERNET.scaled_down(env("BENCH_SCALE_DOWN", 1));
|
||||
|
||||
println!("== Sizes of the types a route is made of");
|
||||
for (name, size) in [
|
||||
("IpPrefix", size_of::<IpPrefix>()),
|
||||
("RibEntry", size_of::<RibEntry>()),
|
||||
(
|
||||
"(IpPrefix, RibEntry), a table slot",
|
||||
size_of::<(IpPrefix, RibEntry)>(),
|
||||
),
|
||||
("PathAttribute", size_of::<PathAttribute>()),
|
||||
("SnapshotEntry", size_of::<SnapshotEntry>()),
|
||||
] {
|
||||
println!("{name:<44} {size:>5} bytes");
|
||||
}
|
||||
let threads = std::thread::available_parallelism().map_or(1, |n| n.get());
|
||||
|
||||
println!(
|
||||
"\n== Table: {} IPv4 + {} IPv6 prefixes, from each of {peers} peers",
|
||||
"== Table: {} IPv4 + {} IPv6 prefixes, from each of {peers} peers",
|
||||
shape.ipv4_prefixes, shape.ipv6_prefixes
|
||||
);
|
||||
println!(
|
||||
"(IpPrefix is {} bytes, PathAttribute {})",
|
||||
size_of::<IpPrefix>(),
|
||||
size_of::<PathAttribute>()
|
||||
);
|
||||
let prefixes = Prefixes::generate(shape);
|
||||
let per_peer: Vec<Vec<UpdateMessage>> = (1..=peers)
|
||||
.map(|peer| announcements(&prefixes, shape, peer))
|
||||
let wire: Vec<Vec<BytesMut>> = (1..=peers)
|
||||
.map(|peer| encoded(&announcements(&prefixes, shape, peer)))
|
||||
.collect();
|
||||
let wire: Vec<Vec<BytesMut>> = per_peer.iter().map(|updates| encoded(updates)).collect();
|
||||
let updates: usize = per_peer.iter().map(Vec::len).sum();
|
||||
let updates: usize = wire.iter().map(Vec::len).sum();
|
||||
let routes = shape.prefixes() * peers as usize;
|
||||
let wire_bytes: usize = wire.iter().flatten().map(BytesMut::len).sum();
|
||||
println!(
|
||||
"{updates} UPDATE messages, {:.1} prefixes each, {:.1} MiB on the wire, {routes} routes",
|
||||
routes as f64 / updates as f64,
|
||||
mib(wire_bytes as i64),
|
||||
mib(wire_bytes),
|
||||
);
|
||||
|
||||
println!("\n== Reading the messages (one thread)");
|
||||
let ctx = wire_context();
|
||||
let started = Instant::now();
|
||||
let decoded: Vec<Vec<Message>> = wire
|
||||
.iter()
|
||||
.map(|messages| {
|
||||
messages
|
||||
.iter()
|
||||
.map(|m| Message::decode(&ctx, m).expect("what we wrote is well formed"))
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
for message in wire.iter().flatten() {
|
||||
black_box(Message::decode(&ctx, message).expect("what we wrote is well formed"));
|
||||
}
|
||||
let elapsed = started.elapsed();
|
||||
row(
|
||||
"decode UPDATEs",
|
||||
@@ -120,224 +90,126 @@ fn main() {
|
||||
"{} routes, {} messages, {:.0} MiB/s",
|
||||
per_second(routes, elapsed),
|
||||
per_second(updates, elapsed),
|
||||
mib(wire_bytes as i64) / elapsed.as_secs_f64()
|
||||
),
|
||||
);
|
||||
drop(decoded);
|
||||
|
||||
floor(&prefixes, &per_peer, routes);
|
||||
|
||||
println!("\n== The same, as the daemon does it today (one thread)");
|
||||
// From here to the filled table: what the routes cost to hold.
|
||||
let table_region = Region::begin();
|
||||
let region = Region::begin();
|
||||
let started = Instant::now();
|
||||
let received: Vec<Vec<ReceivedUpdate>> = per_peer
|
||||
.iter()
|
||||
.map(|updates| updates.iter().map(ReceivedUpdate::from_message).collect())
|
||||
.collect();
|
||||
let elapsed = started.elapsed();
|
||||
row(
|
||||
"UPDATE -> routes (ReceivedUpdate)",
|
||||
elapsed,
|
||||
format!(
|
||||
"{} routes, holding {:.0} MiB",
|
||||
per_second(routes, elapsed),
|
||||
mib(retained(®ion))
|
||||
mib(wire_bytes) / elapsed.as_secs_f64()
|
||||
),
|
||||
);
|
||||
|
||||
println!("\n== From bytes to best routes, without the network");
|
||||
for threads in [1, threads] {
|
||||
into_table(&wire, routes, threads);
|
||||
}
|
||||
|
||||
println!("\n== End to end: {peers} peers send the table over TCP at once");
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("a runtime");
|
||||
|
||||
println!("\n== Adj-RIB-In (the actor, fed from one task)");
|
||||
runtime.block_on(async {
|
||||
let rib = AdjRibIn::spawn();
|
||||
let started = Instant::now();
|
||||
for (peer, updates) in received.into_iter().enumerate() {
|
||||
let peer = peer_id(peer as u32 + 1);
|
||||
for update in updates {
|
||||
rib.apply(peer, update).await.expect("the actor runs");
|
||||
}
|
||||
let server = TestServer::start(65000).await.expect("a server");
|
||||
let mut senders = Vec::new();
|
||||
for (index, messages) in wire.iter().enumerate() {
|
||||
let peer = index as u32 + 1;
|
||||
let mut session = TestPeer::connect(server.addr).await.expect("a connection");
|
||||
session
|
||||
.handshake(64512 + peer, Ipv4Addr::new(10, 0, 0, peer as u8), 90)
|
||||
.await
|
||||
.expect("a session");
|
||||
let stream: Vec<u8> = messages.iter().flat_map(|m| m.iter().copied()).collect();
|
||||
senders.push((session, stream));
|
||||
}
|
||||
let stored = rib.len().await.expect("the actor runs");
|
||||
let elapsed = started.elapsed();
|
||||
assert_eq!(stored, routes);
|
||||
let held = retained(&table_region);
|
||||
row(
|
||||
"insert every route",
|
||||
elapsed,
|
||||
format!(
|
||||
"{} routes; table holds {:.0} MiB = {:.0} bytes a route",
|
||||
per_second(routes, elapsed),
|
||||
mib(held),
|
||||
held as f64 / routes as f64
|
||||
),
|
||||
);
|
||||
|
||||
let started = Instant::now();
|
||||
black_box(rib.len().await.expect("the actor runs"));
|
||||
row("count routes", started.elapsed(), String::new());
|
||||
|
||||
let before = Region::begin();
|
||||
let started = Instant::now();
|
||||
let snapshot = rib.snapshot().await.expect("the actor runs");
|
||||
let elapsed = started.elapsed();
|
||||
row(
|
||||
"snapshot (what /api/routes/received does)",
|
||||
elapsed,
|
||||
format!(
|
||||
"copies {:.0} MiB; nothing else is applied meanwhile",
|
||||
mib(retained(&before))
|
||||
),
|
||||
);
|
||||
drop(snapshot);
|
||||
|
||||
// A second announcement of everything by one peer: every route is
|
||||
// replaced, as after a route refresh.
|
||||
let again: Vec<ReceivedUpdate> = per_peer[0]
|
||||
.iter()
|
||||
.map(ReceivedUpdate::from_message)
|
||||
.collect();
|
||||
let started = Instant::now();
|
||||
for update in again {
|
||||
rib.apply(peer_id(1), update).await.expect("the actor runs");
|
||||
}
|
||||
rib.len().await.expect("the actor runs");
|
||||
let elapsed = started.elapsed();
|
||||
row(
|
||||
"replace one peer's routes",
|
||||
elapsed,
|
||||
per_second(shape.prefixes(), elapsed) + " routes",
|
||||
);
|
||||
|
||||
let started = Instant::now();
|
||||
let removed = rib.remove_peer(peer_id(1)).await.expect("the actor runs");
|
||||
let elapsed = started.elapsed();
|
||||
assert_eq!(removed, shape.prefixes());
|
||||
row(
|
||||
"remove one peer (session down)",
|
||||
elapsed,
|
||||
per_second(removed, elapsed) + " routes",
|
||||
);
|
||||
});
|
||||
|
||||
println!("\n== End to end: {peers} peers send the table over TCP at once");
|
||||
for logging in [false, true] {
|
||||
// The daemon logs a line for every UPDATE at INFO. With no subscriber
|
||||
// installed that costs almost nothing; this shows what it costs
|
||||
// when the lines are formatted, as they are in production (here to
|
||||
// nowhere, so without the cost of the terminal or the disk).
|
||||
if logging {
|
||||
tracing::subscriber::set_global_default(
|
||||
tracing_subscriber::fmt()
|
||||
.with_writer(std::io::sink)
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
.finish(),
|
||||
)
|
||||
.expect("no subscriber was installed before");
|
||||
}
|
||||
runtime.block_on(async {
|
||||
let server = TestServer::start(65000).await.expect("a server");
|
||||
let mut senders = Vec::new();
|
||||
for (index, messages) in wire.iter().enumerate() {
|
||||
let peer = index as u32 + 1;
|
||||
let mut session = TestPeer::connect(server.addr).await.expect("a connection");
|
||||
session
|
||||
.handshake(64512 + peer, Ipv4Addr::new(10, 0, 0, peer as u8), 90)
|
||||
.await
|
||||
.expect("a session");
|
||||
let stream: Vec<u8> = messages.iter().flat_map(|m| m.iter().copied()).collect();
|
||||
senders.push((session, stream));
|
||||
}
|
||||
let started = Instant::now();
|
||||
let sending: Vec<_> = senders
|
||||
.into_iter()
|
||||
.map(|(mut session, stream)| {
|
||||
tokio::spawn(async move {
|
||||
session.send_raw(&stream).await.expect("the server reads");
|
||||
// Held open until the table is in.
|
||||
session
|
||||
})
|
||||
let sending: Vec<_> = senders
|
||||
.into_iter()
|
||||
.map(|(mut session, stream)| {
|
||||
tokio::spawn(async move {
|
||||
session.send_raw(&stream).await.expect("the server reads");
|
||||
// Held open until the table is in.
|
||||
session
|
||||
})
|
||||
.collect();
|
||||
while server.adj_rib_in.len().await.expect("the actor runs") < routes {
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
}
|
||||
let elapsed = started.elapsed();
|
||||
row(
|
||||
if logging {
|
||||
"with a log line per UPDATE"
|
||||
} else {
|
||||
"with logging off"
|
||||
},
|
||||
elapsed,
|
||||
per_second(routes, elapsed) + " routes",
|
||||
);
|
||||
for session in sending {
|
||||
drop(session.await);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
while server.rib.len() < routes {
|
||||
tokio::time::sleep(Duration::from_millis(2)).await;
|
||||
}
|
||||
let elapsed = started.elapsed();
|
||||
row(
|
||||
"sessions, state machine, table",
|
||||
elapsed,
|
||||
format!(
|
||||
"{} routes; {} prefixes with a best route",
|
||||
per_second(routes, elapsed),
|
||||
server.rib.prefix_count()
|
||||
),
|
||||
);
|
||||
for session in sending {
|
||||
drop(session.await);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// A straw man, not a design: the same routes in the plainest compact form,
|
||||
/// to show how far the current table is from what a hash table can do.
|
||||
///
|
||||
/// A prefix is one integer and a route is the number of its attribute set
|
||||
/// (here simply the number of the UPDATE that announced it; a real table
|
||||
/// would intern the sets). One map per peer and address family, filled on
|
||||
/// one thread with no channel in between.
|
||||
fn floor(prefixes: &Prefixes, per_peer: &[Vec<UpdateMessage>], routes: usize) {
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn key(prefix: &IpPrefix) -> u128 {
|
||||
let mut address = [0u8; 16];
|
||||
address[..prefix.bytes().len()].copy_from_slice(prefix.bytes());
|
||||
// The address in the high bits, the length in the low eight. An IPv6
|
||||
// prefix longer than /120 would not fit; the table has none.
|
||||
(u128::from_be_bytes(address) & !0xff) | u128::from(prefix.length())
|
||||
}
|
||||
|
||||
println!("\n== Straw man: packed prefix -> attribute set number, no actor");
|
||||
/// What a session does with each message, without the session: decode it,
|
||||
/// find its attribute set, put its routes in the table, choose the best.
|
||||
/// `threads` threads each take whole peers.
|
||||
fn into_table(wire: &[Vec<BytesMut>], routes: usize, threads: usize) {
|
||||
let region = Region::begin();
|
||||
let table = RibTable::new(threads * 16);
|
||||
let peers: Vec<PeerIndex> = (0..wire.len())
|
||||
.map(|peer| {
|
||||
table
|
||||
.add_peer(PeerInfo {
|
||||
asn: 64512 + peer as u32 + 1,
|
||||
router_id: Ipv4Addr::from(peer as u32 + 1),
|
||||
address: Ipv4Addr::new(192, 0, 2, peer as u8 + 1).into(),
|
||||
ibgp: false,
|
||||
})
|
||||
.expect("room for the peer")
|
||||
})
|
||||
.collect();
|
||||
let ctx = wire_context();
|
||||
let next = AtomicUsize::new(0);
|
||||
let started = Instant::now();
|
||||
let mut tables: Vec<(HashMap<u64, u32>, HashMap<u128, u32>)> = Vec::new();
|
||||
for updates in per_peer {
|
||||
let mut v4: HashMap<u64, u32> = HashMap::with_capacity(prefixes.ipv4.len());
|
||||
let mut v6: HashMap<u128, u32> = HashMap::with_capacity(prefixes.ipv6.len());
|
||||
for (number, update) in updates.iter().enumerate() {
|
||||
for prefix in &update.announced_routes {
|
||||
v4.insert(
|
||||
(key(prefix) >> 64) as u64 | u64::from(prefix.length()),
|
||||
number as u32,
|
||||
);
|
||||
}
|
||||
for attribute in &update.path_attributes {
|
||||
if let PathAttribute::MpReachNlri(mp) = attribute {
|
||||
for prefix in &mp.prefixes {
|
||||
v6.insert(key(prefix), number as u32);
|
||||
std::thread::scope(|scope| {
|
||||
for _ in 0..threads {
|
||||
scope.spawn(|| {
|
||||
loop {
|
||||
let at = next.fetch_add(1, Ordering::Relaxed);
|
||||
let Some(messages) = wire.get(at) else {
|
||||
break;
|
||||
};
|
||||
for message in messages {
|
||||
let decoded =
|
||||
Message::decode_with_report(&ctx, message).expect("well formed");
|
||||
let Message::Update(update) = decoded.message else {
|
||||
continue;
|
||||
};
|
||||
let wire = decoded.attributes_wire.as_deref().expect("kept");
|
||||
if !update.announced_routes.is_empty() {
|
||||
let attrs = table.attrs().intern_received(wire, None).expect("valid");
|
||||
table.announce(peers[at], &attrs, &update.announced_routes);
|
||||
}
|
||||
for attribute in &update.path_attributes {
|
||||
if let PathAttribute::MpReachNlri(mp) = attribute {
|
||||
let attrs = table
|
||||
.attrs()
|
||||
.intern_received(wire, Some(&mp.next_hop))
|
||||
.expect("valid");
|
||||
table.announce(peers[at], &attrs, &mp.prefixes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
tables.push((v4, v6));
|
||||
}
|
||||
});
|
||||
let elapsed = started.elapsed();
|
||||
let stored: usize = tables.iter().map(|(v4, v6)| v4.len() + v6.len()).sum();
|
||||
assert_eq!(stored, routes);
|
||||
let held = retained(®ion);
|
||||
assert_eq!(table.len(), routes);
|
||||
row(
|
||||
"insert every route",
|
||||
&format!("decode, intern, insert, choose: {threads} thread(s)"),
|
||||
elapsed,
|
||||
format!(
|
||||
"{} routes; {:.0} MiB = {:.0} bytes a route (attribute sets not counted)",
|
||||
"{} routes; {}",
|
||||
per_second(routes, elapsed),
|
||||
mib(held),
|
||||
held as f64 / routes as f64
|
||||
describe(region.held(), routes, "route")
|
||||
),
|
||||
);
|
||||
black_box(tables);
|
||||
}
|
||||
@@ -7,18 +7,17 @@
|
||||
//! the peers with the most routes (default: all of them).
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::net::Ipv4Addr;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use bgp_bench::memory::Region;
|
||||
use bgp_bench::mrt::{Dump, parse_attributes};
|
||||
use bgp_packet::message::{NlriNextHop, PathAttribute};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::table::{Event, PeerIndex, PeerInfo, RibTable};
|
||||
use bgp_rib::{AdjRibIn, PeerId, ReceivedUpdate, RibEntry};
|
||||
|
||||
/// Bytes allocated and not freed since `region` began; zero when memory
|
||||
/// is not being counted (see `bgp_bench::memory`).
|
||||
@@ -105,13 +104,6 @@ fn main() {
|
||||
println!("{unparsed} attribute sets could not be parsed; their routes are left out");
|
||||
}
|
||||
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("a runtime");
|
||||
current_table(&runtime, &dump, &peers, &parsed);
|
||||
drop(runtime);
|
||||
|
||||
let threads = std::thread::available_parallelism().map_or(1, |n| n.get());
|
||||
sharded_table(&dump, &peers, &parsed, 1);
|
||||
sharded_table(&dump, &peers, &parsed, threads);
|
||||
@@ -169,120 +161,6 @@ fn describe(dump: &Dump, peers: &[usize]) {
|
||||
);
|
||||
}
|
||||
|
||||
fn peer_id(dump: &Dump, peer: usize) -> PeerId {
|
||||
PeerId {
|
||||
// The port tells apart peers that share an address in the dump.
|
||||
addr: SocketAddr::new(dump.peers[peer].address, peer as u16),
|
||||
asn: dump.peers[peer].asn,
|
||||
}
|
||||
}
|
||||
|
||||
/// A peer's routes as the UPDATEs that would have announced them: routes
|
||||
/// that share their attributes arrive together.
|
||||
fn as_updates(
|
||||
dump: &Dump,
|
||||
peer: usize,
|
||||
parsed: &[Option<Attributes>],
|
||||
received_at: SystemTime,
|
||||
) -> Vec<ReceivedUpdate> {
|
||||
let mut routes = dump.routes[peer].clone();
|
||||
routes.sort_by_key(|(_, set)| *set);
|
||||
routes
|
||||
.chunk_by(|a, b| a.1 == b.1)
|
||||
.filter_map(|group| {
|
||||
let (attributes, next_hop) = parsed[group[0].1 as usize].as_ref()?;
|
||||
Some(ReceivedUpdate {
|
||||
withdrawn: vec![],
|
||||
announced: group
|
||||
.iter()
|
||||
.map(|(prefix, _)| {
|
||||
(
|
||||
*prefix,
|
||||
RibEntry {
|
||||
path_attributes: Arc::clone(attributes),
|
||||
next_hop: next_hop.clone(),
|
||||
received_at,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn current_table(
|
||||
runtime: &tokio::runtime::Runtime,
|
||||
dump: &Dump,
|
||||
peers: &[usize],
|
||||
parsed: &[Option<Attributes>],
|
||||
) {
|
||||
println!("\n== Adj-RIB-In as it is today (the actor, fed from one task)");
|
||||
// From before the routes exist to the filled table: what it holds,
|
||||
// apart from the attribute sets, which were parsed above.
|
||||
let region = Region::begin();
|
||||
let now = SystemTime::now();
|
||||
let updates: Vec<(PeerId, Vec<ReceivedUpdate>)> = peers
|
||||
.iter()
|
||||
.map(|&peer| (peer_id(dump, peer), as_updates(dump, peer, parsed, now)))
|
||||
.collect();
|
||||
let routes: usize = updates
|
||||
.iter()
|
||||
.flat_map(|(_, updates)| updates)
|
||||
.map(|update| update.announced.len())
|
||||
.sum();
|
||||
let messages: usize = updates.iter().map(|(_, updates)| updates.len()).sum();
|
||||
println!(
|
||||
"{routes} routes in {messages} UPDATEs, {:.1} routes each",
|
||||
routes as f64 / messages as f64
|
||||
);
|
||||
let first = updates[0].0;
|
||||
let first_routes: usize = updates[0].1.iter().map(|u| u.announced.len()).sum();
|
||||
|
||||
runtime.block_on(async {
|
||||
let rib = AdjRibIn::spawn();
|
||||
let started = Instant::now();
|
||||
for (peer, updates) in updates {
|
||||
for update in updates {
|
||||
rib.apply(peer, update).await.expect("the actor runs");
|
||||
}
|
||||
}
|
||||
let stored = rib.len().await.expect("the actor runs");
|
||||
let elapsed = started.elapsed();
|
||||
let held = retained(®ion);
|
||||
row(
|
||||
"insert every route",
|
||||
elapsed,
|
||||
format!(
|
||||
"{} routes; {stored} stored; {:.0} MiB = {:.0} bytes a route, attribute sets not counted",
|
||||
per_second(routes, elapsed),
|
||||
mib(held),
|
||||
held as f64 / stored as f64
|
||||
),
|
||||
);
|
||||
|
||||
let before = Region::begin();
|
||||
let started = Instant::now();
|
||||
let snapshot = rib.snapshot().await.expect("the actor runs");
|
||||
row(
|
||||
"snapshot",
|
||||
started.elapsed(),
|
||||
format!("copies {:.0} MiB", mib(retained(&before))),
|
||||
);
|
||||
drop(snapshot);
|
||||
|
||||
let started = Instant::now();
|
||||
let removed = rib.remove_peer(first).await.expect("the actor runs");
|
||||
let elapsed = started.elapsed();
|
||||
assert_eq!(removed, first_routes);
|
||||
row(
|
||||
"remove one peer (session down)",
|
||||
elapsed,
|
||||
per_second(removed, elapsed) + " routes",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// The sharded table, filled by `threads` threads. Each takes whole peers,
|
||||
/// as sessions would, and for every UPDATE interns the attributes and then
|
||||
/// announces the prefixes: everything between a decoded message and a
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
|
||||
use bgp_rib::{AdjRibIn, LocalRib};
|
||||
use bgp_packet::message::{NlriNextHop, PathAttribute};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::LocalRib;
|
||||
use bgp_rib::table::{Event, PeerIndex, Reader, RibTable};
|
||||
use bgp_server::config::Config;
|
||||
use bgp_server::error::ServerError;
|
||||
use bgp_server::server::BgpServerBuilder;
|
||||
@@ -16,8 +20,8 @@ pub struct TestServer {
|
||||
handle: JoinHandle<Result<(), ServerError>>,
|
||||
/// The single IPv4 loopback address the server is bound to.
|
||||
pub addr: SocketAddr,
|
||||
/// Handle to the server's Adj-RIB-In for test introspection.
|
||||
pub adj_rib_in: AdjRibIn,
|
||||
/// The server's routing table.
|
||||
pub rib: Arc<RibTable>,
|
||||
/// Handle to the server's locally originated routes.
|
||||
pub local_rib: LocalRib,
|
||||
/// Live view of the server's established sessions.
|
||||
@@ -58,7 +62,7 @@ impl TestServer {
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("at least one listener");
|
||||
let adj_rib_in = bound.adj_rib_in.clone();
|
||||
let rib = Arc::clone(&bound.rib);
|
||||
let local_rib = bound.local_rib.clone();
|
||||
let registry = bound.registry.clone();
|
||||
|
||||
@@ -66,7 +70,7 @@ impl TestServer {
|
||||
Ok(Self {
|
||||
handle,
|
||||
addr,
|
||||
adj_rib_in,
|
||||
rib,
|
||||
local_rib,
|
||||
registry,
|
||||
local_asn,
|
||||
@@ -74,7 +78,91 @@ impl TestServer {
|
||||
}
|
||||
}
|
||||
|
||||
/// A route in the server's table, as a test wants to look at it.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReceivedRoute {
|
||||
pub prefix: IpPrefix,
|
||||
pub peer: PeerIndex,
|
||||
/// The route's attributes, without MP_REACH_NLRI.
|
||||
pub attributes: Vec<PathAttribute>,
|
||||
/// The next hop, if the route came in MP_REACH_NLRI.
|
||||
pub next_hop: Option<NlriNextHop>,
|
||||
/// Whether it is the best route for its prefix.
|
||||
pub best: bool,
|
||||
}
|
||||
|
||||
/// A change of a prefix's best route.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RouteChange {
|
||||
/// The prefix has a best route, or another one than before.
|
||||
Announced { prefix: IpPrefix, peer: PeerIndex },
|
||||
/// The prefix has no route any more.
|
||||
Withdrawn { prefix: IpPrefix },
|
||||
}
|
||||
|
||||
/// The changes of best route in a server's table from the moment this was
|
||||
/// made, read from the table's journal.
|
||||
pub struct RouteChanges {
|
||||
reader: Reader,
|
||||
pending: std::collections::VecDeque<RouteChange>,
|
||||
}
|
||||
|
||||
impl RouteChanges {
|
||||
/// The next change, waiting for one if need be. Changes to one prefix
|
||||
/// that happen before they are read arrive as one.
|
||||
pub async fn recv(&mut self) -> Result<RouteChange> {
|
||||
loop {
|
||||
if let Some(change) = self.pending.pop_front() {
|
||||
return Ok(change);
|
||||
}
|
||||
self.reader.changed().await;
|
||||
self.read();
|
||||
}
|
||||
}
|
||||
|
||||
fn read(&mut self) {
|
||||
let pending = &mut self.pending;
|
||||
let mut read = |event: Event<'_>| match event {
|
||||
Event::Best { prefix, peer, .. } => {
|
||||
pending.push_back(RouteChange::Announced { prefix, peer })
|
||||
}
|
||||
Event::Withdrawn { prefix } => pending.push_back(RouteChange::Withdrawn { prefix }),
|
||||
Event::Reset | Event::Synced => {}
|
||||
};
|
||||
while !self.reader.poll(1024, &mut read).caught_up {}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestServer {
|
||||
/// Every route in the server's table.
|
||||
pub fn received_routes(&self) -> Vec<ReceivedRoute> {
|
||||
let mut routes = Vec::new();
|
||||
self.rib.for_each_route(|prefix, peer, attrs, best| {
|
||||
let (attributes, next_hop) = attrs.attributes();
|
||||
routes.push(ReceivedRoute {
|
||||
prefix: *prefix,
|
||||
peer,
|
||||
attributes,
|
||||
next_hop,
|
||||
best,
|
||||
});
|
||||
});
|
||||
routes
|
||||
}
|
||||
|
||||
/// Follows the changes of best route from now on.
|
||||
pub fn route_changes(&self) -> RouteChanges {
|
||||
let mut changes = RouteChanges {
|
||||
reader: self.rib.reader(),
|
||||
pending: Default::default(),
|
||||
};
|
||||
// A new reader is first given the table as it is. That is not a
|
||||
// change.
|
||||
changes.read();
|
||||
changes.pending.clear();
|
||||
changes
|
||||
}
|
||||
|
||||
/// Waits for the server to stop by itself and returns why. A healthy
|
||||
/// server never does.
|
||||
pub async fn stopped(&mut self) -> Result<(), ServerError> {
|
||||
@@ -96,7 +184,7 @@ impl TestServer {
|
||||
),
|
||||
registry: self.registry.clone(),
|
||||
local_rib: self.local_rib.clone(),
|
||||
adj_rib_in: self.adj_rib_in.clone(),
|
||||
rib: Arc::clone(&self.rib),
|
||||
};
|
||||
let handle = tokio::spawn(bgp_web::serve_on(state, listener));
|
||||
Ok((base_url, HttpGuard { handle }))
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_integration_tests::test_server::{RouteChange, TestServer};
|
||||
use bgp_packet::message::{
|
||||
AsPathAttribute, CommunitiesPathAttribute, LargeCommunitiesPathAttribute, LargeCommunity,
|
||||
NextHopPathAttribute, OriginPathAttribute, PathAttribute, UpdateMessage,
|
||||
@@ -285,7 +285,7 @@ async fn test_keepalives_continue_alongside_updates() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn test_received_routes_removed_when_session_closes() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let mut rib_events = server.adj_rib_in.subscribe();
|
||||
let mut rib_events = server.route_changes();
|
||||
let mut session_events = server.registry.subscribe();
|
||||
let mut peer = established_peer(&server).await?;
|
||||
wait_for_sessions(&server, 1).await?;
|
||||
@@ -304,22 +304,22 @@ async fn test_received_routes_removed_when_session_closes() -> Result<()> {
|
||||
.await?;
|
||||
for _ in 0..2 {
|
||||
let ev = tokio::time::timeout(TIMEOUT, rib_events.recv()).await??;
|
||||
assert!(matches!(ev, bgp_rib::RibEvent::Announced { .. }), "{ev:?}");
|
||||
assert!(matches!(ev, RouteChange::Announced { .. }), "{ev:?}");
|
||||
}
|
||||
assert_eq!(server.adj_rib_in.snapshot().await.unwrap().len(), 2);
|
||||
assert_eq!(server.received_routes().len(), 2);
|
||||
|
||||
drop(peer);
|
||||
|
||||
let mut withdrawn = vec![];
|
||||
for _ in 0..2 {
|
||||
match tokio::time::timeout(TIMEOUT, rib_events.recv()).await?? {
|
||||
bgp_rib::RibEvent::Withdrawn { prefix, .. } => withdrawn.push(prefix.to_string()),
|
||||
RouteChange::Withdrawn { prefix, .. } => withdrawn.push(prefix.to_string()),
|
||||
other => panic!("expected Withdrawn, got {other:?}"),
|
||||
}
|
||||
}
|
||||
withdrawn.sort();
|
||||
assert_eq!(withdrawn, vec!["198.51.100.0/24", "203.0.113.0/24"]);
|
||||
assert!(server.adj_rib_in.snapshot().await.unwrap().is_empty());
|
||||
assert!(server.received_routes().is_empty());
|
||||
|
||||
// Withdrawals precede the Down event.
|
||||
let _up = tokio::time::timeout(TIMEOUT, session_events.recv()).await??;
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::RouteChange;
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_packet::errors::{BgpError, CeaseSubcode, FsmSubcode, MsgHeaderSubcode, UpdateMsgSubcode};
|
||||
use bgp_packet::message::{
|
||||
@@ -11,7 +12,6 @@ use bgp_packet::message::{
|
||||
};
|
||||
use bgp_primitives::constants::{AddressFamilyId, Afi, Safi};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::RibEvent;
|
||||
use eyre::Result;
|
||||
|
||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
||||
@@ -229,14 +229,7 @@ async fn test_message_split_across_a_keepalive_tick_is_still_one_message() -> Re
|
||||
peer.send_raw(&update[10..]).await?;
|
||||
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while !server
|
||||
.adj_rib_in
|
||||
.snapshot()
|
||||
.await
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|e| e.prefix == prefix)
|
||||
{
|
||||
while !server.received_routes().iter().any(|e| e.prefix == prefix) {
|
||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||
}
|
||||
})
|
||||
@@ -393,7 +386,7 @@ async fn test_server_accepts_ipv6_update() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn test_rib_records_ipv4_announce() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let mut events = server.adj_rib_in.subscribe();
|
||||
let mut events = server.route_changes();
|
||||
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
|
||||
@@ -409,11 +402,11 @@ async fn test_rib_records_ipv4_announce() -> Result<()> {
|
||||
|
||||
let event = tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
|
||||
assert!(
|
||||
matches!(&event, RibEvent::Announced { prefix: p, .. } if p == &prefix),
|
||||
matches!(&event, RouteChange::Announced { prefix: p, .. } if p == &prefix),
|
||||
"Expected Announced for 10.0.0.0/8, got {event:?}",
|
||||
);
|
||||
|
||||
let snapshot = server.adj_rib_in.snapshot().await.unwrap();
|
||||
let snapshot = server.received_routes();
|
||||
assert!(
|
||||
snapshot.iter().any(|e| e.prefix == prefix),
|
||||
"10.0.0.0/8 missing from RIB snapshot",
|
||||
@@ -425,7 +418,7 @@ async fn test_rib_records_ipv4_announce() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn test_rib_records_ipv4_withdraw() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let mut events = server.adj_rib_in.subscribe();
|
||||
let mut events = server.route_changes();
|
||||
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
|
||||
@@ -452,11 +445,11 @@ async fn test_rib_records_ipv4_withdraw() -> Result<()> {
|
||||
|
||||
let event = tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
|
||||
assert!(
|
||||
matches!(&event, RibEvent::Withdrawn { prefix: p, .. } if p == &prefix),
|
||||
matches!(&event, RouteChange::Withdrawn { prefix: p, .. } if p == &prefix),
|
||||
"Expected Withdrawn for 10.0.0.0/8, got {event:?}",
|
||||
);
|
||||
|
||||
let snapshot = server.adj_rib_in.snapshot().await.unwrap();
|
||||
let snapshot = server.received_routes();
|
||||
assert!(
|
||||
!snapshot.iter().any(|e| e.prefix == prefix),
|
||||
"10.0.0.0/8 should have been removed from RIB snapshot",
|
||||
@@ -468,7 +461,7 @@ async fn test_rib_records_ipv4_withdraw() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn test_rib_records_ipv6_announce() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let mut events = server.adj_rib_in.subscribe();
|
||||
let mut events = server.route_changes();
|
||||
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
|
||||
@@ -491,11 +484,11 @@ async fn test_rib_records_ipv6_announce() -> Result<()> {
|
||||
|
||||
let event = tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
|
||||
assert!(
|
||||
matches!(&event, RibEvent::Announced { prefix: p, .. } if p == &prefix),
|
||||
matches!(&event, RouteChange::Announced { prefix: p, .. } if p == &prefix),
|
||||
"Expected Announced for 2001:db8::/32, got {event:?}",
|
||||
);
|
||||
|
||||
let snapshot = server.adj_rib_in.snapshot().await.unwrap();
|
||||
let snapshot = server.received_routes();
|
||||
assert!(
|
||||
snapshot.iter().any(|e| e.prefix == prefix),
|
||||
"2001:db8::/32 missing from RIB snapshot",
|
||||
@@ -507,7 +500,7 @@ async fn test_rib_records_ipv6_announce() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn test_rib_records_ipv6_withdraw() -> Result<()> {
|
||||
let server = TestServer::start(65000).await?;
|
||||
let mut events = server.adj_rib_in.subscribe();
|
||||
let mut events = server.route_changes();
|
||||
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
|
||||
@@ -545,11 +538,11 @@ async fn test_rib_records_ipv6_withdraw() -> Result<()> {
|
||||
|
||||
let event = tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
|
||||
assert!(
|
||||
matches!(&event, RibEvent::Withdrawn { prefix: p, .. } if p == &prefix),
|
||||
matches!(&event, RouteChange::Withdrawn { prefix: p, .. } if p == &prefix),
|
||||
"Expected Withdrawn for 2001:db8::/32, got {event:?}",
|
||||
);
|
||||
|
||||
let snapshot = server.adj_rib_in.snapshot().await.unwrap();
|
||||
let snapshot = server.received_routes();
|
||||
assert!(
|
||||
!snapshot.iter().any(|e| e.prefix == prefix),
|
||||
"2001:db8::/32 should have been removed from RIB snapshot",
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
//! End-to-end tests of route selection: routes for one prefix from several
|
||||
//! peers, which of them the server chooses, and how that is shown.
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::{RouteChange, TestServer};
|
||||
use bgp_packet::message::{
|
||||
AsPathAttribute, Message, NextHopPathAttribute, OriginPathAttribute, PathAttribute,
|
||||
UpdateMessage,
|
||||
};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use eyre::Result;
|
||||
use serde_json::Value;
|
||||
|
||||
const LOCAL_ASN: u32 = 65000;
|
||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
fn prefix(s: &str) -> IpPrefix {
|
||||
IpPrefix::try_from(s).unwrap()
|
||||
}
|
||||
|
||||
/// A session from the peer numbered `number`, in AS 65000 + `number`.
|
||||
async fn peer(server: &TestServer, number: u8) -> Result<TestPeer> {
|
||||
let mut peer = TestPeer::connect(server.addr).await?;
|
||||
peer.handshake(
|
||||
LOCAL_ASN + u32::from(number),
|
||||
Ipv4Addr::new(10, 0, 0, number),
|
||||
90,
|
||||
)
|
||||
.await?;
|
||||
Ok(peer)
|
||||
}
|
||||
|
||||
fn announcement(prefixes: &[IpPrefix], as_path: &[u32]) -> Message {
|
||||
Message::Update(UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: vec![
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
AsPathAttribute::from_asns(as_path.to_vec()),
|
||||
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
||||
],
|
||||
announced_routes: prefixes.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
fn withdrawal(prefixes: &[IpPrefix]) -> Message {
|
||||
Message::Update(UpdateMessage {
|
||||
withdrawn_routes: prefixes.to_vec(),
|
||||
path_attributes: vec![],
|
||||
announced_routes: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
/// Waits until the table holds `routes` routes.
|
||||
async fn wait_for_routes(server: &TestServer, routes: usize) -> Result<()> {
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while server.rib.len() != routes {
|
||||
tokio::time::sleep(Duration::from_millis(5)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The AS path of the route the server holds as best for `prefix`.
|
||||
fn best_path(server: &TestServer, prefix: &IpPrefix) -> Option<Vec<u32>> {
|
||||
let best = server
|
||||
.received_routes()
|
||||
.into_iter()
|
||||
.find(|route| route.prefix == *prefix && route.best)?;
|
||||
best.attributes
|
||||
.iter()
|
||||
.find_map(|attribute| match attribute {
|
||||
PathAttribute::AsPath(path) => Some(path.segments[0].path.clone()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_json(client: &reqwest::Client, url: &str) -> Result<Value> {
|
||||
Ok(client
|
||||
.get(url)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Two peers offer the same prefix. The route with the shorter AS path is
|
||||
/// chosen; when it goes, the other is.
|
||||
#[tokio::test]
|
||||
async fn test_the_best_route_is_chosen_and_replaced_when_it_goes() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let client = reqwest::Client::new();
|
||||
let mut changes = server.route_changes();
|
||||
let p = prefix("203.0.113.0/24");
|
||||
|
||||
let mut far = peer(&server, 1).await?;
|
||||
far.send(&announcement(&[p], &[65001, 64999, 64998]))
|
||||
.await?;
|
||||
wait_for_routes(&server, 1).await?;
|
||||
assert_eq!(best_path(&server, &p), Some(vec![65001, 64999, 64998]));
|
||||
|
||||
let mut near = peer(&server, 2).await?;
|
||||
near.send(&announcement(&[p], &[65002, 64998])).await?;
|
||||
wait_for_routes(&server, 2).await?;
|
||||
assert_eq!(best_path(&server, &p), Some(vec![65002, 64998]));
|
||||
|
||||
// Both routes are kept and shown; one is marked.
|
||||
let status = get_json(&client, &format!("{base}/api/status")).await?;
|
||||
assert_eq!(status["received_routes"], 2);
|
||||
assert_eq!(status["received_prefixes"], 1);
|
||||
let routes = get_json(&client, &format!("{base}/api/routes/received")).await?;
|
||||
let best: Vec<&Value> = routes
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|route| route["best"] == true)
|
||||
.collect();
|
||||
assert_eq!(routes.as_array().unwrap().len(), 2);
|
||||
assert_eq!(best.len(), 1);
|
||||
assert_eq!(best[0]["peer_asn"], 65002);
|
||||
|
||||
// The session with the best route ends: the other route takes over.
|
||||
drop(near);
|
||||
wait_for_routes(&server, 1).await?;
|
||||
assert_eq!(best_path(&server, &p), Some(vec![65001, 64999, 64998]));
|
||||
|
||||
far.send(&withdrawal(&[p])).await?;
|
||||
wait_for_routes(&server, 0).await?;
|
||||
assert_eq!(best_path(&server, &p), None);
|
||||
|
||||
// A follower of the best routes is told of each change: the first
|
||||
// route, the better one, back to the first, none.
|
||||
let mut seen = Vec::new();
|
||||
while seen.last() != Some(&RouteChange::Withdrawn { prefix: p }) {
|
||||
seen.push(tokio::time::timeout(TIMEOUT, changes.recv()).await??);
|
||||
}
|
||||
assert!(
|
||||
seen.iter()
|
||||
.all(|change| matches!(change, RouteChange::Announced { prefix, .. } | RouteChange::Withdrawn { prefix } if *prefix == p)),
|
||||
"{seen:?}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The event stream reports changes of best route, and only those.
|
||||
#[tokio::test]
|
||||
async fn test_changes_of_best_route_are_on_the_event_stream() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let p = prefix("203.0.113.0/24");
|
||||
|
||||
// A route that is there before the client connects is not an event.
|
||||
let mut first = peer(&server, 1).await?;
|
||||
first
|
||||
.send(&announcement(&[prefix("198.51.100.0/24")], &[65001]))
|
||||
.await?;
|
||||
wait_for_routes(&server, 1).await?;
|
||||
|
||||
let mut stream = reqwest::Client::new()
|
||||
.get(format!("{base}/api/events"))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
|
||||
first.send(&announcement(&[p], &[65001, 64998])).await?;
|
||||
wait_for_routes(&server, 2).await?;
|
||||
first.send(&withdrawal(&[p])).await?;
|
||||
wait_for_routes(&server, 1).await?;
|
||||
|
||||
let mut text = String::new();
|
||||
let mut events: Vec<Value> = Vec::new();
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while !events.iter().any(|event| event["type"] == "best_withdrawn") {
|
||||
let chunk = stream
|
||||
.chunk()
|
||||
.await?
|
||||
.ok_or_else(|| eyre::eyre!("stream ended"))?;
|
||||
text.push_str(std::str::from_utf8(&chunk)?);
|
||||
events = text
|
||||
.lines()
|
||||
.filter_map(|line| line.strip_prefix("data:"))
|
||||
.filter_map(|data| serde_json::from_str(data.trim()).ok())
|
||||
.collect();
|
||||
}
|
||||
Ok::<_, eyre::Error>(())
|
||||
})
|
||||
.await??;
|
||||
|
||||
let about_routes: Vec<&Value> = events
|
||||
.iter()
|
||||
.filter(|event| {
|
||||
event["type"]
|
||||
.as_str()
|
||||
.is_some_and(|t| t.starts_with("best_"))
|
||||
})
|
||||
.collect();
|
||||
// The announcement and the withdrawal may have been read as one change
|
||||
// or as two; either way nothing is said of the route that was there
|
||||
// from the start, and the last word is that the prefix is gone.
|
||||
assert!(about_routes.len() <= 2, "{about_routes:?}");
|
||||
for event in &about_routes {
|
||||
let named = event["prefix"]
|
||||
.as_str()
|
||||
.or(event["route"]["prefix"].as_str());
|
||||
assert_eq!(named, Some("203.0.113.0/24"), "{event}");
|
||||
}
|
||||
assert_eq!(about_routes.last().unwrap()["type"], "best_withdrawn");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The table can be far too large to send whole: the API gives a part of
|
||||
/// it, and says how much there is.
|
||||
#[tokio::test]
|
||||
async fn test_received_routes_are_returned_up_to_a_limit() -> Result<()> {
|
||||
let server = TestServer::start(LOCAL_ASN).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let prefixes: Vec<IpPrefix> = (0..40).map(|n| prefix(&format!("10.{n}.0.0/16"))).collect();
|
||||
let mut peer = peer(&server, 1).await?;
|
||||
peer.send(&announcement(&prefixes, &[65001])).await?;
|
||||
wait_for_routes(&server, 40).await?;
|
||||
|
||||
let count = |routes: Value| routes.as_array().unwrap().len();
|
||||
let url = format!("{base}/api/routes/received");
|
||||
assert_eq!(count(get_json(&client, &url).await?), 40);
|
||||
assert_eq!(
|
||||
count(get_json(&client, &format!("{url}?limit=7")).await?),
|
||||
7
|
||||
);
|
||||
let status = get_json(&client, &format!("{base}/api/status")).await?;
|
||||
assert_eq!(status["received_routes"], 40);
|
||||
Ok(())
|
||||
}
|
||||
@@ -240,7 +240,7 @@ async fn test_restarted_peer_replaces_stale_session_when_enabled() -> Result<()>
|
||||
}))
|
||||
.await?;
|
||||
tokio::time::timeout(TIMEOUT, async {
|
||||
while server.adj_rib_in.snapshot().await.unwrap().is_empty() {
|
||||
while server.received_routes().is_empty() {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
@@ -265,7 +265,7 @@ async fn test_restarted_peer_replaces_stale_session_when_enabled() -> Result<()>
|
||||
})
|
||||
.await?;
|
||||
assert!(
|
||||
server.adj_rib_in.snapshot().await.unwrap().is_empty(),
|
||||
server.received_routes().is_empty(),
|
||||
"stale routes were not flushed"
|
||||
);
|
||||
Ok(())
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! What the server does when a part of it that it cannot do without fails.
|
||||
//!
|
||||
//! A RIB's actor stops only by panicking. Before, nothing noticed: sessions
|
||||
//! stayed up, routes sent by peers were dropped on the floor, and the status
|
||||
//! page showed an empty table. Now the server tells its peers it is going
|
||||
//! and stops with an error, for whatever supervises it to restart it.
|
||||
//! The local RIB's actor stops only by panicking. Before, nothing noticed:
|
||||
//! sessions stayed up, nothing was announced any more, and the status page
|
||||
//! showed an empty table. Now the server tells its peers it is going and
|
||||
//! stops with an error, for whatever supervises it to restart it.
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
@@ -47,49 +47,10 @@ async fn farewell(peer: &mut TestPeer) -> Result<BgpError> {
|
||||
.await?
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_losing_the_adj_rib_in_stops_the_server_and_tells_the_peers() -> Result<()> {
|
||||
let mut server = TestServer::start(LOCAL_ASN).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let mut peer = established(&server).await?;
|
||||
|
||||
server.adj_rib_in.stop_for_testing();
|
||||
|
||||
// Nothing has used the table since; the server notices by itself.
|
||||
let outcome = tokio::time::timeout(TIMEOUT, server.stopped()).await?;
|
||||
assert!(
|
||||
matches!(outcome, Err(ServerError::RibStopped(RibStopped { rib })) if rib == "Adj-RIB-In"),
|
||||
"{outcome:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
farewell(&mut peer).await?,
|
||||
BgpError::Cease(CeaseSubcode::AdminShutdown)
|
||||
);
|
||||
|
||||
// The status interface says the table cannot be read, where it used to
|
||||
// show an empty one.
|
||||
let client = reqwest::Client::new();
|
||||
for path in ["/api/status", "/api/routes/received", "/metricz"] {
|
||||
let response = client.get(format!("{base}{path}")).send().await?;
|
||||
assert_eq!(response.status(), 503, "{path}");
|
||||
assert_eq!(
|
||||
response.text().await?,
|
||||
"the Adj-RIB-In has stopped",
|
||||
"{path}"
|
||||
);
|
||||
}
|
||||
// What does not need the table still answers.
|
||||
let response = client
|
||||
.get(format!("{base}/api/routes/local"))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(response.status(), 200);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_losing_the_local_rib_stops_the_server_and_tells_the_peers() -> Result<()> {
|
||||
let mut server = TestServer::start(LOCAL_ASN).await?;
|
||||
let (base, _http) = server.serve_http().await?;
|
||||
let mut peer = established(&server).await?;
|
||||
|
||||
server.local_rib.stop_for_testing();
|
||||
@@ -103,5 +64,24 @@ async fn test_losing_the_local_rib_stops_the_server_and_tells_the_peers() -> Res
|
||||
farewell(&mut peer).await?,
|
||||
BgpError::Cease(CeaseSubcode::AdminShutdown)
|
||||
);
|
||||
|
||||
// The status interface says the table cannot be read, where it used to
|
||||
// show an empty one.
|
||||
let client = reqwest::Client::new();
|
||||
for path in ["/api/status", "/api/routes/local", "/metricz"] {
|
||||
let response = client.get(format!("{base}{path}")).send().await?;
|
||||
assert_eq!(response.status(), 503, "{path}");
|
||||
assert_eq!(
|
||||
response.text().await?,
|
||||
"the local RIB has stopped",
|
||||
"{path}"
|
||||
);
|
||||
}
|
||||
// What does not need that table still answers.
|
||||
let response = client
|
||||
.get(format!("{base}/api/routes/received"))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(response.status(), 200);
|
||||
Ok(())
|
||||
}
|
||||
@@ -298,6 +298,6 @@ async fn test_metricz() -> Result<()> {
|
||||
"{text}"
|
||||
);
|
||||
assert!(metric("bgp_local_routes").is_some(), "{text}");
|
||||
assert!(metric("bgp_adj_rib_in_routes").is_some(), "{text}");
|
||||
assert!(metric("bgp_rib_routes").is_some(), "{text}");
|
||||
Ok(())
|
||||
}
|
||||
@@ -105,7 +105,7 @@ async fn test_bird_open_and_end_of_rib_are_accepted() -> Result<()> {
|
||||
1,
|
||||
"session dropped after End-of-RIB"
|
||||
);
|
||||
assert!(server.adj_rib_in.snapshot().await.unwrap().is_empty());
|
||||
assert!(server.received_routes().is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ async fn test_routes_of_a_family_not_negotiated_are_not_accepted() -> Result<()>
|
||||
|
||||
let routes = tokio::time::timeout(TIMEOUT, async {
|
||||
loop {
|
||||
let routes = server.adj_rib_in.snapshot().await.unwrap();
|
||||
let routes = server.received_routes();
|
||||
if !routes.is_empty() {
|
||||
return routes;
|
||||
}
|
||||
@@ -304,7 +304,7 @@ async fn test_a_peer_without_capabilities_gets_two_octet_as_paths() -> Result<()
|
||||
.await?;
|
||||
let routes = tokio::time::timeout(TIMEOUT, async {
|
||||
loop {
|
||||
let routes = server.adj_rib_in.snapshot().await.unwrap();
|
||||
let routes = server.received_routes();
|
||||
if !routes.is_empty() {
|
||||
return routes;
|
||||
}
|
||||
@@ -314,8 +314,7 @@ async fn test_a_peer_without_capabilities_gets_two_octet_as_paths() -> Result<()
|
||||
.await?;
|
||||
assert!(
|
||||
routes[0]
|
||||
.entry
|
||||
.path_attributes
|
||||
.attributes
|
||||
.contains(&AsPathAttribute::from_asns(vec![PEER_ASN, 64512]))
|
||||
);
|
||||
Ok(())
|
||||
|
||||
@@ -5,11 +5,10 @@ use std::net::Ipv4Addr;
|
||||
use std::time::Duration;
|
||||
|
||||
use bgp_integration_tests::test_peer::TestPeer;
|
||||
use bgp_integration_tests::test_server::TestServer;
|
||||
use bgp_integration_tests::test_server::{ReceivedRoute, TestServer};
|
||||
use bgp_packet::errors::{BgpError, UpdateMsgSubcode};
|
||||
use bgp_packet::message::{LocalPrefPathAttribute, Message, PathAttribute};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::SnapshotEntry;
|
||||
use bgp_server::metrics::UPDATE_ERRORS;
|
||||
use eyre::{Result, bail};
|
||||
|
||||
@@ -70,12 +69,12 @@ async fn established(server: &TestServer, asn: u32) -> Result<TestPeer> {
|
||||
}
|
||||
|
||||
/// Waits until the server's Adj-RIB-In holds exactly `prefixes`.
|
||||
async fn wait_for_routes(server: &TestServer, prefixes: &[&str]) -> Result<Vec<SnapshotEntry>> {
|
||||
async fn wait_for_routes(server: &TestServer, prefixes: &[&str]) -> Result<Vec<ReceivedRoute>> {
|
||||
let mut expected: Vec<IpPrefix> = prefixes.iter().map(|p| prefix(p)).collect();
|
||||
expected.sort_by_key(|p| p.to_string());
|
||||
Ok(tokio::time::timeout(TIMEOUT, async {
|
||||
loop {
|
||||
let mut routes = server.adj_rib_in.snapshot().await.unwrap();
|
||||
let mut routes = server.received_routes();
|
||||
routes.sort_by_key(|r| r.prefix.to_string());
|
||||
if routes.iter().map(|r| &r.prefix).eq(expected.iter()) {
|
||||
return routes;
|
||||
@@ -154,11 +153,10 @@ async fn test_a_malformed_aggregator_is_dropped_and_the_route_kept() -> Result<(
|
||||
peer.send_raw(&update(&attributes, &ROUTE_A)).await?;
|
||||
|
||||
let routes = wait_for_routes(&server, &["10.1.0.0/16"]).await?;
|
||||
assert_eq!(routes[0].entry.path_attributes.len(), 3);
|
||||
assert_eq!(routes[0].attributes.len(), 3);
|
||||
assert!(
|
||||
!routes[0]
|
||||
.entry
|
||||
.path_attributes
|
||||
.attributes
|
||||
.iter()
|
||||
.any(|a| matches!(a, PathAttribute::Aggregator(_)))
|
||||
);
|
||||
@@ -170,10 +168,9 @@ async fn test_a_malformed_aggregator_is_dropped_and_the_route_kept() -> Result<(
|
||||
#[tokio::test]
|
||||
async fn test_local_pref_is_kept_only_from_an_internal_peer() -> Result<()> {
|
||||
let local_pref = attr(WELL_KNOWN, 5, &[0, 0, 0, 200]);
|
||||
let has_local_pref = |routes: &[SnapshotEntry]| {
|
||||
let has_local_pref = |routes: &[ReceivedRoute]| {
|
||||
routes[0]
|
||||
.entry
|
||||
.path_attributes
|
||||
.attributes
|
||||
.contains(&PathAttribute::LocalPref(LocalPrefPathAttribute(200)))
|
||||
};
|
||||
|
||||
|
||||
@@ -67,17 +67,15 @@ impl Message {
|
||||
/// handles without resetting the session have been handled. Use
|
||||
/// [`Message::decode_with_report`] to learn of them.
|
||||
pub fn decode(ctx: &ParserContext, buf: &[u8]) -> Result<Self, DecodeError> {
|
||||
Self::decode_with_report(ctx, buf).map(|(msg, _)| msg)
|
||||
Self::decode_with_report(ctx, buf).map(|decoded| decoded.message)
|
||||
}
|
||||
|
||||
/// As [`Message::decode`], and also returns the errors in an UPDATE
|
||||
/// that were handled without resetting the session. The list is empty
|
||||
/// for a well-formed UPDATE and for every other message.
|
||||
pub fn decode_with_report(
|
||||
ctx: &ParserContext,
|
||||
buf: &[u8],
|
||||
) -> Result<(Self, Vec<UpdateError>), DecodeError> {
|
||||
pub fn decode_with_report(ctx: &ParserContext, buf: &[u8]) -> Result<Decoded, DecodeError> {
|
||||
let mut update_errors = Vec::new();
|
||||
let mut attributes_wire = None;
|
||||
let header_error = |subcode, data: Vec<u8>, detail: String| DecodeError {
|
||||
notification: BgpError::MsgHeader(subcode),
|
||||
data,
|
||||
@@ -154,6 +152,7 @@ impl Message {
|
||||
2 => {
|
||||
let decoded = UpdateMessage::decode(ctx, body)?;
|
||||
update_errors = decoded.errors;
|
||||
attributes_wire = decoded.attributes_wire;
|
||||
Ok(Self::Update(decoded.update))
|
||||
}
|
||||
3 => NotificationMessage::from_wire(ctx, body)
|
||||
@@ -175,7 +174,11 @@ impl Message {
|
||||
format!("unknown BGP message type {other}"),
|
||||
)),
|
||||
}?;
|
||||
Ok((message, update_errors))
|
||||
Ok(Decoded {
|
||||
message,
|
||||
update_errors,
|
||||
attributes_wire,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parses a complete BGP message including the 19-byte header from `buf`.
|
||||
@@ -249,6 +252,18 @@ impl Message {
|
||||
}
|
||||
}
|
||||
|
||||
/// A message, and what else decoding it found out.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Decoded {
|
||||
pub message: Message,
|
||||
/// The errors in an UPDATE that were handled without resetting the
|
||||
/// session (RFC 7606). Empty for a well-formed UPDATE and for every
|
||||
/// other message.
|
||||
pub update_errors: Vec<UpdateError>,
|
||||
/// See [`crate::update_decode::DecodedUpdate::attributes_wire`].
|
||||
pub attributes_wire: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
/// A request to send again every route of one address family (RFC 2918).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RouteRefreshMessage {
|
||||
|
||||
@@ -65,6 +65,15 @@ pub struct DecodedUpdate {
|
||||
pub update: UpdateMessage,
|
||||
/// Empty when the UPDATE was well formed.
|
||||
pub errors: Vec<UpdateError>,
|
||||
/// The attributes in `update`, as they were on the wire, for a table
|
||||
/// that keeps attributes in that form and need not write them out
|
||||
/// again. MP_REACH_NLRI and MP_UNREACH_NLRI are left out, since they
|
||||
/// name prefixes, and so is every attribute that was discarded.
|
||||
///
|
||||
/// `None` when there are no routes for them to belong to, or when the
|
||||
/// bytes are not in the one form such a table keeps: on a session with
|
||||
/// two-octet AS numbers.
|
||||
pub attributes_wire: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
const ORIGIN: u8 = 1;
|
||||
@@ -160,7 +169,8 @@ impl UpdateMessage {
|
||||
let announced_routes = prefixes("NLRI", nlri_buf)?;
|
||||
|
||||
let mut errors = Vec::new();
|
||||
let path_attributes = attributes(ctx, attributes_buf, &mut errors)?;
|
||||
let mut kept_wire = Vec::with_capacity(attributes_buf.len());
|
||||
let path_attributes = attributes(ctx, attributes_buf, &mut errors, &mut kept_wire)?;
|
||||
let mut update = UpdateMessage {
|
||||
withdrawn_routes,
|
||||
path_attributes,
|
||||
@@ -178,21 +188,31 @@ impl UpdateMessage {
|
||||
detail: format!("mandatory attribute {missing} is missing"),
|
||||
});
|
||||
}
|
||||
if errors
|
||||
let withdrawn = errors
|
||||
.iter()
|
||||
.any(|e| e.handling == Handling::TreatAsWithdraw)
|
||||
{
|
||||
.any(|e| e.handling == Handling::TreatAsWithdraw);
|
||||
if withdrawn {
|
||||
update = as_withdrawal(update);
|
||||
}
|
||||
Ok(DecodedUpdate { update, errors })
|
||||
let attributes_wire =
|
||||
(!withdrawn && ctx.four_octet_asn == Some(true) && !kept_wire.is_empty())
|
||||
.then_some(kept_wire);
|
||||
Ok(DecodedUpdate {
|
||||
update,
|
||||
errors,
|
||||
attributes_wire,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads the path attributes, leaving out the ones that are discarded.
|
||||
/// Reads the path attributes, leaving out the ones that are discarded. The
|
||||
/// bytes of the ones that are kept and are a property of a route (all but
|
||||
/// MP_REACH_NLRI and MP_UNREACH_NLRI) are added to `kept_wire`.
|
||||
fn attributes(
|
||||
ctx: &ParserContext,
|
||||
mut buf: &[u8],
|
||||
errors: &mut Vec<UpdateError>,
|
||||
kept_wire: &mut Vec<u8>,
|
||||
) -> Result<Vec<PathAttribute>, DecodeError> {
|
||||
let mut attributes = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
@@ -283,6 +303,7 @@ fn attributes(
|
||||
type_code,
|
||||
payload: value.to_vec(),
|
||||
});
|
||||
kept_wire.extend_from_slice(attribute_wire);
|
||||
continue;
|
||||
};
|
||||
let flags_are_wrong = flags.optional() != optional || flags.transitive() != transitive;
|
||||
@@ -349,7 +370,13 @@ fn attributes(
|
||||
}
|
||||
};
|
||||
match parsed {
|
||||
Ok(attribute) => attributes.push(attribute),
|
||||
Ok(attribute) => {
|
||||
// With wrong flags it was read only to find its routes.
|
||||
if !flags_are_wrong && !matches!(type_code, MP_REACH_NLRI | MP_UNREACH_NLRI) {
|
||||
kept_wire.extend_from_slice(attribute_wire);
|
||||
}
|
||||
attributes.push(attribute);
|
||||
}
|
||||
Err(detail) => match handling_of_malformed(type_code) {
|
||||
Some(handling) => error(
|
||||
handling,
|
||||
@@ -562,6 +589,43 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_kept_attributes_are_handed_over_as_they_were_on_the_wire() {
|
||||
// All of a well-formed UPDATE's attributes, but not MP_REACH_NLRI.
|
||||
let attributes = vec![mp_reach_v6(), origin(), as_path(), next_hop()];
|
||||
let decoded = UpdateMessage::decode(&ebgp(), &body(&[], &attributes, &NLRI)).unwrap();
|
||||
assert_eq!(
|
||||
decoded.attributes_wire,
|
||||
Some([origin(), as_path(), next_hop()].concat())
|
||||
);
|
||||
|
||||
// Not an attribute that was discarded.
|
||||
let decoded = announce_with(&ebgp(), local_pref());
|
||||
assert_eq!(decoded.attributes_wire, Some(mandatory().concat()));
|
||||
|
||||
// Nothing when the routes were withdrawn instead: there is no route
|
||||
// for the attributes to belong to.
|
||||
let decoded = announce_with(&ebgp(), attr(OPTIONAL, MED, &[1]));
|
||||
assert_eq!(decoded.attributes_wire, None);
|
||||
let decoded = UpdateMessage::decode(&ebgp(), &body(&NLRI, &[], &[])).unwrap();
|
||||
assert_eq!(decoded.attributes_wire, None);
|
||||
|
||||
// Nor on a session with two-octet AS numbers, where the bytes are
|
||||
// not in the form a table keeps.
|
||||
let two_octet = ParserContext {
|
||||
four_octet_asn: Some(false),
|
||||
ibgp: false,
|
||||
};
|
||||
let attributes = vec![
|
||||
origin(),
|
||||
attr(WELL_KNOWN, AS_PATH, &[2, 1, 0xfd, 0xe9]),
|
||||
next_hop(),
|
||||
];
|
||||
let decoded = UpdateMessage::decode(&two_octet, &body(&[], &attributes, &NLRI)).unwrap();
|
||||
assert!(decoded.errors.is_empty());
|
||||
assert_eq!(decoded.attributes_wire, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_update_that_announces_nothing_needs_no_attributes() {
|
||||
// End-of-RIB, and a plain withdrawal.
|
||||
|
||||
@@ -454,9 +454,10 @@ fn session_and_message() -> impl Strategy<Value = (Session, Message)> {
|
||||
/// accepts: it can be shown and written, and unless the decoder had to
|
||||
/// repair it, it reads back as itself.
|
||||
fn check_decoding(ctx: &ParserContext, wire: &[u8]) -> Result<(), TestCaseError> {
|
||||
let Ok((msg, repairs)) = Message::decode_with_report(ctx, wire) else {
|
||||
let Ok(decoded) = Message::decode_with_report(ctx, wire) else {
|
||||
return Ok(());
|
||||
};
|
||||
let (msg, repairs) = (decoded.message, decoded.update_errors);
|
||||
// Display of what a peer sent must not be able to panic: it is logged.
|
||||
let _ = format!("{msg:?} {repairs:?}");
|
||||
if let Message::Update(update) = &msg {
|
||||
@@ -494,10 +495,10 @@ fn check_decoding(ctx: &ParserContext, wire: &[u8]) -> Result<(), TestCaseError>
|
||||
}
|
||||
prop_assert_eq!(usize::from(msg.wire_len(ctx).unwrap()), rewritten.len());
|
||||
if repairs.is_empty() {
|
||||
let (again, repairs) = Message::decode_with_report(ctx, &rewritten)
|
||||
let again = Message::decode_with_report(ctx, &rewritten)
|
||||
.map_err(|e| TestCaseError::fail(format!("written but cannot be read: {e}")))?;
|
||||
prop_assert!(repairs.is_empty(), "{:?}", repairs);
|
||||
prop_assert_eq!(again, msg);
|
||||
prop_assert!(again.update_errors.is_empty(), "{:?}", again.update_errors);
|
||||
prop_assert_eq!(again.message, msg);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -595,11 +596,11 @@ proptest! {
|
||||
};
|
||||
prop_assert_eq!(usize::from(msg.wire_len(&ctx).unwrap()), wire.len());
|
||||
|
||||
let (decoded, repairs) = Message::decode_with_report(&ctx, &wire)
|
||||
let decoded = Message::decode_with_report(&ctx, &wire)
|
||||
.map_err(|e| TestCaseError::fail(format!("{e}")))?;
|
||||
// What we write is well formed: nothing for the decoder to repair.
|
||||
prop_assert!(repairs.is_empty(), "{:?}", repairs);
|
||||
prop_assert_eq!(&decoded, &msg);
|
||||
prop_assert!(decoded.update_errors.is_empty(), "{:?}", decoded.update_errors);
|
||||
prop_assert_eq!(&decoded.message, &msg);
|
||||
let (rest, _) = Message::from_wire(&ctx, &wire).unwrap();
|
||||
prop_assert!(rest.is_empty());
|
||||
}
|
||||
|
||||
@@ -1,277 +0,0 @@
|
||||
//! The Adj-RIB-In: routes as peers announced them, before any policy.
|
||||
//!
|
||||
//! One actor owns the table. Handles are cheap to clone and talk to it over
|
||||
//! a channel; changes are published as [`RibEvent`]s.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use tokio::sync::{broadcast, mpsc, oneshot};
|
||||
use tokio::task::AbortHandle;
|
||||
|
||||
use crate::entry::{PeerId, ReceivedUpdate, RibEntry, SnapshotEntry};
|
||||
use crate::error::RibStopped;
|
||||
use crate::event::RibEvent;
|
||||
|
||||
const EVENT_CHANNEL_CAPACITY: usize = 1024;
|
||||
const CMD_CHANNEL_CAPACITY: usize = 256;
|
||||
|
||||
enum RibCommand {
|
||||
/// Apply one UPDATE from `peer`.
|
||||
Apply {
|
||||
peer: PeerId,
|
||||
update: ReceivedUpdate,
|
||||
},
|
||||
/// Drop everything learned from `peer`, e.g. because its session ended.
|
||||
RemovePeer {
|
||||
peer: PeerId,
|
||||
reply: oneshot::Sender<usize>,
|
||||
},
|
||||
Snapshot {
|
||||
reply: oneshot::Sender<Vec<SnapshotEntry>>,
|
||||
},
|
||||
Len {
|
||||
reply: oneshot::Sender<usize>,
|
||||
},
|
||||
}
|
||||
|
||||
const STOPPED: RibStopped = RibStopped { rib: "Adj-RIB-In" };
|
||||
|
||||
/// A cheaply-cloneable handle to the Adj-RIB-In actor.
|
||||
///
|
||||
/// Every operation fails with [`RibStopped`] if the actor is gone; see
|
||||
/// there for what that means.
|
||||
#[derive(Clone)]
|
||||
pub struct AdjRibIn {
|
||||
tx: mpsc::Sender<RibCommand>,
|
||||
events: broadcast::Sender<RibEvent>,
|
||||
actor: AbortHandle,
|
||||
}
|
||||
|
||||
impl AdjRibIn {
|
||||
/// Spawns the backing actor and returns a handle.
|
||||
pub fn spawn() -> Self {
|
||||
let (tx, rx) = mpsc::channel(CMD_CHANNEL_CAPACITY);
|
||||
let (events, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
|
||||
let actor = tokio::spawn(run_actor(rx, events.clone())).abort_handle();
|
||||
Self { tx, events, actor }
|
||||
}
|
||||
|
||||
/// Applies one UPDATE received from `peer`: its withdrawals first, then
|
||||
/// its announcements, so a prefix named in both ends up announced
|
||||
/// (RFC 4271 §4.3).
|
||||
///
|
||||
/// Returns once the update is queued for the actor, which applies
|
||||
/// updates in the order they were queued.
|
||||
pub async fn apply(&self, peer: PeerId, update: ReceivedUpdate) -> Result<(), RibStopped> {
|
||||
self.tx
|
||||
.send(RibCommand::Apply { peer, update })
|
||||
.await
|
||||
.map_err(|_| STOPPED)
|
||||
}
|
||||
|
||||
/// Removes every route learned from `peer`, emitting a
|
||||
/// [`RibEvent::Withdrawn`] for each (RFC 4271 §8.2.2: routes from a
|
||||
/// terminated session are deleted). Returns how many were removed.
|
||||
pub async fn remove_peer(&self, peer: PeerId) -> Result<usize, RibStopped> {
|
||||
self.ask(|reply| RibCommand::RemovePeer { peer, reply })
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns a point-in-time snapshot of all routes in the RIB.
|
||||
pub async fn snapshot(&self) -> Result<Vec<SnapshotEntry>, RibStopped> {
|
||||
self.ask(|reply| RibCommand::Snapshot { reply }).await
|
||||
}
|
||||
|
||||
/// How many routes the RIB holds, without copying them.
|
||||
pub async fn len(&self) -> Result<usize, RibStopped> {
|
||||
self.ask(|reply| RibCommand::Len { reply }).await
|
||||
}
|
||||
|
||||
/// Whether the RIB holds no routes.
|
||||
pub async fn is_empty(&self) -> Result<bool, RibStopped> {
|
||||
Ok(self.len().await? == 0)
|
||||
}
|
||||
|
||||
/// Sends a command that is answered, and waits for the answer.
|
||||
async fn ask<T>(
|
||||
&self,
|
||||
command: impl FnOnce(oneshot::Sender<T>) -> RibCommand,
|
||||
) -> Result<T, RibStopped> {
|
||||
let (reply, answer) = oneshot::channel();
|
||||
self.tx.send(command(reply)).await.map_err(|_| STOPPED)?;
|
||||
answer.await.map_err(|_| STOPPED)
|
||||
}
|
||||
|
||||
/// Resolves when the actor has stopped. It never does in a healthy
|
||||
/// process; whoever owns the RIB waits on this to notice a failure even
|
||||
/// when nothing happens to be using the table.
|
||||
pub async fn stopped(&self) {
|
||||
self.tx.closed().await
|
||||
}
|
||||
|
||||
/// Stops the actor, as a panic in it would. For tests of how the rest
|
||||
/// of the server copes.
|
||||
#[doc(hidden)]
|
||||
pub fn stop_for_testing(&self) {
|
||||
self.actor.abort();
|
||||
}
|
||||
|
||||
/// Subscribe to a stream of RIB mutation events.
|
||||
///
|
||||
/// The receiver will see every [`RibEvent`] from the point of subscription.
|
||||
/// If the consumer falls behind by more than [`EVENT_CHANNEL_CAPACITY`]
|
||||
/// events it will receive a [`broadcast::error::RecvError::Lagged`] error.
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<RibEvent> {
|
||||
self.events.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_actor(mut rx: mpsc::Receiver<RibCommand>, events: broadcast::Sender<RibEvent>) {
|
||||
// Keyed by peer first, so that dropping a peer's routes when its session
|
||||
// ends touches only that peer's table.
|
||||
let mut table: HashMap<PeerId, HashMap<IpPrefix, RibEntry>> = HashMap::new();
|
||||
|
||||
while let Some(cmd) = rx.recv().await {
|
||||
match cmd {
|
||||
// Event send errors below mean there are no subscribers, which is fine.
|
||||
RibCommand::Apply { peer, update } => {
|
||||
let routes = table.entry(peer).or_default();
|
||||
for prefix in update.withdrawn {
|
||||
if routes.remove(&prefix).is_some() {
|
||||
let _ = events.send(RibEvent::Withdrawn { peer, prefix });
|
||||
}
|
||||
}
|
||||
for (prefix, entry) in update.announced {
|
||||
routes.insert(prefix, entry.clone());
|
||||
let _ = events.send(RibEvent::Announced {
|
||||
peer,
|
||||
prefix,
|
||||
entry,
|
||||
});
|
||||
}
|
||||
if routes.is_empty() {
|
||||
table.remove(&peer);
|
||||
}
|
||||
}
|
||||
RibCommand::RemovePeer { peer, reply } => {
|
||||
let routes = table.remove(&peer).unwrap_or_default();
|
||||
let removed = routes.len();
|
||||
for prefix in routes.into_keys() {
|
||||
let _ = events.send(RibEvent::Withdrawn { peer, prefix });
|
||||
}
|
||||
let _ = reply.send(removed);
|
||||
}
|
||||
RibCommand::Snapshot { reply } => {
|
||||
let snap = table
|
||||
.iter()
|
||||
.flat_map(|(peer, routes)| {
|
||||
routes.iter().map(|(prefix, entry)| SnapshotEntry {
|
||||
peer: *peer,
|
||||
prefix: *prefix,
|
||||
entry: entry.clone(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
// A failed reply means the asker stopped waiting.
|
||||
let _ = reply.send(snap);
|
||||
}
|
||||
RibCommand::Len { reply } => {
|
||||
let _ = reply.send(table.values().map(HashMap::len).sum());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn prefix(s: &str) -> IpPrefix {
|
||||
IpPrefix::try_from(s).unwrap()
|
||||
}
|
||||
|
||||
fn peer(port: u16, asn: u32) -> PeerId {
|
||||
PeerId {
|
||||
addr: ([192, 0, 2, 1], port).into(),
|
||||
asn,
|
||||
}
|
||||
}
|
||||
|
||||
fn announce(prefixes: &[&str]) -> ReceivedUpdate {
|
||||
let entry = RibEntry {
|
||||
path_attributes: Arc::from(vec![]),
|
||||
next_hop: None,
|
||||
received_at: SystemTime::now(),
|
||||
};
|
||||
ReceivedUpdate {
|
||||
withdrawn: vec![],
|
||||
announced: prefixes
|
||||
.iter()
|
||||
.map(|p| (prefix(p), entry.clone()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_prefix_withdrawn_and_announced_by_one_update_ends_up_announced() {
|
||||
let rib = AdjRibIn::spawn();
|
||||
let a = peer(179, 65001);
|
||||
rib.apply(a, announce(&["10.0.0.0/8"])).await.unwrap();
|
||||
|
||||
let mut update = announce(&["10.0.0.0/8"]);
|
||||
update.withdrawn = vec![prefix("10.0.0.0/8")];
|
||||
rib.apply(a, update).await.unwrap();
|
||||
|
||||
assert_eq!(rib.snapshot().await.unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn removing_a_peer_leaves_other_peers_routes_alone() {
|
||||
let rib = AdjRibIn::spawn();
|
||||
let (a, b) = (peer(179, 65001), peer(180, 65002));
|
||||
rib.apply(a, announce(&["10.0.0.0/8", "10.1.0.0/16"]))
|
||||
.await
|
||||
.unwrap();
|
||||
rib.apply(b, announce(&["10.0.0.0/8"])).await.unwrap();
|
||||
// Commands are handled in order, so once the snapshot is back the
|
||||
// announcements above have been applied and their events sent.
|
||||
assert_eq!(rib.snapshot().await.unwrap().len(), 3);
|
||||
assert_eq!(rib.len().await, Ok(3));
|
||||
let mut events = rib.subscribe();
|
||||
|
||||
assert_eq!(rib.remove_peer(a).await, Ok(2));
|
||||
|
||||
let left = rib.snapshot().await.unwrap();
|
||||
assert_eq!(left.len(), 1);
|
||||
assert_eq!(left[0].peer, b);
|
||||
for _ in 0..2 {
|
||||
assert!(
|
||||
matches!(events.recv().await.unwrap(), RibEvent::Withdrawn { peer, .. } if peer == a)
|
||||
);
|
||||
}
|
||||
assert_eq!(rib.remove_peer(a).await, Ok(0), "nothing left to remove");
|
||||
}
|
||||
|
||||
/// A stopped actor used to look like an empty table: updates vanished,
|
||||
/// and a snapshot came back with no routes.
|
||||
#[tokio::test]
|
||||
async fn a_stopped_actor_is_an_error_and_not_an_empty_table() {
|
||||
let rib = AdjRibIn::spawn();
|
||||
let a = peer(179, 65001);
|
||||
rib.apply(a, announce(&["10.0.0.0/8"])).await.unwrap();
|
||||
assert_eq!(rib.len().await, Ok(1));
|
||||
|
||||
rib.stop_for_testing();
|
||||
rib.stopped().await;
|
||||
|
||||
assert_eq!(rib.apply(a, announce(&["10.1.0.0/16"])).await, Err(STOPPED));
|
||||
assert_eq!(rib.snapshot().await.map(|s| s.len()), Err(STOPPED));
|
||||
assert_eq!(rib.len().await, Err(STOPPED));
|
||||
assert_eq!(rib.remove_peer(a).await, Err(STOPPED));
|
||||
assert_eq!(STOPPED.to_string(), "the Adj-RIB-In has stopped");
|
||||
}
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use bgp_packet::message::{NlriNextHop, PathAttribute, UpdateMessage};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
|
||||
/// Identifies a BGP peer by its TCP address and negotiated ASN.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct PeerId {
|
||||
pub addr: SocketAddr,
|
||||
pub asn: u32,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PeerId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}(AS{})", self.addr, self.asn)
|
||||
}
|
||||
}
|
||||
|
||||
/// A single route stored in the Adj-RIB-In.
|
||||
///
|
||||
/// Cloning is cheap: the attribute list is shared, not copied.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RibEntry {
|
||||
/// The attributes of the route. Every prefix announced by one UPDATE
|
||||
/// shares one list. MP_REACH_NLRI and MP_UNREACH_NLRI are left out: they
|
||||
/// carry prefixes, not properties of a route, and the one thing in them
|
||||
/// that is a property, the next hop, is in `next_hop`.
|
||||
pub path_attributes: Arc<[PathAttribute]>,
|
||||
/// Next hop of a route that arrived in MP_REACH_NLRI. `None` for an IPv4
|
||||
/// route from the body of the UPDATE, whose next hop is the NEXT_HOP
|
||||
/// attribute in `path_attributes`.
|
||||
pub next_hop: Option<NlriNextHop>,
|
||||
pub received_at: SystemTime,
|
||||
}
|
||||
|
||||
/// A flattened row returned by [`crate::AdjRibIn::snapshot`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SnapshotEntry {
|
||||
pub peer: PeerId,
|
||||
pub prefix: IpPrefix,
|
||||
pub entry: RibEntry,
|
||||
}
|
||||
|
||||
/// What one UPDATE message changes in the Adj-RIB-In.
|
||||
///
|
||||
/// An UPDATE names prefixes in four places: withdrawn routes and NLRI in the
|
||||
/// message body (IPv4 only), and the MP_UNREACH_NLRI and MP_REACH_NLRI
|
||||
/// attributes (every other family, RFC 4760). This gathers them into the
|
||||
/// two lists the RIB acts on.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ReceivedUpdate {
|
||||
pub withdrawn: Vec<IpPrefix>,
|
||||
pub announced: Vec<(IpPrefix, RibEntry)>,
|
||||
}
|
||||
|
||||
impl ReceivedUpdate {
|
||||
pub fn from_message(update: &UpdateMessage) -> Self {
|
||||
Self::from_message_at(update, SystemTime::now())
|
||||
}
|
||||
|
||||
fn from_message_at(update: &UpdateMessage, received_at: SystemTime) -> Self {
|
||||
let mut withdrawn = update.withdrawn_routes.clone();
|
||||
// One allocation for the whole UPDATE, however many prefixes it has.
|
||||
let shared: Arc<[PathAttribute]> = update
|
||||
.path_attributes
|
||||
.iter()
|
||||
.filter(|attr| {
|
||||
!matches!(
|
||||
attr,
|
||||
PathAttribute::MpReachNlri(_) | PathAttribute::MpUnreachNlri(_)
|
||||
)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
let entry = |next_hop| RibEntry {
|
||||
path_attributes: Arc::clone(&shared),
|
||||
next_hop,
|
||||
received_at,
|
||||
};
|
||||
|
||||
let mut announced: Vec<(IpPrefix, RibEntry)> = update
|
||||
.announced_routes
|
||||
.iter()
|
||||
.map(|prefix| (*prefix, entry(None)))
|
||||
.collect();
|
||||
for attr in &update.path_attributes {
|
||||
match attr {
|
||||
PathAttribute::MpReachNlri(mp) => announced.extend(
|
||||
mp.prefixes
|
||||
.iter()
|
||||
.map(|prefix| (*prefix, entry(Some(mp.next_hop.clone())))),
|
||||
),
|
||||
PathAttribute::MpUnreachNlri(mp) => withdrawn.extend(mp.prefixes.iter().cloned()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Self {
|
||||
withdrawn,
|
||||
announced,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::net::Ipv6Addr;
|
||||
|
||||
use bgp_packet::message::{
|
||||
MpReachNlriPathAttribute, MpUnreachNlriPathAttribute, OriginPathAttribute,
|
||||
};
|
||||
use bgp_primitives::constants::{Afi, Safi};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn prefix(s: &str) -> IpPrefix {
|
||||
IpPrefix::try_from(s).unwrap()
|
||||
}
|
||||
|
||||
fn mp_reach(prefixes: Vec<IpPrefix>) -> PathAttribute {
|
||||
PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
|
||||
afi: Afi::IPV6,
|
||||
safi: Safi::UNICAST,
|
||||
next_hop: NlriNextHop::Ipv6(Ipv6Addr::LOCALHOST),
|
||||
prefixes,
|
||||
})
|
||||
}
|
||||
|
||||
/// The Adj-RIB-In once kept a full copy of the attribute list per prefix,
|
||||
/// MP_REACH_NLRI included, so an UPDATE with n prefixes stored n copies
|
||||
/// of all n prefixes.
|
||||
#[test]
|
||||
fn prefixes_of_one_update_share_attributes_that_hold_no_prefixes() {
|
||||
let prefixes: Vec<IpPrefix> = (0..500)
|
||||
.map(|n| prefix(&format!("2001:db8:{n:x}::/48")))
|
||||
.collect();
|
||||
let update = UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: vec![
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
mp_reach(prefixes.clone()),
|
||||
],
|
||||
announced_routes: vec![],
|
||||
};
|
||||
|
||||
let received = ReceivedUpdate::from_message(&update);
|
||||
|
||||
assert_eq!(received.announced.len(), 500);
|
||||
let first = &received.announced[0].1;
|
||||
assert_eq!(
|
||||
first.path_attributes[..],
|
||||
[PathAttribute::Origin(OriginPathAttribute::IGP)]
|
||||
);
|
||||
for (prefix, entry) in &received.announced {
|
||||
assert!(prefixes.contains(prefix));
|
||||
assert!(Arc::ptr_eq(&entry.path_attributes, &first.path_attributes));
|
||||
assert_eq!(entry.next_hop, Some(NlriNextHop::Ipv6(Ipv6Addr::LOCALHOST)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefixes_are_gathered_from_the_body_and_the_multiprotocol_attributes() {
|
||||
let update = UpdateMessage {
|
||||
withdrawn_routes: vec![prefix("10.0.0.0/8")],
|
||||
path_attributes: vec![
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
mp_reach(vec![prefix("2001:db8:1::/48")]),
|
||||
PathAttribute::MpUnreachNlri(MpUnreachNlriPathAttribute {
|
||||
afi: Afi::IPV6,
|
||||
safi: Safi::UNICAST,
|
||||
prefixes: vec![prefix("2001:db8:2::/48")],
|
||||
}),
|
||||
],
|
||||
announced_routes: vec![prefix("192.0.2.0/24")],
|
||||
};
|
||||
|
||||
let received = ReceivedUpdate::from_message(&update);
|
||||
|
||||
assert_eq!(
|
||||
received.withdrawn,
|
||||
vec![prefix("10.0.0.0/8"), prefix("2001:db8:2::/48")]
|
||||
);
|
||||
let announced: Vec<_> = received
|
||||
.announced
|
||||
.iter()
|
||||
.map(|(p, e)| (*p, e.next_hop.is_some()))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
announced,
|
||||
vec![
|
||||
(prefix("192.0.2.0/24"), false),
|
||||
(prefix("2001:db8:1::/48"), true)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,13 @@
|
||||
/// changed.
|
||||
///
|
||||
/// The actor runs for as long as a handle to it exists; it stops early only
|
||||
/// by panicking. That is a bug, and one the server cannot work around:
|
||||
/// routes would be accepted from peers and go nowhere. Callers pass this up
|
||||
/// by panicking. That is a bug, and one the server cannot work around: it
|
||||
/// could no longer say what it originates. Callers pass this up
|
||||
/// until the process exits and is restarted, rather than carry on with a
|
||||
/// table that looks empty.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
|
||||
#[error("the {rib} has stopped")]
|
||||
pub struct RibStopped {
|
||||
/// Which table: "Adj-RIB-In" or "local RIB".
|
||||
/// Which table.
|
||||
pub rib: &'static str,
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
|
||||
use crate::entry::{PeerId, RibEntry};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RibEvent {
|
||||
Announced {
|
||||
peer: PeerId,
|
||||
prefix: IpPrefix,
|
||||
entry: RibEntry,
|
||||
},
|
||||
Withdrawn {
|
||||
peer: PeerId,
|
||||
prefix: IpPrefix,
|
||||
},
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
mod adj_rib_in;
|
||||
mod entry;
|
||||
//! The router's tables: the routes it has learned and chosen
|
||||
//! ([`table::RibTable`]) and the routes it originates ([`LocalRib`]).
|
||||
|
||||
mod error;
|
||||
mod event;
|
||||
mod local_rib;
|
||||
mod peer;
|
||||
pub mod table;
|
||||
|
||||
pub use adj_rib_in::AdjRibIn;
|
||||
pub use entry::{PeerId, ReceivedUpdate, RibEntry, SnapshotEntry};
|
||||
pub use error::RibStopped;
|
||||
pub use event::RibEvent;
|
||||
pub use local_rib::{LocalRib, LocalRibEvent, LocalRoute, LocalRouteAttrs};
|
||||
pub use peer::PeerId;
|
||||
@@ -146,7 +146,9 @@ impl LocalRib {
|
||||
answer.await.map_err(|_| STOPPED)
|
||||
}
|
||||
|
||||
/// Resolves when the actor has stopped. See [`crate::AdjRibIn::stopped`].
|
||||
/// Resolves when the actor has stopped. It never does in a healthy
|
||||
/// process; whoever owns the RIB waits on this to notice a failure even
|
||||
/// when nothing happens to be using the table.
|
||||
pub async fn stopped(&self) {
|
||||
self.tx.closed().await
|
||||
}
|
||||
@@ -158,8 +160,8 @@ impl LocalRib {
|
||||
self.actor.abort();
|
||||
}
|
||||
|
||||
/// Subscribe to mutations. See [`crate::AdjRibIn::subscribe`] for lag
|
||||
/// semantics.
|
||||
/// Subscribe to mutations. A subscriber that falls too far behind is
|
||||
/// told so with [`broadcast::error::RecvError::Lagged`].
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<LocalRibEvent> {
|
||||
self.events.subscribe()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
use std::net::SocketAddr;
|
||||
|
||||
/// Identifies a BGP peer by its TCP address and negotiated ASN.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct PeerId {
|
||||
pub addr: SocketAddr,
|
||||
pub asn: u32,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PeerId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}(AS{})", self.addr, self.asn)
|
||||
}
|
||||
}
|
||||
@@ -321,6 +321,24 @@ impl AttrStore {
|
||||
Some(set)
|
||||
}
|
||||
|
||||
/// The set for attributes as a session received them: `wire` is what
|
||||
/// the decoder kept of the UPDATE's attributes
|
||||
/// (`DecodedUpdate::attributes_wire`), and `next_hop` the next hop of
|
||||
/// the routes if they came in MP_REACH_NLRI.
|
||||
pub fn intern_received(
|
||||
&self,
|
||||
wire: &[u8],
|
||||
next_hop: Option<&NlriNextHop>,
|
||||
) -> Option<Arc<AttrSet>> {
|
||||
let Some(next_hop) = next_hop else {
|
||||
return self.intern_wire(wire);
|
||||
};
|
||||
let mut with_next_hop = BytesMut::with_capacity(wire.len() + 36);
|
||||
with_next_hop.put_slice(wire);
|
||||
encode(&[], Some(next_hop), &mut with_next_hop).ok()?;
|
||||
self.intern_wire(&with_next_hop)
|
||||
}
|
||||
|
||||
/// How many distinct sets are stored.
|
||||
pub fn len(&self) -> usize {
|
||||
(0..STRIPES).map(|i| self.stripe_at(i).len()).sum()
|
||||
|
||||
@@ -275,6 +275,25 @@ impl RibTable {
|
||||
/// A new follower of this table's best routes. It is given the whole
|
||||
/// table first and every change after.
|
||||
pub fn reader(self: &Arc<Self>) -> Reader {
|
||||
self.register(Phase::Resync {
|
||||
next: 0,
|
||||
announced: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// A new follower that is given only the changes from now on, for
|
||||
/// those who want the news and not the table. If it falls too far
|
||||
/// behind it is reset like any other, and is then given the table.
|
||||
pub fn reader_from_now(self: &Arc<Self>) -> Reader {
|
||||
let mut reader = self.register(Phase::Follow { next: 0 });
|
||||
for (index, shard) in self.shards.iter().enumerate() {
|
||||
let shard = read(shard);
|
||||
reader.set_cursor(index, &shard, shard.journal.seq);
|
||||
}
|
||||
reader
|
||||
}
|
||||
|
||||
fn register(self: &Arc<Self>, phase: Phase) -> Reader {
|
||||
let mut slots = self
|
||||
.reader_slots
|
||||
.lock()
|
||||
@@ -300,10 +319,7 @@ impl RibTable {
|
||||
table: Arc::clone(self),
|
||||
slot,
|
||||
cursors: vec![NO_CURSOR; self.shards.len()],
|
||||
phase: Phase::Resync {
|
||||
next: 0,
|
||||
announced: false,
|
||||
},
|
||||
phase,
|
||||
seen: 0,
|
||||
caught_up: false,
|
||||
}
|
||||
|
||||
@@ -124,6 +124,31 @@ fn a_new_reader_is_given_the_table_and_then_the_changes() {
|
||||
mirror.assert_matches(&fixture.table);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reader_from_now_is_given_the_changes_and_not_the_table() {
|
||||
let fixture = Fixture::new(&[external(1)]);
|
||||
let (old, new) = (prefix("10.0.0.0/8"), prefix("10.1.0.0/16"));
|
||||
fixture.announce(0, &old, &Route::via(&[1]));
|
||||
|
||||
let mut reader = fixture.table.reader_from_now();
|
||||
assert_eq!(
|
||||
reader
|
||||
.poll(100, |event| panic!("unexpected {event:?}"))
|
||||
.events,
|
||||
0
|
||||
);
|
||||
|
||||
fixture.announce(0, &new, &Route::via(&[1]));
|
||||
fixture.withdraw(0, &old);
|
||||
let (mut best, mut withdrawn) = (Vec::new(), Vec::new());
|
||||
reader.poll(100, |event| match event {
|
||||
Event::Best { prefix, .. } => best.push(prefix),
|
||||
Event::Withdrawn { prefix } => withdrawn.push(prefix),
|
||||
other => panic!("unexpected {other:?}"),
|
||||
});
|
||||
assert_eq!((best, withdrawn), (vec![new], vec![old]));
|
||||
}
|
||||
|
||||
/// A follower wants what the table has now, not how it got there.
|
||||
#[test]
|
||||
fn many_changes_to_a_prefix_between_reads_are_one_event() {
|
||||
|
||||
@@ -401,6 +401,29 @@ impl RibTable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Up to `limit` routes, with whether each is its prefix's best: every
|
||||
/// route of as many prefixes as fit, in no particular order. For showing
|
||||
/// some of a table that is too large to show all of.
|
||||
pub fn some_routes(&self, limit: usize) -> Vec<(IpPrefix, PeerIndex, Arc<AttrSet>, bool)> {
|
||||
let mut routes = Vec::new();
|
||||
for shard in &self.shards {
|
||||
for row in &read(shard).rows {
|
||||
for (i, path) in row.paths.iter().enumerate() {
|
||||
if routes.len() == limit {
|
||||
return routes;
|
||||
}
|
||||
routes.push((
|
||||
row.prefix,
|
||||
path.peer(),
|
||||
Arc::clone(&path.attrs),
|
||||
i == usize::from(row.best),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
routes
|
||||
}
|
||||
|
||||
/// Calls `visit` with every route in the table and whether it is its
|
||||
/// prefix's best. As [`RibTable::for_each_best`], without stopping the
|
||||
/// table.
|
||||
|
||||
+141
-78
@@ -13,17 +13,20 @@ use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
|
||||
use bgp_packet::errors::{BgpError, DecodeError, MsgHeaderSubcode};
|
||||
use bgp_packet::message::{BGP_HEADER_LEN, BGP_MAX_MSG_LEN, Message, PathAttribute, UpdateMessage};
|
||||
use bgp_packet::message::{
|
||||
BGP_HEADER_LEN, BGP_MAX_MSG_LEN, Decoded, Message, PathAttribute, UpdateMessage,
|
||||
};
|
||||
use bgp_packet::update_decode::UpdateError;
|
||||
use bgp_primitives::parser::ParserContext;
|
||||
use bgp_rib::{AdjRibIn, PeerId, ReceivedUpdate, RibStopped};
|
||||
use bgp_rib::PeerId;
|
||||
use bgp_rib::table::{Outcome, PeerIndex, PeerInfo, RibTable, TooManyPeers};
|
||||
use bytes::BytesMut;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::{Instant, Interval, Sleep, interval_at, sleep};
|
||||
use tracing::{error, info, warn};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use crate::collision::{CollisionTable, ConnectionGuard, Direction};
|
||||
use crate::error::SessionError;
|
||||
@@ -33,6 +36,7 @@ use crate::metrics::{
|
||||
MESSAGES_RECEIVED, MESSAGES_SENT, SESSIONS_ESTABLISHED_TOTAL, UPDATE_ERRORS, message_type_name,
|
||||
};
|
||||
use crate::panic::contained;
|
||||
use crate::routes;
|
||||
use crate::session::{SessionGuard, SessionHandle, SessionRegistry};
|
||||
|
||||
/// Number of outbound messages that may be queued for a session before
|
||||
@@ -45,7 +49,8 @@ pub struct SessionConfig {
|
||||
pub router_id: Ipv4Addr,
|
||||
pub local_asn: u32,
|
||||
pub hold_time_secs: u16,
|
||||
pub adj_rib_in: AdjRibIn,
|
||||
/// Where routes learned from the peer go.
|
||||
pub rib: Arc<RibTable>,
|
||||
pub registry: SessionRegistry,
|
||||
/// Shared table used to resolve connection collisions (RFC 4271 §6.8).
|
||||
pub collisions: CollisionTable,
|
||||
@@ -85,8 +90,8 @@ pub async fn run_session(
|
||||
};
|
||||
|
||||
// Whatever ended the session, routes learned over it are gone
|
||||
// (RFC 4271 §8.2.2). Do this before the registration drops so
|
||||
// subscribers see the withdrawals before the Down event.
|
||||
// (RFC 4271 §8.2.2). Do this before the registration drops so that the
|
||||
// routes are withdrawn before the session is seen to be down.
|
||||
if let Some(established) = session.established.take() {
|
||||
let Established {
|
||||
peer_id,
|
||||
@@ -94,11 +99,13 @@ pub async fn run_session(
|
||||
_registration,
|
||||
..
|
||||
} = established;
|
||||
match routes.remove().await {
|
||||
Ok(removed) => info!(peer = %peer_id, removed_routes = removed, "Session ended"),
|
||||
// No table left to remove them from.
|
||||
Err(e) => error!(peer = %peer_id, err = %e, "Session ended"),
|
||||
}
|
||||
let outcome = routes.remove().await;
|
||||
info!(
|
||||
peer = %peer_id,
|
||||
removed_routes = outcome.removed,
|
||||
best_changed = outcome.best_changed,
|
||||
"Session ended"
|
||||
);
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -129,6 +136,7 @@ impl Session<OwnedReadHalf> {
|
||||
keepalive_timer: None,
|
||||
collision: None,
|
||||
established: None,
|
||||
attributes_wire: None,
|
||||
peer_addr,
|
||||
local_addr,
|
||||
config,
|
||||
@@ -140,6 +148,8 @@ impl Session<OwnedReadHalf> {
|
||||
/// What exists only once a session is Established.
|
||||
struct Established {
|
||||
peer_id: PeerId,
|
||||
/// The peer's number in the routing table.
|
||||
rib_peer: PeerIndex,
|
||||
/// Messages the rest of the server queued for the peer.
|
||||
outbound: mpsc::Receiver<Message>,
|
||||
/// The routes learned from the peer, which go when the session does.
|
||||
@@ -157,24 +167,36 @@ struct Established {
|
||||
/// the task being aborted), dropping this removes them all the same. Left
|
||||
/// in the table they would be offered as routes of a peer that is gone.
|
||||
struct PeerRoutes {
|
||||
adj_rib_in: AdjRibIn,
|
||||
peer: PeerId,
|
||||
table: Arc<RibTable>,
|
||||
peer: PeerIndex,
|
||||
removed: bool,
|
||||
}
|
||||
|
||||
impl PeerRoutes {
|
||||
fn new(adj_rib_in: AdjRibIn, peer: PeerId) -> Self {
|
||||
fn new(table: Arc<RibTable>, peer: PeerIndex) -> Self {
|
||||
Self {
|
||||
adj_rib_in,
|
||||
table,
|
||||
peer,
|
||||
removed: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the routes and returns how many there were.
|
||||
async fn remove(mut self) -> Result<usize, RibStopped> {
|
||||
/// Removes the routes and says what that did.
|
||||
async fn remove(mut self) -> Outcome {
|
||||
self.removed = true;
|
||||
self.adj_rib_in.remove_peer(self.peer).await
|
||||
let (table, peer) = (Arc::clone(&self.table), self.peer);
|
||||
// Looks at every prefix, which for a full table is a tenth of a
|
||||
// second: too long to hold up the other sessions on this thread.
|
||||
tokio::task::spawn_blocking(move || Self::remove_now(&table, peer))
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn remove_now(table: &RibTable, peer: PeerIndex) -> Outcome {
|
||||
let outcome = table.remove_peer(peer);
|
||||
// Most of the peer's attribute sets were its alone.
|
||||
table.attrs().sweep();
|
||||
outcome
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,20 +206,22 @@ impl Drop for PeerRoutes {
|
||||
return;
|
||||
}
|
||||
// Not reached in the normal course of things. Drop cannot wait, so
|
||||
// the removal is handed to a task, if there is still a runtime to
|
||||
// run it.
|
||||
let (adj_rib_in, peer) = (self.adj_rib_in.clone(), self.peer);
|
||||
if let Ok(runtime) = tokio::runtime::Handle::try_current() {
|
||||
runtime.spawn(async move {
|
||||
match adj_rib_in.remove_peer(peer).await {
|
||||
Ok(removed) => warn!(
|
||||
%peer,
|
||||
removed_routes = removed,
|
||||
// the removal is handed to a thread that can take its time, if
|
||||
// there is still a runtime to give it to.
|
||||
let (table, peer) = (Arc::clone(&self.table), self.peer);
|
||||
match tokio::runtime::Handle::try_current() {
|
||||
Ok(runtime) => {
|
||||
runtime.spawn_blocking(move || {
|
||||
let outcome = Self::remove_now(&table, peer);
|
||||
warn!(
|
||||
removed_routes = outcome.removed,
|
||||
"Session task ended abnormally; removed its routes"
|
||||
),
|
||||
Err(e) => error!(%peer, err = %e, "Could not remove routes of ended session"),
|
||||
}
|
||||
});
|
||||
);
|
||||
});
|
||||
}
|
||||
Err(_) => {
|
||||
Self::remove_now(&table, peer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,6 +238,10 @@ struct Session<R> {
|
||||
/// This connection's slot in the collision table, once admitted.
|
||||
collision: Option<ConnectionGuard>,
|
||||
established: Option<Established>,
|
||||
/// The attributes of the UPDATE that is being acted on, as they were
|
||||
/// on the wire, if the decoder kept them. Set when the message is read
|
||||
/// and taken when its routes are stored.
|
||||
attributes_wire: Option<Vec<u8>>,
|
||||
peer_addr: SocketAddr,
|
||||
local_addr: SocketAddr,
|
||||
config: SessionConfig,
|
||||
@@ -255,9 +283,10 @@ impl<R: AsyncRead + Unpin> Session<R> {
|
||||
async fn next_event(&mut self) -> Result<Event, SessionError> {
|
||||
tokio::select! {
|
||||
frame = self.frames.next(&self.ctx) => match frame {
|
||||
Ok((msg, update_errors)) => {
|
||||
self.report(&update_errors);
|
||||
Ok(Event::Message(msg))
|
||||
Ok(decoded) => {
|
||||
self.report(&decoded.update_errors);
|
||||
self.attributes_wire = decoded.attributes_wire;
|
||||
Ok(Event::Message(decoded.message))
|
||||
}
|
||||
Err(SessionError::Decode(e)) => Ok(Event::DecodeFailed(e)),
|
||||
Err(e) => Err(e),
|
||||
@@ -342,7 +371,13 @@ impl<R: AsyncRead + Unpin> Session<R> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Action::Established(negotiated) => self.establish(negotiated),
|
||||
Action::Established(negotiated) => {
|
||||
if let Err(full) = self.establish(negotiated) {
|
||||
// What the machine wanted next no longer applies.
|
||||
error!(peer_addr = %self.peer_addr, err = %full, "Cannot take the peer's routes");
|
||||
queue = self.fsm.handle(Event::TableFull(full)).into();
|
||||
}
|
||||
}
|
||||
Action::RoutesIgnored(count) => {
|
||||
warn!(
|
||||
peer_addr = %self.peer_addr,
|
||||
@@ -351,18 +386,26 @@ impl<R: AsyncRead + Unpin> Session<R> {
|
||||
);
|
||||
}
|
||||
Action::DeliverUpdate(update) => {
|
||||
let attributes_wire = self.attributes_wire.take();
|
||||
if let Some(established) = &self.established {
|
||||
let (withdrawn, announced) = prefix_counts(&update);
|
||||
info!(peer = %established.peer_id, withdrawn, announced, "Received UPDATE");
|
||||
let applied = self
|
||||
.config
|
||||
.adj_rib_in
|
||||
.apply(established.peer_id, ReceivedUpdate::from_message(&update))
|
||||
.await;
|
||||
if let Err(e) = applied {
|
||||
// What the machine wanted next no longer applies.
|
||||
error!(peer = %established.peer_id, err = %e, "Cannot store routes");
|
||||
queue = self.fsm.handle(Event::RibStopped(e)).into();
|
||||
let applied = routes::apply(
|
||||
&self.config.rib,
|
||||
established.rib_peer,
|
||||
&update,
|
||||
attributes_wire.as_deref(),
|
||||
);
|
||||
match applied {
|
||||
// One line for every UPDATE is too much to log
|
||||
// by default: a full table is a million of them.
|
||||
Ok(outcome) => debug!(
|
||||
peer = %established.peer_id,
|
||||
added = outcome.added,
|
||||
replaced = outcome.replaced,
|
||||
removed = outcome.removed,
|
||||
best_changed = outcome.best_changed,
|
||||
"Received UPDATE"
|
||||
),
|
||||
Err(e) => return Some(Err(e.into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -378,12 +421,19 @@ impl<R: AsyncRead + Unpin> Session<R> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Makes the session visible to the rest of the server.
|
||||
fn establish(&mut self, negotiated: Negotiated) {
|
||||
/// Makes room for the peer's routes and makes the session visible to
|
||||
/// the rest of the server.
|
||||
fn establish(&mut self, negotiated: Negotiated) -> Result<(), TooManyPeers> {
|
||||
let peer_id = PeerId {
|
||||
addr: self.peer_addr,
|
||||
asn: negotiated.peer_asn,
|
||||
};
|
||||
let rib_peer = self.config.rib.add_peer(PeerInfo {
|
||||
asn: negotiated.peer_asn,
|
||||
router_id: negotiated.peer_identifier,
|
||||
address: self.peer_addr.ip(),
|
||||
ibgp: negotiated.peer_asn == self.config.local_asn,
|
||||
})?;
|
||||
info!(peer_addr = %self.peer_addr, peer_asn = negotiated.peer_asn, "Session established");
|
||||
SESSIONS_ESTABLISHED_TOTAL.inc();
|
||||
|
||||
@@ -396,22 +446,24 @@ impl<R: AsyncRead + Unpin> Session<R> {
|
||||
self.config.peer_name.clone(),
|
||||
self.config.direction,
|
||||
self.local_addr,
|
||||
negotiated.four_octet_asn,
|
||||
negotiated.families,
|
||||
&negotiated,
|
||||
rib_peer,
|
||||
outbound_tx,
|
||||
));
|
||||
self.established = Some(Established {
|
||||
peer_id,
|
||||
rib_peer,
|
||||
outbound,
|
||||
routes: PeerRoutes::new(self.config.adj_rib_in.clone(), peer_id),
|
||||
routes: PeerRoutes::new(Arc::clone(&self.config.rib), rib_peer),
|
||||
_registration: registration,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send(&mut self, msg: &Message) -> Result<(), SessionError> {
|
||||
if let (Message::Update(update), Some(established)) = (msg, &self.established) {
|
||||
let (withdrawn, announced) = prefix_counts(update);
|
||||
info!(peer = %established.peer_id, withdrawn, announced, "Sending UPDATE");
|
||||
debug!(peer = %established.peer_id, withdrawn, announced, "Sending UPDATE");
|
||||
}
|
||||
if let Message::Notification(notification) = msg {
|
||||
info!(peer_addr = %self.peer_addr, error = %notification.error, "Sending NOTIFICATION, closing session");
|
||||
@@ -500,20 +552,17 @@ impl<R: AsyncRead + Unpin> FrameReader<R> {
|
||||
/// With an UPDATE come the errors in it that were handled without
|
||||
/// resetting the session (RFC 7606); the UPDATE returned is the one to
|
||||
/// act on, with their handling already applied.
|
||||
async fn next(
|
||||
&mut self,
|
||||
ctx: &ParserContext,
|
||||
) -> Result<(Message, Vec<UpdateError>), SessionError> {
|
||||
async fn next(&mut self, ctx: &ParserContext) -> Result<Decoded, SessionError> {
|
||||
loop {
|
||||
if let Some(length) = frame_length(&self.buf)?
|
||||
&& self.buf.len() >= length
|
||||
{
|
||||
let frame = self.buf.split_to(length);
|
||||
let (msg, update_errors) = Message::decode_with_report(ctx, &frame)?;
|
||||
let decoded = Message::decode_with_report(ctx, &frame)?;
|
||||
MESSAGES_RECEIVED
|
||||
.with_label_values(&[message_type_name(&msg)])
|
||||
.with_label_values(&[message_type_name(&decoded.message)])
|
||||
.inc();
|
||||
return Ok((msg, update_errors));
|
||||
return Ok(decoded);
|
||||
}
|
||||
if self.reader.read_buf(&mut self.buf).await? == 0 {
|
||||
return Err(io::Error::new(
|
||||
@@ -592,16 +641,25 @@ mod tests {
|
||||
peer.write_all(&bytes[..47]).await.unwrap();
|
||||
assert!(matches!(
|
||||
frames.next(&ctx).await,
|
||||
Ok((Message::KeepAlive, _))
|
||||
Ok(Decoded {
|
||||
message: Message::KeepAlive,
|
||||
..
|
||||
})
|
||||
));
|
||||
assert!(matches!(
|
||||
frames.next(&ctx).await,
|
||||
Ok((Message::KeepAlive, _))
|
||||
Ok(Decoded {
|
||||
message: Message::KeepAlive,
|
||||
..
|
||||
})
|
||||
));
|
||||
peer.write_all(&bytes[47..]).await.unwrap();
|
||||
assert!(matches!(
|
||||
frames.next(&ctx).await,
|
||||
Ok((Message::KeepAlive, _))
|
||||
Ok(Decoded {
|
||||
message: Message::KeepAlive,
|
||||
..
|
||||
})
|
||||
));
|
||||
|
||||
drop(peer);
|
||||
@@ -628,7 +686,10 @@ mod tests {
|
||||
peer.write_all(&KEEPALIVE[10..]).await.unwrap();
|
||||
assert!(matches!(
|
||||
frames.next(&ctx).await,
|
||||
Ok((Message::KeepAlive, _))
|
||||
Ok(Decoded {
|
||||
message: Message::KeepAlive,
|
||||
..
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
@@ -645,7 +706,7 @@ mod tests {
|
||||
router_id: Ipv4Addr::new(10, 0, 0, 1),
|
||||
local_asn: 65000,
|
||||
hold_time_secs: 90,
|
||||
adj_rib_in: AdjRibIn::spawn(),
|
||||
rib: Arc::new(RibTable::new(4)),
|
||||
registry: SessionRegistry::new(),
|
||||
collisions: CollisionTable::new(false),
|
||||
direction: Direction::Inbound,
|
||||
@@ -676,11 +737,15 @@ mod tests {
|
||||
/// not leave the peer's routes behind.
|
||||
#[tokio::test]
|
||||
async fn routes_are_removed_even_if_the_session_never_cleans_up() {
|
||||
let adj_rib_in = AdjRibIn::spawn();
|
||||
let peer = PeerId {
|
||||
addr: ([192, 0, 2, 1], 179).into(),
|
||||
asn: 65001,
|
||||
};
|
||||
let table = Arc::new(RibTable::new(4));
|
||||
let peer = table
|
||||
.add_peer(PeerInfo {
|
||||
asn: 65001,
|
||||
router_id: Ipv4Addr::new(10, 0, 0, 2),
|
||||
address: Ipv4Addr::new(192, 0, 2, 1).into(),
|
||||
ibgp: false,
|
||||
})
|
||||
.unwrap();
|
||||
let update = UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes: vec![],
|
||||
@@ -688,22 +753,20 @@ mod tests {
|
||||
bgp_primitives::ip_prefix::IpPrefix::try_from("10.0.0.0/8").unwrap(),
|
||||
],
|
||||
};
|
||||
adj_rib_in
|
||||
.apply(peer, ReceivedUpdate::from_message(&update))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(adj_rib_in.len().await, Ok(1));
|
||||
routes::apply(&table, peer, &update, None).unwrap();
|
||||
assert_eq!(table.len(), 1);
|
||||
assert_eq!(table.attrs().len(), 1);
|
||||
|
||||
// As when a panic unwinds the session's task.
|
||||
drop(PeerRoutes::new(adj_rib_in.clone(), peer));
|
||||
drop(PeerRoutes::new(Arc::clone(&table), peer));
|
||||
|
||||
tokio::time::timeout(Duration::from_secs(5), async {
|
||||
while adj_rib_in.len().await != Ok(0) {
|
||||
while !table.is_empty() || !table.attrs().is_empty() {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("the routes are removed");
|
||||
.expect("the routes and their attributes are removed");
|
||||
}
|
||||
|
||||
proptest! {
|
||||
@@ -751,7 +814,7 @@ mod tests {
|
||||
};
|
||||
let reader = async {
|
||||
let mut received = Vec::new();
|
||||
while let Ok((msg, _)) = frames.next(&ctx).await {
|
||||
while let Ok(Decoded { message: msg, .. }) = frames.next(&ctx).await {
|
||||
received.push(msg);
|
||||
}
|
||||
received
|
||||
|
||||
@@ -7,6 +7,7 @@ use bgp_packet::errors::{BgpError, CeaseSubcode, DecodeError, FsmSubcode, OpenMs
|
||||
use bgp_packet::message::NotificationMessage;
|
||||
use bgp_primitives::parser::EncodeError;
|
||||
use bgp_rib::RibStopped;
|
||||
use bgp_rib::table::TooManyPeers;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::family::FamilySet;
|
||||
@@ -42,10 +43,9 @@ pub enum SessionError {
|
||||
Collision,
|
||||
/// The peer offers none of the address families configured for it, so
|
||||
/// the session could carry nothing.
|
||||
/// The table the session's routes go into is gone. The server is on
|
||||
/// its way down; the peer is told so.
|
||||
/// The routing table has no room for another peer.
|
||||
#[error(transparent)]
|
||||
RibStopped(#[from] RibStopped),
|
||||
TableFull(#[from] TooManyPeers),
|
||||
/// A bug: the session's own code panicked. The peer is told the session
|
||||
/// is being reset, and the session is cleaned up like any other.
|
||||
#[error("the session panicked: {0}")]
|
||||
@@ -77,7 +77,7 @@ impl SessionError {
|
||||
Self::OpenRefused(error) => error.clone(),
|
||||
Self::WrongPeerAs { .. } => BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs),
|
||||
Self::Collision => BgpError::Cease(CeaseSubcode::ConnCollisionResolution),
|
||||
Self::RibStopped(_) => BgpError::Cease(CeaseSubcode::AdminShutdown),
|
||||
Self::TableFull(_) => BgpError::Cease(CeaseSubcode::OutOfResources),
|
||||
// The session will be set up again; nothing is wrong at the
|
||||
// peer's end.
|
||||
Self::Panicked(_) => BgpError::Cease(CeaseSubcode::AdminReset),
|
||||
|
||||
@@ -18,7 +18,7 @@ use bgp_packet::errors::{BgpError, DecodeError, OpenMsgSubcode};
|
||||
use bgp_packet::message::{AS_TRANS, BGP4_VERSION, Message, UpdateMessage};
|
||||
use bgp_packet::open::{Capability, OpenMessage, OpenOption};
|
||||
|
||||
use bgp_rib::RibStopped;
|
||||
use bgp_rib::table::TooManyPeers;
|
||||
|
||||
use crate::error::SessionError;
|
||||
use crate::family::FamilySet;
|
||||
@@ -94,9 +94,9 @@ pub enum Event {
|
||||
CollisionLost,
|
||||
/// The rest of the server wants this message sent to the peer.
|
||||
Outbound(Message),
|
||||
/// The table that routes from the peer go into has stopped, so the
|
||||
/// session cannot go on.
|
||||
RibStopped(RibStopped),
|
||||
/// The routing table has no room for this peer's routes, so the session
|
||||
/// cannot go on.
|
||||
TableFull(TooManyPeers),
|
||||
}
|
||||
|
||||
/// Something the driver must do, in the order given.
|
||||
@@ -185,7 +185,7 @@ impl Fsm {
|
||||
Event::Message(msg) => self.on_message(msg),
|
||||
Event::DecodeFailed(e) => self.fail(SessionError::Decode(e)),
|
||||
Event::CollisionLost => self.fail(SessionError::Collision),
|
||||
Event::RibStopped(e) => self.fail(SessionError::RibStopped(e)),
|
||||
Event::TableFull(e) => self.fail(SessionError::TableFull(e)),
|
||||
Event::HoldTimerExpired => self.fail(match self.phase {
|
||||
Phase::Established(_) => SessionError::HoldTimerExpired,
|
||||
_ => SessionError::Timeout(self.state()),
|
||||
@@ -842,13 +842,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn losing_the_rib_ends_the_session_and_tells_the_peer_we_are_going() {
|
||||
fn a_full_table_ends_the_session_and_says_why() {
|
||||
let mut fsm = fsm_in(State::Established, 90);
|
||||
let stopped = RibStopped { rib: "Adj-RIB-In" };
|
||||
let actions = fsm.handle(Event::RibStopped(stopped));
|
||||
let actions = fsm.handle(Event::TableFull(TooManyPeers));
|
||||
let (sent, result) = ending(&fsm, actions);
|
||||
assert_eq!(sent, Some(BgpError::Cease(CeaseSubcode::AdminShutdown)));
|
||||
assert!(matches!(result, Err(SessionError::RibStopped(e)) if e == stopped));
|
||||
assert_eq!(sent, Some(BgpError::Cease(CeaseSubcode::OutOfResources)));
|
||||
assert!(matches!(result, Err(SessionError::TableFull(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -8,6 +8,7 @@ pub mod family;
|
||||
pub mod fsm;
|
||||
pub mod metrics;
|
||||
mod panic;
|
||||
mod routes;
|
||||
pub mod server;
|
||||
pub mod session;
|
||||
mod sync;
|
||||
|
||||
@@ -65,10 +65,34 @@ pub static LOCAL_ROUTES: LazyLock<IntGauge> = LazyLock::new(|| {
|
||||
.expect("metric registration")
|
||||
});
|
||||
|
||||
pub static ADJ_RIB_IN_ROUTES: LazyLock<IntGauge> = LazyLock::new(|| {
|
||||
pub static RIB_ROUTES: LazyLock<IntGauge> = LazyLock::new(|| {
|
||||
register_int_gauge!(
|
||||
"bgp_adj_rib_in_routes",
|
||||
"Routes held in Adj-RIB-In across all peers"
|
||||
"bgp_rib_routes",
|
||||
"Routes learned from peers, counting each peer's separately"
|
||||
)
|
||||
.expect("metric registration")
|
||||
});
|
||||
|
||||
pub static RIB_PREFIXES: LazyLock<IntGauge> = LazyLock::new(|| {
|
||||
register_int_gauge!(
|
||||
"bgp_rib_prefixes",
|
||||
"Prefixes with at least one route, and so with a best route"
|
||||
)
|
||||
.expect("metric registration")
|
||||
});
|
||||
|
||||
pub static RIB_ATTRIBUTE_SETS: LazyLock<IntGauge> = LazyLock::new(|| {
|
||||
register_int_gauge!(
|
||||
"bgp_rib_attribute_sets",
|
||||
"Distinct sets of path attributes held for the routes"
|
||||
)
|
||||
.expect("metric registration")
|
||||
});
|
||||
|
||||
pub static RIB_JOURNAL_ENTRIES: LazyLock<IntGauge> = LazyLock::new(|| {
|
||||
register_int_gauge!(
|
||||
"bgp_rib_journal_entries",
|
||||
"Changes of best route waiting for a follower to read them"
|
||||
)
|
||||
.expect("metric registration")
|
||||
});
|
||||
@@ -83,7 +107,10 @@ pub fn init() {
|
||||
LazyLock::force(&SESSIONS_ESTABLISHED);
|
||||
LazyLock::force(&COLLISIONS);
|
||||
LazyLock::force(&LOCAL_ROUTES);
|
||||
LazyLock::force(&ADJ_RIB_IN_ROUTES);
|
||||
LazyLock::force(&RIB_ROUTES);
|
||||
LazyLock::force(&RIB_PREFIXES);
|
||||
LazyLock::force(&RIB_ATTRIBUTE_SETS);
|
||||
LazyLock::force(&RIB_JOURNAL_ENTRIES);
|
||||
}
|
||||
|
||||
pub fn message_type_name(msg: &Message) -> &'static str {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
//! Putting the routes of a received UPDATE into the routing table.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use bgp_packet::message::{NlriNextHop, PathAttribute, UpdateMessage};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_primitives::parser::EncodeError;
|
||||
use bgp_rib::table::{AttrSet, Outcome, PeerIndex, RibTable};
|
||||
|
||||
/// Applies `update`, received from `peer`, to `table`: its withdrawals
|
||||
/// first and then its announcements, so that a prefix named in both ends up
|
||||
/// announced (RFC 4271 §4.3).
|
||||
///
|
||||
/// An UPDATE names prefixes in four places: withdrawn routes and NLRI in the
|
||||
/// message body, which are IPv4, and the MP_UNREACH_NLRI and MP_REACH_NLRI
|
||||
/// attributes for every other family (RFC 4760).
|
||||
///
|
||||
/// `attributes_wire` is the attributes as the decoder kept them, if it did
|
||||
/// (see `DecodedUpdate::attributes_wire`). With it the table finds the
|
||||
/// attribute set from those bytes, without writing the attributes out again.
|
||||
pub(crate) fn apply(
|
||||
table: &RibTable,
|
||||
peer: PeerIndex,
|
||||
update: &UpdateMessage,
|
||||
attributes_wire: Option<&[u8]>,
|
||||
) -> Result<Outcome, EncodeError> {
|
||||
let mut outcome = Outcome::default();
|
||||
|
||||
if !update.withdrawn_routes.is_empty() {
|
||||
outcome += table.withdraw(peer, &update.withdrawn_routes);
|
||||
}
|
||||
for attribute in &update.path_attributes {
|
||||
if let PathAttribute::MpUnreachNlri(mp) = attribute {
|
||||
outcome += table.withdraw(peer, &mp.prefixes);
|
||||
}
|
||||
}
|
||||
|
||||
let attributes = |next_hop: Option<&NlriNextHop>| -> Result<Arc<AttrSet>, EncodeError> {
|
||||
if let Some(set) =
|
||||
attributes_wire.and_then(|wire| table.attrs().intern_received(wire, next_hop))
|
||||
{
|
||||
return Ok(set);
|
||||
}
|
||||
table.attrs().intern(&update.path_attributes, next_hop)
|
||||
};
|
||||
let mut announce = |prefixes: &[IpPrefix], next_hop| -> Result<(), EncodeError> {
|
||||
if !prefixes.is_empty() {
|
||||
outcome += table.announce(peer, &attributes(next_hop)?, prefixes);
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
// Routes in the body have their next hop in the NEXT_HOP attribute,
|
||||
// which is one of the set's attributes; routes in MP_REACH_NLRI bring
|
||||
// theirs along.
|
||||
announce(&update.announced_routes, None)?;
|
||||
for attribute in &update.path_attributes {
|
||||
if let PathAttribute::MpReachNlri(mp) = attribute {
|
||||
announce(&mp.prefixes, Some(&mp.next_hop))?;
|
||||
}
|
||||
}
|
||||
Ok(outcome)
|
||||
}
|
||||
+21
-19
@@ -9,7 +9,8 @@ use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::task::{JoinError, JoinHandle};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use bgp_rib::{AdjRibIn, LocalRib, RibStopped};
|
||||
use bgp_rib::table::RibTable;
|
||||
use bgp_rib::{LocalRib, RibStopped};
|
||||
|
||||
use crate::advertise::spawn_advertiser;
|
||||
use crate::collision::{CollisionTable, Direction};
|
||||
@@ -30,9 +31,10 @@ pub struct BgpServerBuilder {
|
||||
pub struct BgpServer {
|
||||
config: Config,
|
||||
listeners: Vec<(SocketAddr, TcpListener)>,
|
||||
/// Shared RIB handle — created at bind time so callers can clone it before
|
||||
/// the accept loop starts (e.g. tests that want to introspect the RIB).
|
||||
pub adj_rib_in: AdjRibIn,
|
||||
/// The routing table: every route learned from peers, and the best one
|
||||
/// for each prefix. Created at bind time so that callers can share it
|
||||
/// before the accept loop starts.
|
||||
pub rib: Arc<RibTable>,
|
||||
/// Routes this router originates; announced to every established peer.
|
||||
pub local_rib: LocalRib,
|
||||
/// Live view of established sessions.
|
||||
@@ -65,7 +67,7 @@ impl BgpServerBuilder {
|
||||
}
|
||||
Ok(BgpServer {
|
||||
listeners,
|
||||
adj_rib_in: AdjRibIn::spawn(),
|
||||
rib: Arc::new(RibTable::new(table_shards())),
|
||||
local_rib,
|
||||
registry: SessionRegistry::with_configured(
|
||||
self.config
|
||||
@@ -111,7 +113,7 @@ impl BgpServer {
|
||||
router_id: self.config.router_id,
|
||||
local_asn: self.config.local_asn,
|
||||
hold_time_secs: self.config.hold_time_secs,
|
||||
adj_rib_in: self.adj_rib_in.clone(),
|
||||
rib: Arc::clone(&self.rib),
|
||||
registry: self.registry.clone(),
|
||||
collisions: CollisionTable::new(self.config.collision_detect_established_state),
|
||||
direction: Direction::Inbound,
|
||||
@@ -148,7 +150,7 @@ impl BgpServer {
|
||||
loop {
|
||||
let (listen_addr, result, listener) = tokio::select! {
|
||||
Some(accepted) = accepting.next() => accepted,
|
||||
failure = failed(&self.adj_rib_in, &self.local_rib, &mut tasks) => {
|
||||
failure = failed(&self.local_rib, &mut tasks) => {
|
||||
return Err(give_up(&self.registry, failure).await);
|
||||
}
|
||||
};
|
||||
@@ -192,6 +194,13 @@ async fn give_up(registry: &SessionRegistry, failure: ServerError) -> ServerErro
|
||||
failure
|
||||
}
|
||||
|
||||
/// How many shards the routing table has. Sessions write to it from every
|
||||
/// core at once; a good many more shards than cores keeps two of them from
|
||||
/// wanting the same one.
|
||||
fn table_shards() -> usize {
|
||||
16 * std::thread::available_parallelism().map_or(1, |cores| cores.get())
|
||||
}
|
||||
|
||||
/// How long sessions get to send their Cease when the server gives up.
|
||||
const SHUTDOWN_GRACE: Duration = Duration::from_secs(2);
|
||||
|
||||
@@ -201,19 +210,14 @@ fn named(name: &'static str, task: JoinHandle<()>) -> NamedTask {
|
||||
Box::pin(async move { (name, task.await) })
|
||||
}
|
||||
|
||||
/// Resolves when something the server depends on has stopped: a RIB's
|
||||
/// actor or one of `tasks`. In a healthy server it never resolves.
|
||||
/// Resolves when something the server depends on has stopped: the local
|
||||
/// RIB's actor or one of `tasks`. In a healthy server it never resolves.
|
||||
///
|
||||
/// Nothing else would notice. A stopped RIB is only found out by whoever
|
||||
/// next uses it, and a stopped task by nobody at all: the server would go
|
||||
/// on accepting sessions while storing or announcing nothing.
|
||||
async fn failed(
|
||||
adj_rib_in: &AdjRibIn,
|
||||
local_rib: &LocalRib,
|
||||
tasks: &mut FuturesUnordered<NamedTask>,
|
||||
) -> ServerError {
|
||||
async fn failed(local_rib: &LocalRib, tasks: &mut FuturesUnordered<NamedTask>) -> ServerError {
|
||||
tokio::select! {
|
||||
() = adj_rib_in.stopped() => RibStopped { rib: "Adj-RIB-In" }.into(),
|
||||
() = local_rib.stopped() => RibStopped { rib: "local RIB" }.into(),
|
||||
Some((task, result)) = tasks.next() => ServerError::TaskStopped {
|
||||
task,
|
||||
@@ -244,7 +248,6 @@ mod tests {
|
||||
/// If one of them panics, nothing else would ever find out.
|
||||
#[tokio::test]
|
||||
async fn a_task_that_panics_is_a_failure_of_the_server() {
|
||||
let adj_rib_in = AdjRibIn::spawn();
|
||||
let local_rib = LocalRib::spawn();
|
||||
let mut tasks: FuturesUnordered<NamedTask> = FuturesUnordered::new();
|
||||
tasks.push(named("dialer", tokio::spawn(std::future::pending())));
|
||||
@@ -253,7 +256,7 @@ mod tests {
|
||||
tokio::spawn(async { panic!("no route to announce") }),
|
||||
));
|
||||
|
||||
let failure = failed(&adj_rib_in, &local_rib, &mut tasks).await;
|
||||
let failure = failed(&local_rib, &mut tasks).await;
|
||||
assert_eq!(
|
||||
failure.to_string(),
|
||||
"the advertiser task stopped unexpectedly: it panicked: no route to announce"
|
||||
@@ -262,13 +265,12 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_stopped_rib_is_a_failure_of_the_server() {
|
||||
let adj_rib_in = AdjRibIn::spawn();
|
||||
let local_rib = LocalRib::spawn();
|
||||
let mut tasks: FuturesUnordered<NamedTask> = FuturesUnordered::new();
|
||||
tasks.push(named("advertiser", tokio::spawn(std::future::pending())));
|
||||
|
||||
local_rib.stop_for_testing();
|
||||
let failure = failed(&adj_rib_in, &local_rib, &mut tasks).await;
|
||||
let failure = failed(&local_rib, &mut tasks).await;
|
||||
assert!(
|
||||
matches!(failure, ServerError::RibStopped(RibStopped { rib }) if rib == "local RIB")
|
||||
);
|
||||
|
||||
@@ -16,10 +16,12 @@ use bgp_packet::message::NotificationMessage;
|
||||
|
||||
use bgp_packet::message::Message;
|
||||
use bgp_rib::PeerId;
|
||||
use bgp_rib::table::PeerIndex;
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
|
||||
use crate::collision::Direction;
|
||||
use crate::family::FamilySet;
|
||||
use crate::fsm::Negotiated;
|
||||
use crate::metrics::SESSIONS_ESTABLISHED;
|
||||
use crate::sync::IgnorePoisonRw;
|
||||
|
||||
@@ -64,6 +66,9 @@ pub struct SessionHandle {
|
||||
/// The address families negotiated with the peer. Routes of any other
|
||||
/// family are neither sent to it nor accepted from it.
|
||||
pub families: FamilySet,
|
||||
/// The peer's number in the routing table: what its routes there are
|
||||
/// marked with.
|
||||
pub rib_peer: PeerIndex,
|
||||
outbound: mpsc::Sender<Message>,
|
||||
}
|
||||
|
||||
@@ -77,8 +82,8 @@ impl SessionHandle {
|
||||
name: Option<String>,
|
||||
direction: Direction,
|
||||
local_addr: SocketAddr,
|
||||
four_octet_asn: bool,
|
||||
families: FamilySet,
|
||||
negotiated: &Negotiated,
|
||||
rib_peer: PeerIndex,
|
||||
outbound: mpsc::Sender<Message>,
|
||||
) -> Self {
|
||||
Self {
|
||||
@@ -87,8 +92,9 @@ impl SessionHandle {
|
||||
direction,
|
||||
local_addr,
|
||||
established_at: SystemTime::now(),
|
||||
four_octet_asn,
|
||||
families,
|
||||
four_octet_asn: negotiated.four_octet_asn,
|
||||
families: negotiated.families.clone(),
|
||||
rib_peer,
|
||||
outbound,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ version.workspace = true
|
||||
[dependencies]
|
||||
axum.workspace = true
|
||||
bgp-packet.workspace = true
|
||||
bgp-primitives.workspace = true
|
||||
bgp-rib.workspace = true
|
||||
bgp-server.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
+71
-25
@@ -4,14 +4,18 @@
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use axum::Json;
|
||||
use axum::extract::State;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use axum::extract::{Query, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use bgp_packet::community;
|
||||
use bgp_packet::message::{OriginPathAttribute, PathAttribute};
|
||||
use bgp_rib::{LocalRoute, RibEntry, RibStopped, SnapshotEntry};
|
||||
use bgp_packet::message::{NlriNextHop, OriginPathAttribute, PathAttribute};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::table::{AttrSet, PeerIndex};
|
||||
use bgp_rib::{LocalRoute, PeerId, RibStopped};
|
||||
use bgp_server::session::{ConfiguredPeer, DialState, SessionHandle, SessionRegistry};
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
@@ -35,7 +39,10 @@ pub struct StatusView {
|
||||
pub uptime_secs: u64,
|
||||
pub sessions: usize,
|
||||
pub local_routes: usize,
|
||||
/// Routes learned from peers, counting each peer's separately.
|
||||
pub received_routes: usize,
|
||||
/// Prefixes with at least one such route, and so with a best route.
|
||||
pub received_prefixes: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
@@ -187,14 +194,14 @@ pub struct AttrSummary {
|
||||
pub large_communities: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn summarize(entry: &RibEntry) -> AttrSummary {
|
||||
pub fn summarize(attributes: &[PathAttribute], next_hop: Option<&NlriNextHop>) -> AttrSummary {
|
||||
let mut s = AttrSummary {
|
||||
// A multiprotocol route has its next hop here; an IPv4 route has it
|
||||
// in the NEXT_HOP attribute below.
|
||||
next_hop: entry.next_hop.as_ref().map(ToString::to_string),
|
||||
next_hop: next_hop.map(ToString::to_string),
|
||||
..Default::default()
|
||||
};
|
||||
for attr in entry.path_attributes.iter() {
|
||||
for attr in attributes {
|
||||
match attr {
|
||||
PathAttribute::Origin(o) => s.origin = Some(origin_name(*o).to_string()),
|
||||
PathAttribute::AsPath(p) => {
|
||||
@@ -228,23 +235,48 @@ pub struct ReceivedRouteView {
|
||||
pub peer_addr: String,
|
||||
pub peer_asn: u32,
|
||||
pub prefix: String,
|
||||
pub received_at_unix: u64,
|
||||
/// Whether this is the route chosen for its prefix.
|
||||
pub best: bool,
|
||||
pub summary: AttrSummary,
|
||||
/// The decoded attribute list. MP_REACH_NLRI is not in it: its next hop
|
||||
/// is in the summary and its prefixes are routes of their own.
|
||||
pub attributes: Vec<PathAttribute>,
|
||||
}
|
||||
|
||||
impl From<&SnapshotEntry> for ReceivedRouteView {
|
||||
fn from(e: &SnapshotEntry) -> Self {
|
||||
Self {
|
||||
peer_addr: e.peer.addr.to_string(),
|
||||
peer_asn: e.peer.asn,
|
||||
prefix: e.prefix.to_string(),
|
||||
received_at_unix: unix_secs(e.entry.received_at),
|
||||
summary: summarize(&e.entry),
|
||||
attributes: e.entry.path_attributes.to_vec(),
|
||||
}
|
||||
/// Who the routes in the table are from: the table knows peers by number,
|
||||
/// the sessions know which number is whose.
|
||||
pub(crate) struct PeerNames(HashMap<PeerIndex, PeerId>);
|
||||
|
||||
impl PeerNames {
|
||||
pub(crate) fn of(registry: &SessionRegistry) -> Self {
|
||||
Self(
|
||||
registry
|
||||
.list()
|
||||
.into_iter()
|
||||
.map(|session| (session.rib_peer, session.peer))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// A view of one route. `None` if the route's session has just ended:
|
||||
/// the route is on its way out of the table.
|
||||
pub(crate) fn view(
|
||||
&self,
|
||||
prefix: &IpPrefix,
|
||||
peer: PeerIndex,
|
||||
attrs: &AttrSet,
|
||||
best: bool,
|
||||
) -> Option<ReceivedRouteView> {
|
||||
let peer = self.0.get(&peer)?;
|
||||
let (attributes, next_hop) = attrs.attributes();
|
||||
Some(ReceivedRouteView {
|
||||
peer_addr: peer.addr.to_string(),
|
||||
peer_asn: peer.asn,
|
||||
prefix: prefix.to_string(),
|
||||
best,
|
||||
summary: summarize(&attributes, next_hop.as_ref()),
|
||||
attributes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,7 +318,8 @@ pub async fn status(State(state): State<AppState>) -> Result<Json<StatusView>, U
|
||||
uptime_secs: state.info.started.elapsed().as_secs(),
|
||||
sessions: state.registry.list().len(),
|
||||
local_routes: state.local_rib.len().await?,
|
||||
received_routes: state.adj_rib_in.len().await?,
|
||||
received_routes: state.rib.len(),
|
||||
received_prefixes: state.rib.prefix_count(),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -322,16 +355,29 @@ pub async fn local_routes(
|
||||
Ok(Json(routes))
|
||||
}
|
||||
|
||||
/// How many routes [`received_routes`] returns unless asked for another
|
||||
/// number. The table can hold hundreds of millions.
|
||||
const DEFAULT_ROUTE_LIMIT: usize = 1000;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RouteQuery {
|
||||
/// At most this many routes.
|
||||
limit: Option<usize>,
|
||||
}
|
||||
|
||||
/// Some of the routes learned from peers: all the routes of as many
|
||||
/// prefixes as fit in the limit. `/api/status` has the totals.
|
||||
pub async fn received_routes(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<Vec<ReceivedRouteView>>, Unavailable> {
|
||||
Query(query): Query<RouteQuery>,
|
||||
) -> Json<Vec<ReceivedRouteView>> {
|
||||
let names = PeerNames::of(&state.registry);
|
||||
let mut routes: Vec<ReceivedRouteView> = state
|
||||
.adj_rib_in
|
||||
.snapshot()
|
||||
.await?
|
||||
.rib
|
||||
.some_routes(query.limit.unwrap_or(DEFAULT_ROUTE_LIMIT))
|
||||
.iter()
|
||||
.map(ReceivedRouteView::from)
|
||||
.filter_map(|(prefix, peer, attrs, best)| names.view(prefix, *peer, attrs, *best))
|
||||
.collect();
|
||||
routes.sort_by(|a, b| (&a.prefix, &a.peer_addr).cmp(&(&b.prefix, &b.peer_addr)));
|
||||
Ok(Json(routes))
|
||||
Json(routes)
|
||||
}
|
||||
+96
-41
@@ -1,19 +1,23 @@
|
||||
//! Server-sent event stream merging session, local-route and received-route
|
||||
//! Server-sent event stream merging session, local-route and best-route
|
||||
//! changes into one feed the UI can follow.
|
||||
|
||||
use std::convert::Infallible;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::extract::State;
|
||||
use axum::response::sse::{Event, KeepAlive, Sse};
|
||||
use bgp_rib::{LocalRibEvent, RibEvent};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::LocalRibEvent;
|
||||
use bgp_rib::table::{self as journal, AttrSet, PeerIndex};
|
||||
use bgp_server::session::SessionEvent;
|
||||
use futures::stream::{Stream, StreamExt};
|
||||
use serde::Serialize;
|
||||
use tokio_stream::wrappers::BroadcastStream;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
|
||||
use tokio_stream::wrappers::{BroadcastStream, ReceiverStream};
|
||||
|
||||
use crate::AppState;
|
||||
use crate::api::{LocalRouteView, PeerView, ReceivedRouteView, summarize};
|
||||
use crate::api::{LocalRouteView, PeerNames, PeerView, ReceivedRouteView};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
@@ -35,16 +39,17 @@ pub enum WireEvent {
|
||||
LocalWithdrawn {
|
||||
prefix: String,
|
||||
},
|
||||
ReceivedAnnounced {
|
||||
/// The best route for a prefix is now this one.
|
||||
BestChanged {
|
||||
route: ReceivedRouteView,
|
||||
},
|
||||
ReceivedWithdrawn {
|
||||
peer_addr: String,
|
||||
peer_asn: u32,
|
||||
/// A prefix has no route any more.
|
||||
BestWithdrawn {
|
||||
prefix: String,
|
||||
},
|
||||
/// The client fell behind and missed `missed` events on `stream`; it
|
||||
/// should refetch the corresponding table.
|
||||
/// The client fell behind and missed events on `stream` (`missed` of
|
||||
/// them, where that is known); it should refetch the corresponding
|
||||
/// table.
|
||||
Lagged {
|
||||
stream: &'static str,
|
||||
missed: u64,
|
||||
@@ -85,38 +90,88 @@ impl WireEvent {
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn from_received(ev: Result<RibEvent, BroadcastStreamRecvError>) -> Self {
|
||||
match ev {
|
||||
Ok(RibEvent::Announced {
|
||||
peer,
|
||||
prefix,
|
||||
entry,
|
||||
}) => Self::ReceivedAnnounced {
|
||||
route: ReceivedRouteView {
|
||||
peer_addr: peer.addr.to_string(),
|
||||
peer_asn: peer.asn,
|
||||
prefix: prefix.to_string(),
|
||||
received_at_unix: entry
|
||||
.received_at
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0),
|
||||
summary: summarize(&entry),
|
||||
attributes: entry.path_attributes.to_vec(),
|
||||
},
|
||||
},
|
||||
Ok(RibEvent::Withdrawn { peer, prefix }) => Self::ReceivedWithdrawn {
|
||||
peer_addr: peer.addr.to_string(),
|
||||
peer_asn: peer.asn,
|
||||
prefix: prefix.to_string(),
|
||||
},
|
||||
Err(BroadcastStreamRecvError::Lagged(n)) => Self::Lagged {
|
||||
stream: "received_routes",
|
||||
missed: n,
|
||||
},
|
||||
/// The table's changes of best route as events, from when the client
|
||||
/// connects. Each client follows the table's journal with a reader of its
|
||||
/// own, so one that reads slowly holds nobody else up.
|
||||
fn best_routes(state: &AppState) -> impl Stream<Item = WireEvent> + use<> {
|
||||
let (events, stream) = mpsc::channel(BEST_ROUTE_BUFFER);
|
||||
let registry = state.registry.clone();
|
||||
// Taken before this returns, so that nothing that happens after the
|
||||
// client is answered is missed.
|
||||
let mut reader = state.rib.reader_from_now();
|
||||
tokio::spawn(async move {
|
||||
// If the reader falls too far behind it is given the whole table
|
||||
// again. That is not news: the client is told it missed something
|
||||
// and gets its table from the API.
|
||||
let mut syncing = false;
|
||||
loop {
|
||||
tokio::select! {
|
||||
() = reader.changed() => {}
|
||||
() = events.closed() => return,
|
||||
}
|
||||
loop {
|
||||
// Only what is needed is taken while the table is held;
|
||||
// the views are made afterwards.
|
||||
let mut changes = Vec::new();
|
||||
let progress = reader.poll(BEST_ROUTE_BUFFER, |event| match event {
|
||||
journal::Event::Reset => {
|
||||
syncing = true;
|
||||
changes.push(Change::Lagged);
|
||||
}
|
||||
journal::Event::Synced => syncing = false,
|
||||
journal::Event::Best {
|
||||
prefix,
|
||||
peer,
|
||||
attrs,
|
||||
} if !syncing => {
|
||||
changes.push(Change::Best(prefix, peer, Arc::clone(attrs)));
|
||||
}
|
||||
journal::Event::Withdrawn { prefix } if !syncing => {
|
||||
changes.push(Change::Withdrawn(prefix));
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
let names = PeerNames::of(®istry);
|
||||
for change in changes {
|
||||
let event = match change {
|
||||
Change::Best(prefix, peer, attrs) => {
|
||||
match names.view(&prefix, peer, &attrs, true) {
|
||||
Some(route) => WireEvent::BestChanged { route },
|
||||
None => continue,
|
||||
}
|
||||
}
|
||||
Change::Withdrawn(prefix) => WireEvent::BestWithdrawn {
|
||||
prefix: prefix.to_string(),
|
||||
},
|
||||
Change::Lagged => WireEvent::Lagged {
|
||||
stream: "best_routes",
|
||||
missed: 0,
|
||||
},
|
||||
};
|
||||
if events.send(event).await.is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if progress.caught_up {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
ReceiverStream::new(stream)
|
||||
}
|
||||
|
||||
/// How many best-route events are read from the journal, and queued for a
|
||||
/// client, at a time.
|
||||
const BEST_ROUTE_BUFFER: usize = 256;
|
||||
|
||||
enum Change {
|
||||
Best(IpPrefix, PeerIndex, Arc<AttrSet>),
|
||||
Withdrawn(IpPrefix),
|
||||
/// The client's reader fell too far behind the table.
|
||||
Lagged,
|
||||
}
|
||||
|
||||
pub async fn stream(
|
||||
@@ -124,7 +179,7 @@ pub async fn stream(
|
||||
) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
|
||||
let sessions = BroadcastStream::new(state.registry.subscribe()).map(WireEvent::from_session);
|
||||
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 = best_routes(&state);
|
||||
|
||||
let merged =
|
||||
futures::stream::select(sessions, futures::stream::select(local, received)).map(|ev| {
|
||||
|
||||
@@ -17,7 +17,8 @@ use axum::Router;
|
||||
use axum::http::header;
|
||||
use axum::response::{Html, IntoResponse, Redirect};
|
||||
use axum::routing::get;
|
||||
use bgp_rib::{AdjRibIn, LocalRib};
|
||||
use bgp_rib::LocalRib;
|
||||
use bgp_rib::table::RibTable;
|
||||
use bgp_server::session::SessionRegistry;
|
||||
use tokio::net::TcpListener;
|
||||
use tracing::info;
|
||||
@@ -78,7 +79,8 @@ pub struct AppState {
|
||||
pub info: Arc<RouterInfo>,
|
||||
pub registry: SessionRegistry,
|
||||
pub local_rib: LocalRib,
|
||||
pub adj_rib_in: AdjRibIn,
|
||||
/// The routing table: routes learned from peers and the best of them.
|
||||
pub rib: Arc<RibTable>,
|
||||
}
|
||||
|
||||
/// The status page, embedded so the binary is self-contained.
|
||||
|
||||
+12
-12
@@ -4,26 +4,26 @@
|
||||
use axum::extract::State;
|
||||
use axum::http::{StatusCode, header};
|
||||
use axum::response::IntoResponse;
|
||||
use bgp_rib::RibStopped;
|
||||
use bgp_server::metrics::{ADJ_RIB_IN_ROUTES, LOCAL_ROUTES};
|
||||
use bgp_server::metrics::{
|
||||
LOCAL_ROUTES, RIB_ATTRIBUTE_SETS, RIB_JOURNAL_ENTRIES, RIB_PREFIXES, RIB_ROUTES,
|
||||
};
|
||||
use prometheus::{Encoder, TextEncoder};
|
||||
|
||||
use crate::AppState;
|
||||
|
||||
pub async fn metricz(State(state): State<AppState>) -> impl IntoResponse {
|
||||
// A scrape that cannot read the tables fails. Serving the last known
|
||||
// sizes would hide that the daemon is broken from the one system whose
|
||||
// A scrape that cannot read a table fails. Serving the last known
|
||||
// size would hide that the daemon is broken from the one system whose
|
||||
// job is to notice.
|
||||
let sizes = async {
|
||||
Ok::<_, RibStopped>((state.local_rib.len().await?, state.adj_rib_in.len().await?))
|
||||
};
|
||||
match sizes.await {
|
||||
Ok((local, received)) => {
|
||||
LOCAL_ROUTES.set(i64::try_from(local).unwrap_or(i64::MAX));
|
||||
ADJ_RIB_IN_ROUTES.set(i64::try_from(received).unwrap_or(i64::MAX));
|
||||
}
|
||||
let gauge = |count: usize| i64::try_from(count).unwrap_or(i64::MAX);
|
||||
match state.local_rib.len().await {
|
||||
Ok(local) => LOCAL_ROUTES.set(gauge(local)),
|
||||
Err(e) => return (StatusCode::SERVICE_UNAVAILABLE, e.to_string()).into_response(),
|
||||
}
|
||||
RIB_ROUTES.set(gauge(state.rib.len()));
|
||||
RIB_PREFIXES.set(gauge(state.rib.prefix_count()));
|
||||
RIB_ATTRIBUTE_SETS.set(gauge(state.rib.attrs().len()));
|
||||
RIB_JOURNAL_ENTRIES.set(gauge(state.rib.journal_len()));
|
||||
|
||||
let mut buf = Vec::new();
|
||||
match TextEncoder::new().encode(&prometheus::gather(), &mut buf) {
|
||||
|
||||
@@ -67,9 +67,9 @@ View: <a href="/metricz">variables</a>, <a href="/api/status">status json</a>, <
|
||||
<tbody id="local-body"></tbody>
|
||||
</table>
|
||||
|
||||
<h1 id="received">Received routes, Adj-RIB-In (<span id="received-count">...</span>)</h1>
|
||||
<h1 id="received">Received routes (<span id="received-count">...</span>)</h1>
|
||||
<table class="data">
|
||||
<thead><tr><th>prefix</th><th>from peer</th><th>AS path</th><th>next hop</th><th>origin</th><th>MED</th><th>local pref</th><th>communities</th><th>large communities</th><th>age</th></tr></thead>
|
||||
<thead><tr><th>prefix</th><th>from peer</th><th>AS path</th><th>next hop</th><th>origin</th><th>MED</th><th>local pref</th><th>communities</th><th>large communities</th><th>best</th></tr></thead>
|
||||
<tbody id="received-body"></tbody>
|
||||
</table>
|
||||
|
||||
@@ -162,16 +162,19 @@ View: <a href="/metricz">variables</a>, <a href="/api/status">status json</a>, <
|
||||
}
|
||||
|
||||
function renderReceived() {
|
||||
const b = $('received-body'); $('received-count').textContent = state.received.length;
|
||||
const b = $('received-body'), st = state.status;
|
||||
$('received-count').textContent = st && st.received_routes > state.received.length
|
||||
? `showing ${state.received.length} of ${st.received_routes}, for ${st.received_prefixes} prefixes`
|
||||
: `${state.received.length}${st ? `, for ${st.received_prefixes} prefixes` : ''}`;
|
||||
if (!state.received.length) { setHTML(b, '<tr><td colspan="10" class="dim">No routes received from peers.</td></tr>'); return; }
|
||||
setHTML(b, state.received.map(r => { const s = r.summary; return `<tr>
|
||||
<td class="mono">${esc(r.prefix)}</td><td class="mono">${esc(r.peer_addr)} <span class="dim">AS${r.peer_asn}</span></td><td class="mono">${asPath(s.as_path)}</td>
|
||||
<td class="mono">${s.next_hop ? esc(s.next_hop) : DASH}</td><td>${s.origin ? esc(s.origin) : DASH}</td><td>${s.med ?? DASH}</td><td>${s.local_pref ?? DASH}</td>
|
||||
<td class="mono">${commList(s.communities, true)}</td><td class="mono">${commList(s.large_communities)}</td><td>${age(r.received_at_unix)}</td></tr>`; }).join(''));
|
||||
<td class="mono">${commList(s.communities, true)}</td><td class="mono">${commList(s.large_communities)}</td><td>${r.best ? '<span class="ok">best</span>' : ''}</td></tr>`; }).join(''));
|
||||
}
|
||||
|
||||
// ---- data loading ------------------------------------------------------
|
||||
async function loadStatus() { state.status = await getJSON('/api/status'); renderStatus(); renderLocal(); }
|
||||
async function loadStatus() { state.status = await getJSON('/api/status'); renderStatus(); renderLocal(); renderReceived(); }
|
||||
async function loadPeers() { state.peers = await getJSON('/api/peers'); renderPeers(); }
|
||||
async function loadLocal() { state.local = await getJSON('/api/routes/local'); renderLocal(); }
|
||||
async function loadReceived() { state.received = await getJSON('/api/routes/received'); renderReceived(); }
|
||||
@@ -193,8 +196,8 @@ View: <a href="/metricz">variables</a>, <a href="/api/status">status json</a>, <
|
||||
(r.communities.length ? ` [${names(r.communities)}]` : '') + (r.large_communities.length ? ` [${names(r.large_communities)}]` : '') +
|
||||
(r.prepend ? ` prepend ${r.prepend}` : '') + (r.med != null ? ` med ${r.med}` : ''); }
|
||||
case 'local_withdrawn': return `<span class="warn">no longer originating</span> ${esc(ev.prefix)}`;
|
||||
case 'received_announced': return `received ${esc(ev.route.prefix)} from AS${ev.route.peer_asn} path [${ev.route.summary.as_path.join(' ')}]`;
|
||||
case 'received_withdrawn': return `peer AS${ev.peer_asn} <span class="warn">withdrew</span> ${esc(ev.prefix)}`;
|
||||
case 'best_changed': return `best route for ${esc(ev.route.prefix)} is now from AS${ev.route.peer_asn} path [${ev.route.summary.as_path.join(' ')}]`;
|
||||
case 'best_withdrawn': return `${esc(ev.prefix)} has <span class="warn">no route</span> any more`;
|
||||
case 'lagged': return `<span class="bad">missed ${ev.missed} events</span> on ${esc(ev.stream)}`;
|
||||
default: return esc(JSON.stringify(ev));
|
||||
}
|
||||
|
||||
+5
-5
@@ -16,9 +16,10 @@ pub fn decode_and_check(four_octet_asn: bool, ibgp: bool, wire: &[u8]) {
|
||||
four_octet_asn: Some(four_octet_asn),
|
||||
ibgp,
|
||||
};
|
||||
let Ok((msg, repairs)) = Message::decode_with_report(&ctx, wire) else {
|
||||
let Ok(decoded) = Message::decode_with_report(&ctx, wire) else {
|
||||
return;
|
||||
};
|
||||
let (msg, repairs) = (decoded.message, decoded.update_errors);
|
||||
let _ = format!("{msg:?} {repairs:?}");
|
||||
|
||||
let mut rewritten = BytesMut::new();
|
||||
@@ -36,9 +37,8 @@ pub fn decode_and_check(four_octet_asn: bool, ibgp: bool, wire: &[u8]) {
|
||||
// An UPDATE the decoder had to repair (RFC 7606) is not what was on the
|
||||
// wire, and need not read back as itself.
|
||||
if repairs.is_empty() {
|
||||
let (again, repairs) =
|
||||
Message::decode_with_report(&ctx, &rewritten).expect("and read back");
|
||||
assert!(repairs.is_empty(), "{repairs:?}");
|
||||
assert_eq!(again, msg);
|
||||
let again = Message::decode_with_report(&ctx, &rewritten).expect("and read back");
|
||||
assert!(again.update_errors.is_empty(), "{:?}", again.update_errors);
|
||||
assert_eq!(again.message, msg);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user