Rework the speaker: sans-I/O FSM, sharded table, RFC 7606, ADD-PATH
The work of the speaker-cleanup branch as one commit; the branch keeps the step-by-step history. Sessions - The state machine is a pure function from events to actions, driven by one task per connection. Idle, Connect and Active belong to a dialer that keeps configured peers up with backoff. - Capabilities are negotiated: address families, four-octet AS numbers with AS_TRANS and AS4_PATH, route refresh, ADD-PATH per family and direction. - Collision detection per RFC 4271 section 6.8, by address and identifier. - Connections are taken only from configured peers, a few per address. - A panic in a session is contained and answered with a Cease; a core task that stops shuts the server down with a Cease to every peer. - Writes that make no progress are given up (RFC 9687). Codec - Malformed UPDATEs are handled as RFC 7606 says: treat-as-withdraw, attribute discard, or session reset, with the NOTIFICATION to send. - Address families are open types; any can be named and ignored. - Prefixes are fixed-size values, normalised, with path identifiers. - Lengths are filled in after writing; one definition of each type code. - Property tests and fuzz targets for every decoder. Routing table - Adj-RIBs-In and Loc-RIB in one sharded table that sessions write to directly. Routes are sixteen bytes; attribute sets are interned in wire form. Best-path selection per RFC 4271 section 9.1.2.2 with deterministic MED. - A journal per shard lets followers read changes at their own pace; one that falls behind is resynchronised. - On a RIPE RIS dump of 148 peers and 39.3M routes: loaded in 2.0 s on 12 cores, about 52 bytes a route. Announcing - Originated routes come from the configuration. Each session has its own exporter with an Adj-RIB-Out, so a slow peer delays only itself. - Routes to a peer in our own AS have an empty path and a LOCAL_PREF. Operations - HTTP status page, JSON API, event stream and Prometheus metrics. - Benchmarks on synthetic and real tables. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
1ec60255ea
commit
630c175918
138 files changed
+21650
-2925
No files matched your search
@@ -0,0 +1,42 @@
|
||||
[package]
|
||||
name = "bgp-bench"
|
||||
description = "Benchmarks at the scale of the Internet routing table"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
version.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
bgp-packet.workspace = true
|
||||
bgp-primitives.workspace = true
|
||||
bytes.workspace = true
|
||||
flate2 = "1"
|
||||
stats_alloc = { version = "0.1.10", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
bgp-integration-tests.workspace = true
|
||||
bgp-rib.workspace = true
|
||||
bgp-server.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
[features]
|
||||
# Counts every allocation, to report how much memory a table holds. The
|
||||
# counters are shared by all threads and slow them down, so timings are
|
||||
# taken without this and memory is measured in a run of its own.
|
||||
count-memory = ["dep:stats_alloc"]
|
||||
|
||||
[[bench]]
|
||||
name = "full_table"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "real_table"
|
||||
harness = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,215 @@
|
||||
//! 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 --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 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;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
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};
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use bgp_rib::table::{PeerIndex, PeerInfo, RibTable};
|
||||
use bytes::BytesMut;
|
||||
|
||||
fn env(name: &str, default: usize) -> usize {
|
||||
std::env::var(name)
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn mib(bytes: usize) -> f64 {
|
||||
bytes as f64 / (1024.0 * 1024.0)
|
||||
}
|
||||
|
||||
fn per_second(count: usize, elapsed: Duration) -> String {
|
||||
let rate = count as f64 / elapsed.as_secs_f64();
|
||||
if rate >= 1e6 {
|
||||
format!("{:.2} M/s", rate / 1e6)
|
||||
} else {
|
||||
format!("{:.0} k/s", rate / 1e3)
|
||||
}
|
||||
}
|
||||
|
||||
fn row(what: &str, elapsed: Duration, detail: String) {
|
||||
println!("{what:<44} {:>9.3} s {detail}", elapsed.as_secs_f64());
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let peers = env("BENCH_PEERS", 4) as u32;
|
||||
let shape = Shape::INTERNET.scaled_down(env("BENCH_SCALE_DOWN", 1));
|
||||
let threads = std::thread::available_parallelism().map_or(1, |n| n.get());
|
||||
|
||||
println!(
|
||||
"== 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 wire: Vec<Vec<BytesMut>> = (1..=peers)
|
||||
.map(|peer| encoded(&announcements(&prefixes, shape, peer)))
|
||||
.collect();
|
||||
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),
|
||||
);
|
||||
|
||||
println!("\n== Reading the messages (one thread)");
|
||||
let ctx = wire_context();
|
||||
let started = Instant::now();
|
||||
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",
|
||||
elapsed,
|
||||
format!(
|
||||
"{} routes, {} messages, {:.0} MiB/s",
|
||||
per_second(routes, elapsed),
|
||||
per_second(updates, elapsed),
|
||||
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");
|
||||
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
|
||||
})
|
||||
})
|
||||
.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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 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();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
let elapsed = started.elapsed();
|
||||
assert_eq!(table.len(), routes);
|
||||
row(
|
||||
&format!("decode, intern, insert, choose: {threads} thread(s)"),
|
||||
elapsed,
|
||||
format!(
|
||||
"{} routes; {}",
|
||||
per_second(routes, elapsed),
|
||||
describe(region.held(), routes, "route")
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
//! The same questions as `full_table`, asked of a real table dump: every
|
||||
//! route that the peers of a RIPE RIS route collector had announced to it.
|
||||
//!
|
||||
//! Needs a dump in `crates/bench/data/` (see the README there) or at the
|
||||
//! path in `BENCH_MRT`. Without one it says so and does nothing, so that
|
||||
//! `cargo bench` works on a fresh checkout. `BENCH_PEERS` limits the run to
|
||||
//! the peers with the most routes (default: all of them).
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
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};
|
||||
|
||||
/// 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 {
|
||||
bytes as f64 / (1024.0 * 1024.0)
|
||||
}
|
||||
|
||||
fn per_second(count: usize, elapsed: Duration) -> String {
|
||||
format!("{:.2} M/s", count as f64 / elapsed.as_secs_f64() / 1e6)
|
||||
}
|
||||
|
||||
fn row(what: &str, elapsed: Duration, detail: String) {
|
||||
println!("{what:<44} {:>9.3} s {detail}", elapsed.as_secs_f64());
|
||||
}
|
||||
|
||||
fn dump_path() -> Option<PathBuf> {
|
||||
if let Ok(path) = std::env::var("BENCH_MRT") {
|
||||
return Some(path.into());
|
||||
}
|
||||
let data = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("data");
|
||||
let mut dumps: Vec<PathBuf> = std::fs::read_dir(data)
|
||||
.ok()?
|
||||
.filter_map(|entry| Some(entry.ok()?.path()))
|
||||
.filter(|path| path.extension().is_some_and(|e| e == "gz"))
|
||||
.collect();
|
||||
dumps.sort();
|
||||
dumps.pop()
|
||||
}
|
||||
|
||||
type Attributes = (Arc<[PathAttribute]>, Option<NlriNextHop>);
|
||||
|
||||
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 Some(path) = dump_path() else {
|
||||
println!("No table dump found; see crates/bench/data/README.md. Nothing to do.");
|
||||
return;
|
||||
};
|
||||
println!("== Reading {}", path.display());
|
||||
let started = Instant::now();
|
||||
let dump = Dump::read(&path).expect("a readable dump");
|
||||
println!(
|
||||
"{} peers, {} routes, {} distinct attribute sets, read in {:.0} s",
|
||||
dump.peers.len(),
|
||||
dump.route_count(),
|
||||
dump.attribute_sets.len(),
|
||||
started.elapsed().as_secs_f64()
|
||||
);
|
||||
|
||||
let limit = std::env::var("BENCH_PEERS")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(dump.peers.len());
|
||||
let peers = dump.largest_peers(limit);
|
||||
let peers: Vec<usize> = peers
|
||||
.into_iter()
|
||||
.filter(|&peer| !dump.routes[peer].is_empty())
|
||||
.collect();
|
||||
describe(&dump, &peers);
|
||||
|
||||
// Parsed once, outside the timings: reading messages is measured by
|
||||
// `full_table`, and here the attributes arrive already parsed.
|
||||
let region = Region::begin();
|
||||
let parsed: Vec<Option<Attributes>> = dump
|
||||
.attribute_sets
|
||||
.iter()
|
||||
.map(|wire| parse_attributes(wire).map(|(a, next_hop)| (Arc::from(a), next_hop)))
|
||||
.collect();
|
||||
let parsed_bytes = retained(®ion);
|
||||
println!(
|
||||
"\nattribute sets, parsed (Arc<[PathAttribute]>) {:.0} MiB = {:.0} bytes a set",
|
||||
mib(parsed_bytes),
|
||||
parsed_bytes as f64 / parsed.len() as f64
|
||||
);
|
||||
let unparsed = parsed.iter().filter(|a| a.is_none()).count();
|
||||
if unparsed > 0 {
|
||||
println!("{unparsed} attribute sets could not be parsed; their routes are left out");
|
||||
}
|
||||
|
||||
let threads = std::thread::available_parallelism().map_or(1, |n| n.get());
|
||||
sharded_table(&dump, &peers, &parsed, 1);
|
||||
sharded_table(&dump, &peers, &parsed, threads);
|
||||
followed_table(&dump, &peers, &parsed, threads);
|
||||
}
|
||||
|
||||
/// What the table looks like, as far as it decides what a RIB costs.
|
||||
fn describe(dump: &Dump, peers: &[usize]) {
|
||||
let routes: usize = peers.iter().map(|&p| dump.routes[p].len()).sum();
|
||||
let full = peers
|
||||
.iter()
|
||||
.filter(|&&p| dump.routes[p].len() > 800_000)
|
||||
.count();
|
||||
let prefixes: HashSet<IpPrefix> = peers
|
||||
.iter()
|
||||
.flat_map(|&p| dump.routes[p].iter().map(|(prefix, _)| *prefix))
|
||||
.collect();
|
||||
let mut sets_used = HashSet::new();
|
||||
let mut sets_per_peer = 0usize;
|
||||
for &peer in peers {
|
||||
let own: HashSet<u32> = dump.routes[peer].iter().map(|(_, set)| *set).collect();
|
||||
sets_per_peer += own.len();
|
||||
sets_used.extend(own);
|
||||
}
|
||||
let attribute_bytes: usize = sets_used
|
||||
.iter()
|
||||
.map(|&set| dump.attribute_sets[set as usize].len())
|
||||
.sum();
|
||||
println!("\n== The table ({} peers used)", peers.len());
|
||||
println!("routes {routes:>12}");
|
||||
println!("peers with more than 800,000 routes {full:>12}");
|
||||
println!(
|
||||
"distinct prefixes {:>12}",
|
||||
prefixes.len()
|
||||
);
|
||||
println!(
|
||||
"routes per prefix {:>12.1}",
|
||||
routes as f64 / prefixes.len() as f64
|
||||
);
|
||||
println!(
|
||||
"distinct attribute sets {:>12}",
|
||||
sets_used.len()
|
||||
);
|
||||
println!(
|
||||
" if each peer's were kept apart {sets_per_peer:>12} ({:.2}x: sets shared between peers)",
|
||||
sets_per_peer as f64 / sets_used.len() as f64
|
||||
);
|
||||
println!(
|
||||
"routes per attribute set {:>12.1}",
|
||||
routes as f64 / sets_used.len() as f64
|
||||
);
|
||||
println!(
|
||||
"attribute set on the wire, mean {:>12.0} bytes",
|
||||
attribute_bytes as f64 / sets_used.len() as f64
|
||||
);
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// chosen best route.
|
||||
fn sharded_table(dump: &Dump, peers: &[usize], parsed: &[Option<Attributes>], threads: usize) {
|
||||
println!("\n== The sharded table, {threads} thread(s)");
|
||||
let updates = grouped(dump, peers, parsed);
|
||||
let routes: usize = updates
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|(_, prefixes)| prefixes.len())
|
||||
.sum();
|
||||
|
||||
let region = Region::begin();
|
||||
let table = Arc::new(RibTable::new(threads * 16));
|
||||
let indices = add_peers(&table, dump, peers);
|
||||
let started = Instant::now();
|
||||
let best_changes = load(&table, dump, &updates, &indices, threads);
|
||||
let elapsed = started.elapsed();
|
||||
assert_eq!(table.len(), routes);
|
||||
let held = retained(®ion);
|
||||
row(
|
||||
"intern attributes, insert, choose best",
|
||||
elapsed,
|
||||
format!(
|
||||
"{} routes; {:.0} MiB = {:.0} bytes a route, attribute sets included",
|
||||
per_second(routes, elapsed),
|
||||
mib(held),
|
||||
held as f64 / routes as f64
|
||||
),
|
||||
);
|
||||
println!(
|
||||
" {} prefixes, {} attribute sets, best route changed {} times",
|
||||
table.prefix_count(),
|
||||
table.attrs().len(),
|
||||
best_changes
|
||||
);
|
||||
|
||||
let started = Instant::now();
|
||||
let mut best = 0usize;
|
||||
table.for_each_best(|prefix, _, _| {
|
||||
best += usize::from(std::hint::black_box(prefix).length() > 0)
|
||||
});
|
||||
row(
|
||||
"read every best route",
|
||||
started.elapsed(),
|
||||
format!("{best} of them, in place, table not stopped"),
|
||||
);
|
||||
|
||||
// Reading has to look at each route for the timing to mean anything.
|
||||
let started = Instant::now();
|
||||
let mut all = 0usize;
|
||||
let mut hops = 0u64;
|
||||
table.for_each_route(|route| {
|
||||
all += 1;
|
||||
hops += u64::from(route.attrs.key().as_path_len);
|
||||
});
|
||||
row(
|
||||
"read every route and its attributes",
|
||||
started.elapsed(),
|
||||
format!(
|
||||
"{all} of them, mean AS path {:.1}",
|
||||
hops as f64 / all as f64
|
||||
),
|
||||
);
|
||||
|
||||
let started = Instant::now();
|
||||
let outcome = table.remove_peer(indices[0]);
|
||||
let elapsed = started.elapsed();
|
||||
row(
|
||||
"remove one peer (session down)",
|
||||
elapsed,
|
||||
format!(
|
||||
"{} routes; best route changed for {} prefixes",
|
||||
per_second(outcome.removed, elapsed),
|
||||
outcome.best_changed
|
||||
),
|
||||
);
|
||||
let started = Instant::now();
|
||||
let swept = table.attrs().sweep();
|
||||
row(
|
||||
"drop attribute sets no longer used",
|
||||
started.elapsed(),
|
||||
format!("{swept} dropped"),
|
||||
);
|
||||
}
|
||||
|
||||
/// A peer's routes grouped as UPDATEs: the attribute set and the prefixes
|
||||
/// announced with it.
|
||||
type Updates = Vec<Vec<(u32, Vec<IpPrefix>)>>;
|
||||
|
||||
fn grouped(dump: &Dump, peers: &[usize], parsed: &[Option<Attributes>]) -> Updates {
|
||||
peers
|
||||
.iter()
|
||||
.map(|&peer| {
|
||||
let mut routes = dump.routes[peer].clone();
|
||||
routes.sort_by_key(|(_, set)| *set);
|
||||
routes
|
||||
.chunk_by(|a, b| a.1 == b.1)
|
||||
.filter(|group| parsed[group[0].1 as usize].is_some())
|
||||
.map(|group| {
|
||||
(
|
||||
group[0].1,
|
||||
group.iter().map(|(prefix, _)| *prefix).collect(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn add_peers(table: &RibTable, dump: &Dump, peers: &[usize]) -> Vec<PeerIndex> {
|
||||
peers
|
||||
.iter()
|
||||
.map(|&peer| {
|
||||
table
|
||||
.add_peer(PeerInfo {
|
||||
asn: dump.peers[peer].asn,
|
||||
router_id: Ipv4Addr::from(peer as u32 + 1),
|
||||
address: dump.peers[peer].address,
|
||||
ibgp: false,
|
||||
})
|
||||
.expect("room for the peer")
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Announces everything in `updates` on `threads` threads, each taking
|
||||
/// whole peers. Returns how many times a best route changed.
|
||||
fn load(
|
||||
table: &RibTable,
|
||||
dump: &Dump,
|
||||
updates: &Updates,
|
||||
indices: &[PeerIndex],
|
||||
threads: usize,
|
||||
) -> usize {
|
||||
let next = AtomicUsize::new(0);
|
||||
let best_changes = AtomicUsize::new(0);
|
||||
std::thread::scope(|scope| {
|
||||
for _ in 0..threads {
|
||||
scope.spawn(|| {
|
||||
let mut changes = 0;
|
||||
loop {
|
||||
let at = next.fetch_add(1, Ordering::Relaxed);
|
||||
let Some(updates) = updates.get(at) else {
|
||||
break;
|
||||
};
|
||||
for (set, prefixes) in updates {
|
||||
// From the bytes, as a dump has them and as a
|
||||
// session could hand them over without parsing.
|
||||
let Some(attrs) = table
|
||||
.attrs()
|
||||
.intern_wire(&dump.attribute_sets[*set as usize])
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
changes += table.announce(indices[at], &attrs, prefixes).best_changed;
|
||||
}
|
||||
}
|
||||
best_changes.fetch_add(changes, Ordering::Relaxed);
|
||||
});
|
||||
}
|
||||
});
|
||||
best_changes.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// The same load with someone following the best routes through the
|
||||
/// journal: once reading as fast as it can while the table fills, once not
|
||||
/// reading at all until the table is full.
|
||||
fn followed_table(dump: &Dump, peers: &[usize], parsed: &[Option<Attributes>], threads: usize) {
|
||||
println!("\n== The sharded table with a follower, {threads} thread(s)");
|
||||
let updates = grouped(dump, peers, parsed);
|
||||
let routes: usize = updates
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|(_, prefixes)| prefixes.len())
|
||||
.sum();
|
||||
let count = |counts: &mut (usize, usize, usize), event: Event<'_>| match event {
|
||||
Event::Best { attrs, .. } => {
|
||||
// Look at the route, as a follower would.
|
||||
counts.0 += usize::from(attrs.key().as_path_len > 0);
|
||||
}
|
||||
Event::Withdrawn { .. } => counts.1 += 1,
|
||||
Event::Reset | Event::Synced => counts.2 += 1,
|
||||
// Not given to a follower of the best routes.
|
||||
Event::Routes { .. } => {}
|
||||
};
|
||||
|
||||
// Reading while the table fills.
|
||||
let table = Arc::new(RibTable::new(threads * 16));
|
||||
let indices = add_peers(&table, dump, peers);
|
||||
let done = AtomicBool::new(false);
|
||||
let started = Instant::now();
|
||||
let (best_changes, counts, longest, after_load) = std::thread::scope(|scope| {
|
||||
let follower = scope.spawn(|| {
|
||||
let mut reader = table.reader();
|
||||
let mut counts = (0, 0, 0);
|
||||
let mut longest = 0;
|
||||
while !done.load(Ordering::Acquire) {
|
||||
reader.poll(4096, |event| count(&mut counts, event));
|
||||
longest = longest.max(table.journal_len());
|
||||
}
|
||||
let finishing = Instant::now();
|
||||
while !reader
|
||||
.poll(4096, |event| count(&mut counts, event))
|
||||
.caught_up
|
||||
{}
|
||||
(counts, longest, finishing.elapsed())
|
||||
});
|
||||
let best_changes = load(&table, dump, &updates, &indices, threads);
|
||||
done.store(true, Ordering::Release);
|
||||
let (counts, longest, after_load) = follower.join().expect("the follower runs");
|
||||
(best_changes, counts, longest, after_load)
|
||||
});
|
||||
let elapsed = started.elapsed();
|
||||
assert_eq!(table.len(), routes);
|
||||
row(
|
||||
"load, follower reading throughout",
|
||||
elapsed,
|
||||
format!(
|
||||
"{} routes; caught up {:.3} s after the last route",
|
||||
per_second(routes, elapsed),
|
||||
after_load.as_secs_f64()
|
||||
),
|
||||
);
|
||||
println!(
|
||||
" best route changed {best_changes} times; the follower was given {} events for it; \n journal at most {longest} entries for {} prefixes",
|
||||
counts.0 + counts.1,
|
||||
table.prefix_count()
|
||||
);
|
||||
drop(table);
|
||||
|
||||
// Not reading: the journal has to stay small on its own account.
|
||||
let table = Arc::new(RibTable::new(threads * 16));
|
||||
let indices = add_peers(&table, dump, peers);
|
||||
let mut reader = table.reader();
|
||||
let mut counts = (0, 0, 0);
|
||||
reader.poll(usize::MAX, |event| count(&mut counts, event));
|
||||
let started = Instant::now();
|
||||
load(&table, dump, &updates, &indices, threads);
|
||||
let elapsed = started.elapsed();
|
||||
row(
|
||||
"load, follower not reading",
|
||||
elapsed,
|
||||
format!(
|
||||
"{} routes; journal holds {} entries for {} prefixes",
|
||||
per_second(routes, elapsed),
|
||||
table.journal_len(),
|
||||
table.prefix_count()
|
||||
),
|
||||
);
|
||||
let started = Instant::now();
|
||||
let mut counts = (0, 0, 0);
|
||||
while !reader
|
||||
.poll(4096, |event| count(&mut counts, event))
|
||||
.caught_up
|
||||
{}
|
||||
let elapsed = started.elapsed();
|
||||
row(
|
||||
"the follower then catches up",
|
||||
elapsed,
|
||||
format!(
|
||||
"{} events, {} of them a reset",
|
||||
counts.0 + counts.1,
|
||||
counts.2
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# Table dumps downloaded for benchmarks; see README.md.
|
||||
*
|
||||
!.gitignore
|
||||
!README.md
|
||||
@@ -0,0 +1,18 @@
|
||||
# Table dumps for the benchmarks
|
||||
|
||||
`cargo bench -p bgp-bench --bench real_table` runs on a real routing table:
|
||||
a dump of every route that the peers of a route collector had announced to
|
||||
it. The dumps are large and are not checked in. Fetch one into this
|
||||
directory:
|
||||
|
||||
```bash
|
||||
curl -O https://data.ris.ripe.net/rrc03/2026.09/bview.20260915.0000.gz
|
||||
```
|
||||
|
||||
That is RIPE RIS collector `rrc03`, which peers at AMS-IX: about 340 MB,
|
||||
3.4 GB uncompressed. Any `bview` file from <https://data.ris.ripe.net/> or
|
||||
a RouteViews RIB works; the most recent `.gz` here is used, or the one named
|
||||
by `BENCH_MRT`.
|
||||
|
||||
The benchmark needs memory in proportion to the dump. `BENCH_PEERS=8`
|
||||
limits it to the eight peers with the most routes.
|
||||
@@ -0,0 +1,285 @@
|
||||
//! A synthetic copy of the Internet routing table, as the UPDATE messages a
|
||||
//! peer would send to announce it.
|
||||
//!
|
||||
//! It is made up, not captured, so that the benchmarks need no data files
|
||||
//! and give the same input every run. It is shaped like the real thing where
|
||||
//! that matters for cost:
|
||||
//!
|
||||
//! - about a million IPv4 prefixes, most of them /24s, and about a quarter
|
||||
//! of a million IPv6 prefixes, most of them /48s;
|
||||
//! - prefixes that share their attributes are announced together, a handful
|
||||
//! to an UPDATE, as routers do;
|
||||
//! - AS paths a few hops long, and communities on most routes;
|
||||
//! - several peers announce the same prefixes, each with attributes of its
|
||||
//! own.
|
||||
|
||||
pub mod memory;
|
||||
pub mod mrt;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
use bgp_packet::message::{
|
||||
AsPathAttribute, AsPathSegment, CommunitiesPathAttribute, LargeCommunitiesPathAttribute,
|
||||
LargeCommunity, Message, MpReachNlriPathAttribute, MultiExitDiscPathAttribute,
|
||||
NextHopPathAttribute, NlriNextHop, OriginPathAttribute, PathAttribute, UpdateMessage,
|
||||
};
|
||||
use bgp_primitives::constants::{AfiSafi, IpVersion};
|
||||
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
|
||||
use bgp_primitives::parser::ParserContext;
|
||||
use bytes::BytesMut;
|
||||
|
||||
/// How big a table to make.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Shape {
|
||||
pub ipv4_prefixes: usize,
|
||||
pub ipv6_prefixes: usize,
|
||||
/// How many prefixes share one set of attributes, on average.
|
||||
pub prefixes_per_attribute_set: usize,
|
||||
}
|
||||
|
||||
impl Shape {
|
||||
/// Roughly the default-free zone in 2026.
|
||||
pub const INTERNET: Self = Self {
|
||||
ipv4_prefixes: 1_000_000,
|
||||
ipv6_prefixes: 230_000,
|
||||
prefixes_per_attribute_set: 6,
|
||||
};
|
||||
|
||||
pub fn prefixes(&self) -> usize {
|
||||
self.ipv4_prefixes + self.ipv6_prefixes
|
||||
}
|
||||
|
||||
/// The same shape, `factor` times smaller.
|
||||
pub fn scaled_down(&self, factor: usize) -> Self {
|
||||
Self {
|
||||
ipv4_prefixes: self.ipv4_prefixes / factor,
|
||||
ipv6_prefixes: self.ipv6_prefixes / factor,
|
||||
..*self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A small deterministic generator (SplitMix64); no need for a dependency.
|
||||
pub struct Rng(u64);
|
||||
|
||||
impl Rng {
|
||||
pub fn new(seed: u64) -> Self {
|
||||
Self(seed)
|
||||
}
|
||||
|
||||
pub fn next_u64(&mut self) -> u64 {
|
||||
self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
|
||||
let mut z = self.0;
|
||||
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
|
||||
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
|
||||
z ^ (z >> 31)
|
||||
}
|
||||
|
||||
/// A number below `bound`.
|
||||
pub fn below(&mut self, bound: u64) -> u64 {
|
||||
self.next_u64() % bound
|
||||
}
|
||||
|
||||
/// An entry of `weighted`, picked with probability proportional to its
|
||||
/// weight.
|
||||
fn pick<T: Copy>(&mut self, weighted: &[(T, u64)]) -> T {
|
||||
let total: u64 = weighted.iter().map(|(_, w)| w).sum();
|
||||
let mut at = self.below(total);
|
||||
for (value, weight) in weighted {
|
||||
if at < *weight {
|
||||
return *value;
|
||||
}
|
||||
at -= weight;
|
||||
}
|
||||
weighted[weighted.len() - 1].0
|
||||
}
|
||||
}
|
||||
|
||||
/// The prefixes of the table. Every peer announces these same prefixes.
|
||||
pub struct Prefixes {
|
||||
pub ipv4: Vec<IpPrefix>,
|
||||
pub ipv6: Vec<IpPrefix>,
|
||||
}
|
||||
|
||||
impl Prefixes {
|
||||
pub fn generate(shape: Shape) -> Self {
|
||||
let mut rng = Rng::new(0x1234_5678);
|
||||
// Shares of prefix lengths, close to what the table looks like.
|
||||
let v4_lengths: [(u8, u64); 9] = [
|
||||
(24, 60),
|
||||
(23, 10),
|
||||
(22, 11),
|
||||
(21, 5),
|
||||
(20, 4),
|
||||
(19, 3),
|
||||
(18, 2),
|
||||
(16, 4),
|
||||
(15, 1),
|
||||
];
|
||||
let v6_lengths: [(u8, u64); 7] = [
|
||||
(48, 48),
|
||||
(32, 14),
|
||||
(40, 10),
|
||||
(44, 10),
|
||||
(36, 6),
|
||||
(29, 6),
|
||||
(47, 6),
|
||||
];
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
let mut ipv4 = Vec::with_capacity(shape.ipv4_prefixes);
|
||||
while ipv4.len() < shape.ipv4_prefixes {
|
||||
let length = rng.pick(&v4_lengths);
|
||||
// Unicast space, 1.0.0.0 to 223.255.255.255.
|
||||
let address = 0x0100_0000 + (rng.next_u64() as u32) % 0xdf00_0000;
|
||||
let prefix = IpPrefix::new(IpVersion::Ipv4, address.to_be_bytes(), length)
|
||||
.expect("four bytes cover any IPv4 prefix");
|
||||
if seen.insert(prefix) {
|
||||
ipv4.push(prefix);
|
||||
}
|
||||
}
|
||||
let mut ipv6 = Vec::with_capacity(shape.ipv6_prefixes);
|
||||
while ipv6.len() < shape.ipv6_prefixes {
|
||||
let length = rng.pick(&v6_lengths);
|
||||
// Global unicast, 2000::/3.
|
||||
let address = (0x2000_u128 << 112) | (u128::from(rng.next_u64()) << 61 >> 3);
|
||||
let prefix = IpPrefix::new(IpVersion::Ipv6, address.to_be_bytes(), length)
|
||||
.expect("sixteen bytes cover any IPv6 prefix");
|
||||
if seen.insert(prefix) {
|
||||
ipv6.push(prefix);
|
||||
}
|
||||
}
|
||||
Self { ipv4, ipv6 }
|
||||
}
|
||||
}
|
||||
|
||||
/// The UPDATE messages with which the peer numbered `peer` announces the
|
||||
/// whole table. The peer's AS is `64512 + peer`.
|
||||
pub fn announcements(prefixes: &Prefixes, shape: Shape, peer: u32) -> Vec<UpdateMessage> {
|
||||
let mut rng = Rng::new(0x9999 + u64::from(peer));
|
||||
let peer_asn = 64512 + peer;
|
||||
let attributes = |rng: &mut Rng| {
|
||||
let hops = 2 + rng.below(4) as usize;
|
||||
let mut path = vec![peer_asn];
|
||||
path.extend((0..hops).map(|_| 1 + rng.below(400_000) as u32));
|
||||
let mut attributes = vec![
|
||||
PathAttribute::Origin(OriginPathAttribute::IGP),
|
||||
PathAttribute::AsPath(AsPathAttribute {
|
||||
segments: vec![AsPathSegment {
|
||||
ordered: true,
|
||||
path,
|
||||
}],
|
||||
}),
|
||||
];
|
||||
let mut later = Vec::new();
|
||||
if rng.below(4) == 0 {
|
||||
later.push(PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(
|
||||
rng.below(1000) as u32,
|
||||
)));
|
||||
}
|
||||
let communities = rng.below(7) as usize;
|
||||
if communities > 0 {
|
||||
later.push(PathAttribute::Communities(CommunitiesPathAttribute(
|
||||
(0..communities)
|
||||
.map(|_| (rng.below(65000) as u16, rng.below(1000) as u16))
|
||||
.collect(),
|
||||
)));
|
||||
}
|
||||
let large = if rng.below(5) == 0 {
|
||||
Some(PathAttribute::LargeCommunities(
|
||||
LargeCommunitiesPathAttribute {
|
||||
communities: vec![LargeCommunity {
|
||||
global_admin: peer_asn,
|
||||
data_1: rng.below(100) as u32,
|
||||
data_2: rng.below(100) as u32,
|
||||
}],
|
||||
},
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(std::mem::take(&mut attributes), later, large)
|
||||
};
|
||||
|
||||
let mut updates = Vec::new();
|
||||
// Prefixes that share attributes travel together. A set's prefixes are a
|
||||
// run of the (randomly ordered) list, a few long.
|
||||
let mut grouped = |list: &[IpPrefix], rng: &mut Rng, ipv6: bool| {
|
||||
let mut rest = list;
|
||||
while !rest.is_empty() {
|
||||
let size = (1 + rng.below(2 * shape.prefixes_per_attribute_set as u64 - 1) as usize)
|
||||
.min(rest.len());
|
||||
let (group, after) = rest.split_at(size);
|
||||
rest = after;
|
||||
|
||||
let (mut path_attributes, later, large) = attributes(rng);
|
||||
let update = if ipv6 {
|
||||
// In order of type code: MED and COMMUNITIES come before
|
||||
// MP_REACH_NLRI.
|
||||
path_attributes.extend(later);
|
||||
path_attributes.push(PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
|
||||
family: AfiSafi::IPV6_UNICAST,
|
||||
next_hop: NlriNextHop::Ipv6(Ipv6Addr::new(
|
||||
0x2001,
|
||||
0xdb8,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
peer as u16,
|
||||
)),
|
||||
prefixes: Nlri::plain(group.iter().copied()),
|
||||
}));
|
||||
path_attributes.extend(large);
|
||||
UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes,
|
||||
announced_routes: vec![],
|
||||
}
|
||||
} else {
|
||||
path_attributes.push(PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(
|
||||
192, 0, 2, peer as u8,
|
||||
))));
|
||||
path_attributes.extend(later);
|
||||
path_attributes.extend(large);
|
||||
UpdateMessage {
|
||||
withdrawn_routes: vec![],
|
||||
path_attributes,
|
||||
announced_routes: Nlri::plain(group.iter().copied()),
|
||||
}
|
||||
};
|
||||
updates.push(update);
|
||||
}
|
||||
};
|
||||
grouped(&prefixes.ipv4, &mut rng, false);
|
||||
grouped(&prefixes.ipv6, &mut rng, true);
|
||||
updates
|
||||
}
|
||||
|
||||
/// `updates` as they are on the wire, on a session with four-octet AS
|
||||
/// numbers.
|
||||
pub fn encoded(updates: &[UpdateMessage]) -> Vec<BytesMut> {
|
||||
let ctx = wire_context();
|
||||
updates
|
||||
.iter()
|
||||
.map(|update| {
|
||||
let mut wire = BytesMut::new();
|
||||
Message::Update(update.clone())
|
||||
.to_wire(&ctx, &mut wire)
|
||||
.expect("a few prefixes fit one message");
|
||||
wire
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The context of the sessions the benchmarks pretend to have: an external
|
||||
/// peer with four-octet AS numbers.
|
||||
pub fn wire_context() -> ParserContext {
|
||||
ParserContext {
|
||||
four_octet_asn: true,
|
||||
ibgp: false,
|
||||
add_path: Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//! Counting the memory a benchmark holds.
|
||||
//!
|
||||
//! With the `count-memory` feature every allocation is counted, and
|
||||
//! [`Region::held`] says how many bytes were allocated and not freed since
|
||||
//! the region began. Without it nothing is counted and the answer is
|
||||
//! `None`: the counters are shared by all threads, and counting slows down
|
||||
//! exactly the code whose speed is being measured.
|
||||
|
||||
#[cfg(feature = "count-memory")]
|
||||
mod counting {
|
||||
use std::alloc::System;
|
||||
|
||||
use stats_alloc::{INSTRUMENTED_SYSTEM, StatsAlloc};
|
||||
|
||||
#[global_allocator]
|
||||
static GLOBAL: &StatsAlloc<System> = &INSTRUMENTED_SYSTEM;
|
||||
|
||||
pub struct Region(stats_alloc::Region<'static, System>);
|
||||
|
||||
impl Region {
|
||||
pub fn begin() -> Self {
|
||||
Self(stats_alloc::Region::new(GLOBAL))
|
||||
}
|
||||
|
||||
pub fn held(&self) -> Option<i64> {
|
||||
let stats = self.0.change();
|
||||
Some(stats.bytes_allocated as i64 - stats.bytes_deallocated as i64)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "count-memory"))]
|
||||
mod counting {
|
||||
pub struct Region;
|
||||
|
||||
impl Region {
|
||||
pub fn begin() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn held(&self) -> Option<i64> {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use counting::Region;
|
||||
|
||||
/// "`bytes` MiB = n bytes a `unit`", or a note that memory is not being
|
||||
/// counted.
|
||||
pub fn describe(held: Option<i64>, count: usize, unit: &str) -> String {
|
||||
match held {
|
||||
Some(bytes) => format!(
|
||||
"{:.0} MiB = {:.0} bytes a {unit}",
|
||||
bytes as f64 / (1024.0 * 1024.0),
|
||||
bytes as f64 / count as f64
|
||||
),
|
||||
None => "memory not counted (run with --features count-memory)".into(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
//! Reads a table dump in MRT format (RFC 6396, TABLE_DUMP_V2), as published
|
||||
//! by RIPE RIS and RouteViews: the routes that every peer of a route
|
||||
//! collector had announced to it at one moment.
|
||||
//!
|
||||
//! Only what the benchmarks need is read: the IPv4 and IPv6 unicast tables.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::{self, BufReader, Read};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
use std::path::Path;
|
||||
|
||||
use bgp_packet::message::{NlriNextHop, PathAttribute};
|
||||
use bgp_primitives::constants::IpVersion;
|
||||
use bgp_primitives::ip_prefix::IpPrefix;
|
||||
use flate2::read::MultiGzDecoder;
|
||||
|
||||
use crate::wire_context;
|
||||
|
||||
const TABLE_DUMP_V2: u16 = 13;
|
||||
const PEER_INDEX_TABLE: u16 = 1;
|
||||
const RIB_IPV4_UNICAST: u16 = 2;
|
||||
const RIB_IPV6_UNICAST: u16 = 4;
|
||||
const MP_REACH_NLRI: u8 = 14;
|
||||
|
||||
/// A peer of the collector.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DumpPeer {
|
||||
pub address: IpAddr,
|
||||
pub asn: u32,
|
||||
}
|
||||
|
||||
/// The contents of a table dump.
|
||||
pub struct Dump {
|
||||
/// When the dump was taken, in seconds since the Unix epoch.
|
||||
pub timestamp: u32,
|
||||
pub peers: Vec<DumpPeer>,
|
||||
/// Every distinct list of path attributes in the dump, as it is on the
|
||||
/// wire. Routes refer to these by index.
|
||||
pub attribute_sets: Vec<Box<[u8]>>,
|
||||
/// For each peer (by index into `peers`), its routes: a prefix and the
|
||||
/// index of its attributes.
|
||||
pub routes: Vec<Vec<(IpPrefix, u32)>>,
|
||||
}
|
||||
|
||||
impl Dump {
|
||||
/// Reads the dump at `path`, gzip-compressed as it is published.
|
||||
pub fn read(path: &Path) -> io::Result<Self> {
|
||||
let mut input = BufReader::with_capacity(
|
||||
1 << 20,
|
||||
MultiGzDecoder::new(BufReader::new(File::open(path)?)),
|
||||
);
|
||||
let mut dump = Dump {
|
||||
timestamp: 0,
|
||||
peers: Vec::new(),
|
||||
attribute_sets: Vec::new(),
|
||||
routes: Vec::new(),
|
||||
};
|
||||
let mut interned: HashMap<Box<[u8]>, u32> = HashMap::new();
|
||||
let mut body = Vec::new();
|
||||
loop {
|
||||
let mut header = [0u8; 12];
|
||||
match input.read_exact(&mut header) {
|
||||
Ok(()) => {}
|
||||
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break,
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
let timestamp = u32::from_be_bytes([header[0], header[1], header[2], header[3]]);
|
||||
let kind = u16::from_be_bytes([header[4], header[5]]);
|
||||
let subtype = u16::from_be_bytes([header[6], header[7]]);
|
||||
let length = u32::from_be_bytes([header[8], header[9], header[10], header[11]]);
|
||||
body.resize(length as usize, 0);
|
||||
input.read_exact(&mut body)?;
|
||||
if kind != TABLE_DUMP_V2 {
|
||||
continue;
|
||||
}
|
||||
let mut record = Cursor(&body);
|
||||
let malformed = || io::Error::new(io::ErrorKind::InvalidData, "truncated MRT record");
|
||||
match subtype {
|
||||
PEER_INDEX_TABLE => {
|
||||
dump.timestamp = timestamp;
|
||||
dump.peers = peer_index_table(&mut record).ok_or_else(malformed)?;
|
||||
dump.routes = vec![Vec::new(); dump.peers.len()];
|
||||
}
|
||||
RIB_IPV4_UNICAST | RIB_IPV6_UNICAST => {
|
||||
let version = if subtype == RIB_IPV4_UNICAST {
|
||||
IpVersion::Ipv4
|
||||
} else {
|
||||
IpVersion::Ipv6
|
||||
};
|
||||
rib_entries(&mut record, version, &mut dump, &mut interned)
|
||||
.ok_or_else(malformed)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(dump)
|
||||
}
|
||||
|
||||
pub fn route_count(&self) -> usize {
|
||||
self.routes.iter().map(Vec::len).sum()
|
||||
}
|
||||
|
||||
/// The peers with the most routes, most first: the ones that send a
|
||||
/// full table.
|
||||
pub fn largest_peers(&self, count: usize) -> Vec<usize> {
|
||||
let mut peers: Vec<usize> = (0..self.peers.len()).collect();
|
||||
peers.sort_by_key(|&peer| std::cmp::Reverse(self.routes[peer].len()));
|
||||
peers.truncate(count);
|
||||
peers
|
||||
}
|
||||
}
|
||||
|
||||
struct Cursor<'a>(&'a [u8]);
|
||||
|
||||
impl<'a> Cursor<'a> {
|
||||
fn take(&mut self, n: usize) -> Option<&'a [u8]> {
|
||||
if self.0.len() < n {
|
||||
return None;
|
||||
}
|
||||
let (taken, rest) = self.0.split_at(n);
|
||||
self.0 = rest;
|
||||
Some(taken)
|
||||
}
|
||||
fn u8(&mut self) -> Option<u8> {
|
||||
self.take(1).map(|b| b[0])
|
||||
}
|
||||
fn u16(&mut self) -> Option<u16> {
|
||||
self.take(2).map(|b| u16::from_be_bytes([b[0], b[1]]))
|
||||
}
|
||||
fn u32(&mut self) -> Option<u32> {
|
||||
self.take(4)
|
||||
.map(|b| u32::from_be_bytes([b[0], b[1], b[2], b[3]]))
|
||||
}
|
||||
}
|
||||
|
||||
fn peer_index_table(record: &mut Cursor<'_>) -> Option<Vec<DumpPeer>> {
|
||||
record.u32()?; // collector's BGP identifier
|
||||
let name = record.u16()?;
|
||||
record.take(name.into())?;
|
||||
let count = record.u16()?;
|
||||
let mut peers = Vec::with_capacity(count.into());
|
||||
for _ in 0..count {
|
||||
let kind = record.u8()?;
|
||||
record.u32()?; // peer's BGP identifier
|
||||
let address = if kind & 1 == 1 {
|
||||
let bytes: [u8; 16] = record.take(16)?.try_into().ok()?;
|
||||
IpAddr::from(Ipv6Addr::from(bytes))
|
||||
} else {
|
||||
let bytes: [u8; 4] = record.take(4)?.try_into().ok()?;
|
||||
IpAddr::from(Ipv4Addr::from(bytes))
|
||||
};
|
||||
let asn = if kind & 2 == 2 {
|
||||
record.u32()?
|
||||
} else {
|
||||
record.u16()?.into()
|
||||
};
|
||||
peers.push(DumpPeer { address, asn });
|
||||
}
|
||||
Some(peers)
|
||||
}
|
||||
|
||||
fn rib_entries(
|
||||
record: &mut Cursor<'_>,
|
||||
version: IpVersion,
|
||||
dump: &mut Dump,
|
||||
interned: &mut HashMap<Box<[u8]>, u32>,
|
||||
) -> Option<()> {
|
||||
record.u32()?; // sequence number
|
||||
let length = record.u8()?;
|
||||
let bytes = record.take(usize::from(length.div_ceil(8)))?;
|
||||
// A prefix the collector should not have had is not ours to fix.
|
||||
let prefix = IpPrefix::new(version, bytes, length).ok();
|
||||
let count = record.u16()?;
|
||||
for _ in 0..count {
|
||||
let peer = usize::from(record.u16()?);
|
||||
record.u32()?; // when the route was received
|
||||
let attributes_len = record.u16()?;
|
||||
let attributes = record.take(attributes_len.into())?;
|
||||
let (Some(prefix), Some(routes)) = (prefix, dump.routes.get_mut(peer)) else {
|
||||
continue;
|
||||
};
|
||||
let id = match interned.get(attributes) {
|
||||
Some(id) => *id,
|
||||
None => {
|
||||
let id = u32::try_from(dump.attribute_sets.len()).ok()?;
|
||||
dump.attribute_sets.push(attributes.into());
|
||||
interned.insert(attributes.into(), id);
|
||||
id
|
||||
}
|
||||
};
|
||||
routes.push((prefix, id));
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// The path attributes of a route in a dump, parsed.
|
||||
///
|
||||
/// In a dump the AS numbers are always four octets wide, and
|
||||
/// MP_REACH_NLRI holds only the next hop (RFC 6396 §4.3.4): the prefix is in
|
||||
/// the record. The next hop is returned beside the other attributes, as the
|
||||
/// RIB keeps it. `None` if the attributes cannot be parsed.
|
||||
pub fn parse_attributes(mut wire: &[u8]) -> Option<(Vec<PathAttribute>, Option<NlriNextHop>)> {
|
||||
let ctx = wire_context();
|
||||
let mut attributes = Vec::new();
|
||||
let mut next_hop = None;
|
||||
while !wire.is_empty() {
|
||||
if wire.get(1) == Some(&MP_REACH_NLRI) {
|
||||
let extended = wire[0] & 0x10 != 0;
|
||||
let (length, header) = if extended {
|
||||
(
|
||||
usize::from(u16::from_be_bytes([*wire.get(2)?, *wire.get(3)?])),
|
||||
4,
|
||||
)
|
||||
} else {
|
||||
(usize::from(*wire.get(2)?), 3)
|
||||
};
|
||||
let value = wire.get(header..header + length)?;
|
||||
let address = |bytes: &[u8]| Some(Ipv6Addr::from(<[u8; 16]>::try_from(bytes).ok()?));
|
||||
next_hop = match value {
|
||||
[4, a, b, c, d] => Some(NlriNextHop::Ipv4(Ipv4Addr::new(*a, *b, *c, *d))),
|
||||
[16, rest @ ..] => Some(NlriNextHop::Ipv6(address(rest)?)),
|
||||
[32, rest @ ..] if rest.len() == 32 => Some(NlriNextHop::Ipv6WithLl {
|
||||
global: address(&rest[..16])?,
|
||||
link_local: address(&rest[16..])?,
|
||||
}),
|
||||
_ => return None,
|
||||
};
|
||||
wire = &wire[header + length..];
|
||||
continue;
|
||||
}
|
||||
let (rest, attribute) = PathAttribute::from_wire(&ctx, wire).ok()?;
|
||||
attributes.push(attribute);
|
||||
wire = rest;
|
||||
}
|
||||
Some((attributes, next_hop))
|
||||
}
|
||||
Reference in new issue
Block a user