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:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-10-02 12:35:54 +02:00
1 parent 1ec60255ea
commit 630c175918
138 files changed
+21650 -2925

No files matched your search

+42
View File
@@ -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
+215
View File
@@ -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")
),
);
}
+431
View File
@@ -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(&region);
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(&region);
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
),
);
}
+4
View File
@@ -0,0 +1,4 @@
# Table dumps downloaded for benchmarks; see README.md.
*
!.gitignore
!README.md
+18
View File
@@ -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.
+285
View File
@@ -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(),
}
}
+60
View File
@@ -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(),
}
}
+237
View File
@@ -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))
}
+12
View File
@@ -1,6 +1,11 @@
[package]
name = "bgp-integration-tests"
authors.workspace = true
edition.workspace = true
homepage.workspace = true
license.workspace = true
repository.workspace = true
version.workspace = true
publish = false
[dependencies]
@@ -8,6 +13,13 @@ bgp-packet.workspace = true
bgp-primitives.workspace = true
bgp-rib.workspace = true
bgp-server.workspace = true
bgp-web.workspace = true
bytes.workspace = true
clap.workspace = true
eyre.workspace = true
reqwest.workspace = true
serde_json.workspace = true
tokio.workspace = true
[lints]
workspace = true
@@ -0,0 +1,184 @@
//! A minimal stand-in for a neighbouring router: connects to the daemon,
//! completes the handshake, optionally announces some prefixes, and prints
//! every UPDATE it receives. Useful for trying things out when no real
//! router is around.
//!
//! cargo run -p bgp-integration-tests --example fake_peer -- \
//! --server 127.0.0.1:1790 --asn 65001 --announce 203.0.113.0/24
//!
//! With `--echo`, every prefix received is re-announced back to the server
//! with this peer's ASN prepended, which lets a single daemon show its own
//! routes "coming back" in the received table.
//!
//! With `--listen ADDR` it instead waits for the server to dial it (one
//! connection at a time), which exercises the server's active side.
use std::net::Ipv4Addr;
use std::time::Duration;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_packet::message::{
AsPathAttribute, CommunitiesPathAttribute, Message, NextHopPathAttribute, OriginPathAttribute,
PathAttribute, UpdateMessage,
};
use bgp_primitives::ip_prefix::IpPrefix;
use clap::Parser;
use eyre::Result;
use tokio::net::TcpListener;
#[derive(Parser, Debug)]
struct Args {
/// Address of the daemon's BGP listener to connect to.
#[arg(long, default_value = "127.0.0.1:1790", conflicts_with = "listen")]
server: String,
/// Instead of connecting, listen here and accept connections from the
/// server, one at a time, forever.
#[arg(long)]
listen: Option<String>,
/// ASN this peer speaks as.
#[arg(long, default_value_t = 65001)]
asn: u32,
/// Router ID to send in OPEN.
#[arg(long, default_value = "10.0.0.2")]
router_id: Ipv4Addr,
/// Hold time to negotiate.
#[arg(long, default_value_t = 30)]
hold_time: u16,
/// Prefixes to announce after the session is established.
#[arg(long)]
announce: Vec<String>,
/// Re-announce every received prefix back to the server, with our ASN
/// prepended and NO_EXPORT-tagged routes dropped.
#[arg(long)]
echo: bool,
}
const NO_EXPORT: (u16, u16) = (0xFFFF, 0xFF01);
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
match &args.listen {
Some(listen) => {
let listener = TcpListener::bind(listen).await?;
println!("listening on {}", listener.local_addr()?);
loop {
let (stream, from) = listener.accept().await?;
println!("accepted connection from {from}");
if let Err(e) = run(&args, TestPeer::from_stream(stream), from).await {
println!("session ended: {e}");
}
}
}
None => {
let addr = args.server.parse()?;
run(&args, TestPeer::connect(addr).await?, addr).await
}
}
}
async fn run(args: &Args, mut peer: TestPeer, addr: std::net::SocketAddr) -> Result<()> {
let open = peer
.handshake(args.asn, args.router_id, args.hold_time)
.await?;
println!(
"established with {} (AS{} router-id {})",
addr, open.asn, open.identifier
);
for p in &args.announce {
let prefix = IpPrefix::try_from(p.as_str())?;
let update = announce(prefix, vec![args.asn], vec![(args.asn as u16, 1)]);
peer.send(&Message::Update(update)).await?;
println!("announced {prefix}");
}
let keepalive = Duration::from_secs((args.hold_time / 3).max(1) as u64);
let mut ticker = tokio::time::interval(keepalive);
loop {
tokio::select! {
_ = ticker.tick() => peer.send(&Message::KeepAlive).await?,
msg = peer.recv() => match msg? {
Message::KeepAlive => {}
Message::Update(u) => {
for w in &u.withdrawn_routes {
println!("WITHDRAW {w}");
}
for a in &u.announced_routes {
println!("UPDATE {a} attrs={:?}", u.path_attributes);
}
if args.echo {
echo_back(&mut peer, &u, args.asn).await?;
}
}
Message::Notification(n) => {
println!("NOTIFICATION {}; session over", n.error);
return Ok(());
}
other => println!("unexpected {other:?}"),
},
}
}
}
fn announce(prefix: IpPrefix, as_path: Vec<u32>, communities: Vec<(u16, u16)>) -> UpdateMessage {
let mut path_attributes = vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(as_path),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(10, 0, 0, 2))),
];
if !communities.is_empty() {
path_attributes.push(PathAttribute::Communities(CommunitiesPathAttribute(
communities,
)));
}
UpdateMessage {
withdrawn_routes: vec![],
path_attributes,
announced_routes: vec![prefix.into()],
}
}
async fn echo_back(peer: &mut TestPeer, u: &UpdateMessage, asn: u32) -> Result<()> {
let mut as_path = vec![asn];
let mut communities = vec![];
for attr in &u.path_attributes {
match attr {
PathAttribute::AsPath(p) => {
as_path.extend(p.segments.iter().flat_map(|s| s.path.iter().copied()))
}
PathAttribute::Communities(c) => communities = c.0.clone(),
_ => {}
}
}
if communities.contains(&NO_EXPORT) {
println!(" not echoing: NO_EXPORT");
return Ok(());
}
for nlri in &u.announced_routes {
let prefix = nlri.prefix;
peer.send(&Message::Update(announce(
prefix,
as_path.clone(),
communities.clone(),
)))
.await?;
println!(" echoed {prefix} back with path {as_path:?}");
}
if !u.withdrawn_routes.is_empty() {
peer.send(&Message::Update(UpdateMessage {
withdrawn_routes: u.withdrawn_routes.clone(),
path_attributes: vec![],
announced_routes: vec![],
}))
.await?;
println!(
" echoed withdraw of {:?}",
u.withdrawn_routes
.iter()
.map(|p| p.to_string())
.collect::<Vec<_>>()
);
}
Ok(())
}
+105 -19
View File
@@ -1,8 +1,8 @@
use std::net::{Ipv4Addr, SocketAddr};
use bgp_packet::message::{BGP_HEADER_LEN, Message};
use bgp_packet::message::{BGP_HEADER_LEN, Message, UpdateMessage};
use bgp_packet::open::{Capability, OpenMessage, OpenOption};
use bgp_primitives::constants::{AddressFamilyId, SubsequentAfi};
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
use bgp_primitives::parser::ParserContext;
use bytes::BytesMut;
use eyre::{Result, bail, eyre};
@@ -14,18 +14,41 @@ use tokio::net::TcpStream;
pub struct TestPeer {
stream: TcpStream,
pub ctx: ParserContext,
/// The address families for which this peer sends path identifiers
/// with its prefixes (ADD-PATH, RFC 7911). What it expects to be sent
/// is `ctx.add_path`. A test sets both to match the capabilities it
/// exchanged.
pub send_add_path: Vec<AfiSafi>,
/// Whether the OPEN we sent offered four-octet AS numbers. They are in
/// use only if the server's did too.
offered_four_octet_asn: bool,
}
impl TestPeer {
pub async fn connect(addr: SocketAddr) -> Result<Self> {
let stream = TcpStream::connect(addr).await?;
Ok(Self { stream, ctx: ParserContext::default() })
Ok(Self::from_stream(stream))
}
/// Wraps an already-connected stream, e.g. one accepted from a server
/// that dialled us.
pub fn from_stream(stream: TcpStream) -> Self {
Self {
stream,
ctx: ParserContext::default(),
send_add_path: Vec::new(),
offered_four_octet_asn: true,
}
}
/// Send a BGP message.
pub async fn send(&mut self, msg: &Message) -> Result<()> {
let send_ctx = ParserContext {
add_path: self.send_add_path.clone(),
..self.ctx.clone()
};
let mut buf = BytesMut::new();
msg.to_wire(&self.ctx, &mut buf)?;
msg.to_wire(&send_ctx, &mut buf)?;
self.stream.write_all(&buf).await?;
Ok(())
}
@@ -42,11 +65,23 @@ impl TestPeer {
let mut buf = vec![0u8; total_len];
buf[..BGP_HEADER_LEN as usize].copy_from_slice(&header);
self.stream.read_exact(&mut buf[BGP_HEADER_LEN as usize..]).await?;
self.stream
.read_exact(&mut buf[BGP_HEADER_LEN as usize..])
.await?;
let (_, msg) = Message::from_wire(&self.ctx, &buf)
.map_err(|e| eyre!("Failed to parse BGP message: {e:?}"))?;
Ok(msg)
Message::decode(&self.ctx, &buf).map_err(|e| eyre!("Failed to parse BGP message: {e}"))
}
/// Read messages until an UPDATE arrives, skipping KEEPALIVEs. Any other
/// message type is an error.
pub async fn recv_update(&mut self) -> Result<UpdateMessage> {
loop {
match self.recv().await? {
Message::Update(u) => return Ok(u),
Message::KeepAlive => {}
other => bail!("Expected UPDATE, got {:?}", other),
}
}
}
/// Receive and unwrap as `OpenMessage`, failing if a different type arrives.
@@ -58,34 +93,67 @@ impl TestPeer {
matches!(opt, OpenOption::Capabilities(caps)
if caps.iter().any(|c| matches!(c, Capability::FourByteAsn { .. })))
});
self.ctx.four_octet_asn = Some(four_byte);
self.ctx.four_octet_asn = four_byte && self.offered_four_octet_asn;
Ok(o)
}
other => bail!("Expected OPEN, got {:?}", other),
}
}
/// Send an OPEN message with the given parameters and standard capabilities.
/// Send an OPEN message with the given parameters and the capabilities
/// of a typical dual-stack speaker: four-octet AS numbers, IPv4 and IPv6
/// unicast, route refresh.
pub async fn send_open(
&mut self,
local_asn: u32,
router_id: Ipv4Addr,
hold_time: u16,
) -> Result<()> {
let asn_field = if local_asn > u16::MAX as u32 { 23456 } else { local_asn as u16 };
let capabilities = vec![
Capability::FourByteAsn { asn: local_asn },
Capability::MultiProtocol {
afi: Afi::IPV4,
safi: Safi::UNICAST,
},
Capability::MultiProtocol {
afi: Afi::IPV6,
safi: Safi::UNICAST,
},
Capability::RouteRefresh,
];
self.send_open_with(local_asn, router_id, hold_time, capabilities)
.await
}
/// Send an OPEN message carrying exactly `capabilities`.
pub async fn send_open_with(
&mut self,
local_asn: u32,
router_id: Ipv4Addr,
hold_time: u16,
capabilities: Vec<Capability>,
) -> Result<()> {
self.offered_four_octet_asn = capabilities
.iter()
.any(|c| matches!(c, Capability::FourByteAsn { .. }));
// If the server's OPEN has been read already, what was settled
// then still depends on what is offered now.
self.ctx.four_octet_asn &= self.offered_four_octet_asn;
let asn_field = if local_asn > u16::MAX as u32 {
23456
} else {
local_asn as u16
};
let open = Message::Open(OpenMessage {
version: 4,
asn: asn_field,
hold_time,
identifier: router_id,
options: vec![OpenOption::Capabilities(vec![
Capability::FourByteAsn { asn: local_asn },
Capability::MultiProtocol {
afi: AddressFamilyId::Ipv4,
safi: SubsequentAfi::Unicast,
},
Capability::RouteRefresh,
])],
options: if capabilities.is_empty() {
vec![]
} else {
vec![OpenOption::Capabilities(capabilities)]
},
});
self.send(&open).await
}
@@ -99,6 +167,24 @@ impl TestPeer {
hold_time: u16,
) -> Result<OpenMessage> {
self.send_open(local_asn, router_id, hold_time).await?;
self.finish_handshake().await
}
/// As [`TestPeer::handshake`], with an OPEN carrying exactly
/// `capabilities`.
pub async fn handshake_with(
&mut self,
local_asn: u32,
router_id: Ipv4Addr,
hold_time: u16,
capabilities: Vec<Capability>,
) -> Result<OpenMessage> {
self.send_open_with(local_asn, router_id, hold_time, capabilities)
.await?;
self.finish_handshake().await
}
async fn finish_handshake(&mut self) -> Result<OpenMessage> {
let server_open = self.recv_open().await?;
// Expect server KEEPALIVE
match self.recv().await? {
+173 -12
View File
@@ -1,19 +1,32 @@
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use bgp_rib::AdjRibIn;
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::server::BgpServerBuilder;
use bgp_server::error::ServerError;
use bgp_server::server::BgpServer;
use bgp_server::session::SessionRegistry;
use bgp_web::{AppState, RouterInfo};
use eyre::Result;
use tokio::net::TcpListener;
use tokio::task::JoinHandle;
/// A BGP server started on an ephemeral port for testing.
/// Aborts the server task when dropped.
pub struct TestServer {
handle: JoinHandle<Result<()>>,
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.
pub registry: SessionRegistry,
pub local_asn: u32,
}
impl TestServer {
@@ -24,28 +37,176 @@ impl TestServer {
/// Start a server with an explicit hold time (useful for timer tests).
pub async fn start_with_hold_time(local_asn: u32, hold_time_secs: u16) -> Result<Self> {
Self::start_inner(local_asn, hold_time_secs).await
Self::start_with(local_asn, |c| c.hold_time_secs = hold_time_secs).await
}
async fn start_inner(local_asn: u32, hold_time_secs: u16) -> Result<Self> {
let config = Config {
/// Start a server after letting `customize` edit the default test config
/// (e.g. to add configured peers).
pub async fn start_with(local_asn: u32, customize: impl FnOnce(&mut Config)) -> Result<Self> {
let mut config = Config {
router_id: Ipv4Addr::new(127, 0, 0, 1),
local_asn,
hold_time_secs,
hold_time_secs: 90,
listen_endpoints: vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)],
peers: vec![],
connect_retry: std::time::Duration::from_secs(5),
collision_detect_established_state: false,
// Most tests play a peer that connects from wherever the
// system puts it; the ones about who may connect turn this off.
accept_unconfigured_peers: true,
originate: vec![],
http_endpoint: None,
};
customize(&mut config);
let bound = BgpServerBuilder::new(config).bind().await?;
let bound = BgpServer::bind(config).await?;
let addr = bound
.local_addrs()
.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();
let handle = tokio::spawn(bound.run());
Ok(Self { handle, addr, adj_rib_in })
Ok(Self {
handle,
addr,
rib,
local_rib,
registry,
local_asn,
})
}
}
/// 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 peer's identifier for the route; zero without ADD-PATH.
pub path_id: u32,
/// 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 }),
// Not given to a follower of the best routes.
Event::Routes { .. } => {}
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(|route| {
let (attributes, next_hop) = route.attrs.attributes().expect("stored attributes parse");
routes.push(ReceivedRoute {
prefix: route.prefix,
peer: route.peer,
path_id: route.path_id,
attributes,
next_hop,
best: route.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> {
(&mut self.handle)
.await
.expect("the server task does not panic")
}
/// Starts the HTTP API on an ephemeral loopback port and returns its
/// base URL (e.g. `http://127.0.0.1:12345`). The API task is aborted
/// when the returned guard drops.
pub async fn serve_http(&self) -> Result<(String, HttpGuard)> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let base_url = format!("http://{}", listener.local_addr()?);
let state = AppState {
info: std::sync::Arc::new(
RouterInfo::new(Ipv4Addr::new(127, 0, 0, 1), self.local_asn, "test")
.with_config("test.textpb", format!("local_asn: {}\n", self.local_asn)),
),
registry: self.registry.clone(),
local_rib: self.local_rib.clone(),
rib: Arc::clone(&self.rib),
};
let handle = tokio::spawn(bgp_web::serve_on(state, listener));
Ok((base_url, HttpGuard { handle }))
}
}
/// Aborts the HTTP API task when dropped.
pub struct HttpGuard {
handle: JoinHandle<std::io::Result<()>>,
}
impl Drop for HttpGuard {
fn drop(&mut self) {
self.handle.abort();
}
}
+288
View File
@@ -0,0 +1,288 @@
//! End-to-end tests of ADD-PATH (RFC 7911): a peer sending several routes
//! for one prefix, told apart by path identifiers.
use std::net::{Ipv4Addr, SocketAddr};
use std::time::Duration;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::TestServer;
use bgp_packet::message::{
AsPathAttribute, Message, NextHopPathAttribute, OriginPathAttribute, PathAttribute,
UpdateMessage,
};
use bgp_packet::open::{AddPathEntry, AddPathSendRecv, Capability, OpenMessage};
use bgp_primitives::constants::AfiSafi;
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
use bgp_rib::{LocalRoute, LocalRouteAttrs};
use bgp_server::config::PeerConfig;
use bgp_server::family::{FamilySet, SUPPORTED};
use eyre::Result;
use serde_json::Value;
const LOCAL_ASN: u32 = 65000;
const PEER_ASN: u32 = 65001;
const TIMEOUT: Duration = Duration::from_secs(5);
fn prefix(s: &str) -> IpPrefix {
IpPrefix::try_from(s).unwrap()
}
/// A server on which connections from this machine are those of a
/// configured peer, with ADD-PATH offered to it as `add_path` says.
async fn server_offering(
add_path: Option<AddPathSendRecv>,
originate: &[&str],
) -> Result<TestServer> {
let originate: Vec<LocalRoute> = originate
.iter()
.map(|p| LocalRoute::new(prefix(p), LocalRouteAttrs::default()))
.collect();
TestServer::start_with(LOCAL_ASN, |c| {
c.peers = vec![PeerConfig {
name: "route-server-client".into(),
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1),
remote_asn: PEER_ASN,
hold_time_secs: 90,
local_address: None,
families: FamilySet::supported(),
add_path,
}];
c.originate = originate;
// Keep the dialer out of the way.
c.connect_retry = Duration::from_secs(3600);
})
.await
}
/// A session from a peer whose OPEN offers ADD-PATH as `offer` says, for
/// IPv4 unicast. Returns the peer and the server's OPEN.
async fn peer_offering(
server: &TestServer,
offer: Option<AddPathSendRecv>,
) -> Result<(TestPeer, OpenMessage)> {
let mut capabilities = vec![Capability::FourByteAsn { asn: PEER_ASN }];
capabilities.extend(SUPPORTED.into_iter().map(Capability::multiprotocol));
if let Some(send_recv) = offer {
capabilities.push(Capability::AddPath {
entries: vec![AddPathEntry {
afi: AfiSafi::IPV4_UNICAST.afi,
safi: AfiSafi::IPV4_UNICAST.safi,
send_recv,
}],
});
}
let mut peer = TestPeer::connect(server.addr).await?;
let open = peer
.handshake_with(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 90, capabilities)
.await?;
tokio::time::timeout(TIMEOUT, async {
while server.registry.list().is_empty() {
tokio::time::sleep(Duration::from_millis(5)).await;
}
})
.await?;
Ok((peer, open))
}
fn add_path_offered(open: &OpenMessage) -> Vec<(AfiSafi, AddPathSendRecv)> {
open.capabilities()
.filter_map(|capability| match capability {
Capability::AddPath { entries } => Some(entries),
_ => None,
})
.flatten()
.map(|entry| (entry.family(), entry.send_recv))
.collect()
}
fn announcement(nlri: Nlri, 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: vec![nlri],
})
}
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 routes the server holds, as (path identifier, AS path, best).
fn held(server: &TestServer) -> Vec<(u32, Vec<u32>, bool)> {
let mut held: Vec<(u32, Vec<u32>, bool)> = server
.received_routes()
.into_iter()
.map(|route| {
let path = route
.attributes
.iter()
.find_map(|attribute| match attribute {
PathAttribute::AsPath(path) => Some(path.segments[0].path.clone()),
_ => None,
})
.unwrap_or_default();
(route.path_id, path, route.best)
})
.collect();
held.sort();
held
}
/// A peer that sends several routes for one prefix: each is kept, the best
/// of them is chosen, and each is withdrawn on its own.
#[tokio::test]
async fn test_several_routes_for_a_prefix_from_one_peer() -> Result<()> {
let server = server_offering(Some(AddPathSendRecv::Receive), &[]).await?;
let (base, _http) = server.serve_http().await?;
let (mut peer, open) = peer_offering(&server, Some(AddPathSendRecv::Send)).await?;
// The server offered to receive, for every family it offers.
assert_eq!(
add_path_offered(&open),
SUPPORTED.map(|family| (family, AddPathSendRecv::Receive))
);
// In use for the one family the peer sends for, and in that direction.
let session = &server.registry.list()[0];
assert_eq!(
session.add_path_receive,
[AfiSafi::IPV4_UNICAST].into_iter().collect()
);
assert!(session.add_path_send.is_empty());
peer.send_add_path = vec![AfiSafi::IPV4_UNICAST];
let p = prefix("203.0.113.0/24");
let path = |path_id| Nlri { prefix: p, path_id };
peer.send(&announcement(path(1), &[PEER_ASN, 64999, 64998]))
.await?;
peer.send(&announcement(path(2), &[PEER_ASN, 64998]))
.await?;
wait_for_routes(&server, 2).await?;
assert_eq!(
held(&server),
[
(1, vec![PEER_ASN, 64999, 64998], false),
(2, vec![PEER_ASN, 64998], true),
]
);
assert_eq!(server.rib.prefix_count(), 1);
// The API tells them apart too.
let routes: Value = reqwest::get(format!("{base}/api/routes/received"))
.await?
.json()
.await?;
let mut ids: Vec<u64> = routes
.as_array()
.unwrap()
.iter()
.map(|route| route["path_id"].as_u64().unwrap())
.collect();
ids.sort_unstable();
assert_eq!(ids, [1, 2]);
let peers: Value = reqwest::get(format!("{base}/api/peers"))
.await?
.json()
.await?;
assert_eq!(
peers[0]["add_path_receive"],
serde_json::json!(["IPv4 unicast"])
);
// Announcing a path again replaces that path only.
peer.send(&announcement(path(1), &[PEER_ASN])).await?;
tokio::time::timeout(TIMEOUT, async {
while held(&server)[0].1.len() != 1 {
tokio::time::sleep(Duration::from_millis(5)).await;
}
})
.await?;
assert_eq!(
held(&server),
[(1, vec![PEER_ASN], true), (2, vec![PEER_ASN, 64998], false),]
);
// And withdrawing one leaves the other.
peer.send(&Message::Update(UpdateMessage {
withdrawn_routes: vec![path(1)],
path_attributes: vec![],
announced_routes: vec![],
}))
.await?;
wait_for_routes(&server, 1).await?;
assert_eq!(held(&server), [(2, vec![PEER_ASN, 64998], true)]);
// When the session ends, all of its routes go.
peer.send(&announcement(path(3), &[PEER_ASN, 1, 2, 3]))
.await?;
wait_for_routes(&server, 2).await?;
drop(peer);
wait_for_routes(&server, 0).await?;
Ok(())
}
/// ADD-PATH changes how prefixes are written. It must be in use only when
/// both sides said so; a peer that offers it to a server that does not is
/// spoken to, and listened to, in plain BGP.
#[tokio::test]
async fn test_add_path_is_not_used_unless_both_sides_offer_it() -> Result<()> {
let server = server_offering(None, &["10.100.0.0/16"]).await?;
let (mut peer, open) = peer_offering(&server, Some(AddPathSendRecv::Both)).await?;
assert!(add_path_offered(&open).is_empty());
let session = &server.registry.list()[0];
assert!(session.add_path_receive.is_empty() && session.add_path_send.is_empty());
// Plain prefixes both ways.
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
peer.send(&announcement(prefix("203.0.113.0/24").into(), &[PEER_ASN]))
.await?;
wait_for_routes(&server, 1).await?;
assert_eq!(held(&server), [(0, vec![PEER_ASN], true)]);
// And the other way round: offered by the server, not by the peer.
let server = server_offering(Some(AddPathSendRecv::Both), &["10.100.0.0/16"]).await?;
let (mut peer, open) = peer_offering(&server, None).await?;
assert_eq!(add_path_offered(&open).len(), SUPPORTED.len());
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
Ok(())
}
/// Where the peer offered to receive and the server to send, the server's
/// prefixes carry a path identifier, and a peer that reads them so
/// understands them.
#[tokio::test]
async fn test_prefixes_are_sent_with_path_identifiers_when_agreed() -> Result<()> {
let server = server_offering(Some(AddPathSendRecv::Both), &["10.100.0.0/16"]).await?;
let (mut peer, _) = peer_offering(&server, Some(AddPathSendRecv::Receive)).await?;
let session = &server.registry.list()[0];
assert_eq!(
session.add_path_send,
[AfiSafi::IPV4_UNICAST].into_iter().collect()
);
assert!(session.add_path_receive.is_empty());
// Read as agreed: with identifiers.
peer.ctx.add_path = vec![AfiSafi::IPV4_UNICAST];
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
assert_eq!(update.announced_routes.len(), 1);
assert_eq!(update.announced_routes[0].prefix, prefix("10.100.0.0/16"));
// The peer did not offer to send, so the server reads its prefixes
// plain.
peer.send(&announcement(prefix("203.0.113.0/24").into(), &[PEER_ASN]))
.await?;
wait_for_routes(&server, 1).await?;
assert_eq!(held(&server), [(0, vec![PEER_ASN], true)]);
Ok(())
}
+112
View File
@@ -0,0 +1,112 @@
//! Who may connect: only the addresses of configured peers, and each of
//! them only so many times at once.
use std::net::{Ipv4Addr, SocketAddr};
use std::time::Duration;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::TestServer;
use bgp_server::config::PeerConfig;
use bgp_server::family::FamilySet;
use eyre::Result;
use tokio::net::TcpListener;
const LOCAL_ASN: u32 = 65000;
const PEER_ASN: u32 = 65001;
const TIMEOUT: Duration = Duration::from_secs(5);
fn peer_at(endpoint: SocketAddr) -> PeerConfig {
PeerConfig {
name: "r1".into(),
endpoint,
remote_asn: PEER_ASN,
hold_time_secs: 90,
local_address: None,
families: FamilySet::supported(),
add_path: None,
}
}
/// Whether the server closed the connection without sending anything.
async fn closed_unanswered(peer: &mut TestPeer) -> bool {
matches!(
tokio::time::timeout(TIMEOUT, peer.recv()).await,
Ok(Err(_))
)
}
/// A host that is not a configured peer gets nothing: no OPEN to answer,
/// no session, no routes taken from it. Any host used to be able to open a
/// session as any AS it named.
#[tokio::test]
async fn test_a_host_that_is_not_a_configured_peer_is_refused() -> Result<()> {
// The only peer configured is somewhere else (documentation space).
let server = TestServer::start_with(LOCAL_ASN, |c| {
c.accept_unconfigured_peers = false;
c.peers = vec![peer_at("192.0.2.9:179".parse().unwrap())];
})
.await?;
let mut stranger = TestPeer::connect(server.addr).await?;
assert!(closed_unanswered(&mut stranger).await);
assert!(server.registry.list().is_empty());
Ok(())
}
/// The address of a configured peer is let in, and held to what is
/// configured for that peer.
#[tokio::test]
async fn test_a_configured_peer_is_let_in() -> Result<()> {
// Somewhere for the server's own dialling to go; never answered.
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let endpoint = listener.local_addr()?;
let server = TestServer::start_with(LOCAL_ASN, |c| {
c.accept_unconfigured_peers = false;
c.peers = vec![peer_at(endpoint)];
})
.await?;
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
.await?;
tokio::time::timeout(TIMEOUT, async {
while server.registry.list().is_empty() {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
assert_eq!(server.registry.list()[0].name.as_deref(), Some("r1"));
Ok(())
}
/// One address may hold only a few connections at a time, so that one host
/// cannot use up the server's sockets with connections that never get as
/// far as a session.
#[tokio::test]
async fn test_one_address_may_open_only_a_few_connections() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
// Four that say nothing: each is sent the server's OPEN and waited on.
let mut held = Vec::new();
for _ in 0..4 {
let mut peer = TestPeer::connect(server.addr).await?;
tokio::time::timeout(TIMEOUT, peer.recv_open()).await??;
held.push(peer);
}
let mut one_too_many = TestPeer::connect(server.addr).await?;
assert!(closed_unanswered(&mut one_too_many).await);
// When one of them goes, there is room again.
held.pop();
tokio::time::timeout(TIMEOUT, async {
loop {
let mut peer = TestPeer::connect(server.addr).await?;
if peer.recv_open().await.is_ok() {
return Ok::<_, eyre::Report>(());
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
})
.await??;
Ok(())
}
+500
View File
@@ -0,0 +1,500 @@
//! End-to-end tests for locally originated routes: announcing via the
//! `LocalRib` handle must produce UPDATEs on every established session.
use std::net::Ipv4Addr;
use std::time::Duration;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::{RouteChange, TestServer};
use bgp_packet::message::{
AsPathAttribute, CommunitiesPathAttribute, LargeCommunitiesPathAttribute, LargeCommunity,
LocalPrefPathAttribute, NextHopPathAttribute, OriginPathAttribute, PathAttribute,
UpdateMessage,
};
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::{LocalRoute, LocalRouteAttrs};
use bgp_server::session::SessionEvent;
use eyre::Result;
const LOCAL_ASN: u32 = 65000;
const PEER_ASN: u32 = 65001;
const TIMEOUT: Duration = Duration::from_secs(5);
fn prefix(s: &str) -> IpPrefix {
IpPrefix::try_from(s).unwrap()
}
fn route(p: &str, attrs: LocalRouteAttrs) -> LocalRoute {
LocalRoute::new(prefix(p), attrs)
}
async fn established_peer(server: &TestServer) -> Result<TestPeer> {
established_peer_with_id(server, Ipv4Addr::new(10, 0, 0, 1)).await
}
/// Distinct routers need distinct BGP Identifiers, or the second connection
/// is (correctly) treated as a collision with the first.
async fn established_peer_with_id(server: &TestServer, router_id: Ipv4Addr) -> Result<TestPeer> {
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(PEER_ASN, router_id, 90).await?;
Ok(peer)
}
async fn next_update(peer: &mut TestPeer) -> Result<UpdateMessage> {
tokio::time::timeout(TIMEOUT, peer.recv_update()).await?
}
/// Waits until the registry reports `n` established sessions.
async fn wait_for_sessions(server: &TestServer, n: usize) -> Result<()> {
tokio::time::timeout(TIMEOUT, async {
while server.registry.list().len() != n {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
Ok(())
}
// ---------------------------------------------------------------------------
// Session registry
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_registry_tracks_session_lifecycle() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut events = server.registry.subscribe();
assert!(server.registry.list().is_empty());
let peer = established_peer(&server).await?;
let up = tokio::time::timeout(TIMEOUT, events.recv()).await??;
let SessionEvent::Up(handle) = up else {
panic!("expected Up, got {up:?}")
};
assert_eq!(handle.peer.asn, PEER_ASN);
assert_eq!(handle.local_addr, server.addr);
assert!(handle.four_octet_asn);
assert_eq!(server.registry.list().len(), 1);
drop(peer);
let down = tokio::time::timeout(TIMEOUT, events.recv()).await??;
assert!(
matches!(down, SessionEvent::Down(p) if p == handle.peer),
"got {down:?}"
);
assert!(server.registry.list().is_empty());
Ok(())
}
// ---------------------------------------------------------------------------
// Announce / withdraw
// ---------------------------------------------------------------------------
/// A route announced while the session is up is sent immediately, with the
/// attributes the announcer chose and our address as the next hop.
#[tokio::test]
async fn test_announce_after_session_up() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = established_peer(&server).await?;
wait_for_sessions(&server, 1).await?;
server
.local_rib
.announce(route(
"10.100.1.0/24",
LocalRouteAttrs {
origin: OriginPathAttribute::IGP,
prepend: 0,
med: None,
communities: vec![(65000, 100), (65535, 65281)],
large_communities: vec![LargeCommunity {
global_admin: 65000,
data_1: 7,
data_2: 8,
}],
},
))
.await?;
let update = next_update(&mut peer).await?;
assert_eq!(update.announced_routes, vec![prefix("10.100.1.0/24")]);
assert!(update.withdrawn_routes.is_empty());
assert_eq!(
update.path_attributes,
vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![LOCAL_ASN]),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::LOCALHOST)),
PathAttribute::Communities(CommunitiesPathAttribute(vec![
(65000, 100),
(65535, 65281)
])),
PathAttribute::LargeCommunities(LargeCommunitiesPathAttribute {
communities: vec![LargeCommunity {
global_admin: 65000,
data_1: 7,
data_2: 8
}],
}),
]
);
Ok(())
}
/// Routes announced before any session exists are replayed when a session
/// reaches Established.
#[tokio::test]
async fn test_routes_replayed_on_session_up() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
server
.local_rib
.announce(route("10.100.1.0/24", LocalRouteAttrs::default()))
.await?;
server
.local_rib
.announce(route("10.100.2.0/24", LocalRouteAttrs::default()))
.await?;
let mut peer = established_peer(&server).await?;
let mut seen = vec![next_update(&mut peer).await?, next_update(&mut peer).await?]
.into_iter()
.flat_map(|u| u.announced_routes)
.map(|p| p.to_string())
.collect::<Vec<_>>();
seen.sort();
assert_eq!(seen, vec!["10.100.1.0/24", "10.100.2.0/24"]);
Ok(())
}
/// An announcement is fanned out to every established session.
#[tokio::test]
async fn test_announce_reaches_all_sessions() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut a = established_peer_with_id(&server, Ipv4Addr::new(10, 0, 0, 1)).await?;
let mut b = established_peer_with_id(&server, Ipv4Addr::new(10, 0, 0, 2)).await?;
wait_for_sessions(&server, 2).await?;
server
.local_rib
.announce(route("10.100.1.0/24", LocalRouteAttrs::default()))
.await?;
for peer in [&mut a, &mut b] {
let update = next_update(peer).await?;
assert_eq!(update.announced_routes, vec![prefix("10.100.1.0/24")]);
}
Ok(())
}
#[tokio::test]
async fn test_withdraw_sends_withdrawn_route() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = established_peer(&server).await?;
wait_for_sessions(&server, 1).await?;
server
.local_rib
.announce(route("10.100.1.0/24", LocalRouteAttrs::default()))
.await?;
let _ = next_update(&mut peer).await?;
server.local_rib.withdraw(prefix("10.100.1.0/24")).await?;
let update = next_update(&mut peer).await?;
assert_eq!(update.withdrawn_routes, vec![prefix("10.100.1.0/24")]);
assert!(update.announced_routes.is_empty());
assert!(update.path_attributes.is_empty());
Ok(())
}
/// Re-announcing with new attributes sends a fresh UPDATE (implicit replace).
#[tokio::test]
async fn test_reannounce_with_prepend_and_med() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = established_peer(&server).await?;
wait_for_sessions(&server, 1).await?;
server
.local_rib
.announce(route("10.100.1.0/24", LocalRouteAttrs::default()))
.await?;
let first = next_update(&mut peer).await?;
assert_eq!(
first.path_attributes[1],
AsPathAttribute::from_asns(vec![LOCAL_ASN])
);
server
.local_rib
.announce(route(
"10.100.1.0/24",
LocalRouteAttrs {
prepend: 3,
med: Some(42),
..LocalRouteAttrs::default()
},
))
.await?;
let second = next_update(&mut peer).await?;
assert_eq!(second.announced_routes, vec![prefix("10.100.1.0/24")]);
assert_eq!(
second.path_attributes[1],
AsPathAttribute::from_asns(vec![LOCAL_ASN; 4])
);
assert!(
second
.path_attributes
.iter()
.any(|a| matches!(a, PathAttribute::MultiExitDisc(m) if m.0 == 42))
);
Ok(())
}
/// The session stays healthy (keepalives keep flowing) while UPDATEs are sent.
#[tokio::test]
async fn test_keepalives_continue_alongside_updates() -> Result<()> {
let server = TestServer::start_with_hold_time(LOCAL_ASN, 3).await?;
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 3)
.await?;
wait_for_sessions(&server, 1).await?;
server
.local_rib
.announce(route("10.100.1.0/24", LocalRouteAttrs::default()))
.await?;
let mut saw_update = false;
let mut saw_keepalive = false;
tokio::time::timeout(TIMEOUT, async {
while !(saw_update && saw_keepalive) {
match peer.recv().await? {
bgp_packet::message::Message::Update(_) => saw_update = true,
bgp_packet::message::Message::KeepAlive => saw_keepalive = true,
other => eyre::bail!("unexpected {other:?}"),
}
}
Ok::<_, eyre::Error>(())
})
.await??;
Ok(())
}
/// Routes learned from a peer disappear from the Adj-RIB-In when its
/// session ends, with a Withdrawn event for each.
#[tokio::test]
async fn test_received_routes_removed_when_session_closes() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
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?;
peer.send(&bgp_packet::message::Message::Update(
bgp_packet::message::UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![PEER_ASN]),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(10, 0, 0, 1))),
],
announced_routes: vec![
prefix("203.0.113.0/24").into(),
prefix("198.51.100.0/24").into(),
],
},
))
.await?;
for _ in 0..2 {
let ev = tokio::time::timeout(TIMEOUT, rib_events.recv()).await??;
assert!(matches!(ev, RouteChange::Announced { .. }), "{ev:?}");
}
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?? {
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.received_routes().is_empty());
// Withdrawals precede the Down event.
let _up = tokio::time::timeout(TIMEOUT, session_events.recv()).await??;
let down = tokio::time::timeout(TIMEOUT, session_events.recv()).await??;
assert!(matches!(down, SessionEvent::Down(_)), "{down:?}");
Ok(())
}
// ---------------------------------------------------------------------------
// Route refresh and shutdown
// ---------------------------------------------------------------------------
/// ROUTE-REFRESH for IPv4 unicast exactly as a router sends it (RFC 2918):
/// 19-byte header plus AFI, reserved, SAFI.
#[rustfmt::skip]
const ROUTE_REFRESH_IPV4: &[u8] = &[
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x17, 0x05,
0x00, 0x01, 0x00, 0x01,
];
/// A peer whose import policy changed asks for our routes again; it must get
/// every local route re-sent, and the session must stay up.
#[tokio::test]
async fn test_route_refresh_resends_local_routes() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = established_peer(&server).await?;
wait_for_sessions(&server, 1).await?;
server
.local_rib
.announce(route("10.100.1.0/24", LocalRouteAttrs::default()))
.await?;
server
.local_rib
.announce(route("10.100.2.0/24", LocalRouteAttrs::default()))
.await?;
let _ = next_update(&mut peer).await?;
let _ = next_update(&mut peer).await?;
peer.send_raw(ROUTE_REFRESH_IPV4).await?;
let mut resent = vec![next_update(&mut peer).await?, next_update(&mut peer).await?]
.into_iter()
.flat_map(|u| u.announced_routes)
.map(|p| p.to_string())
.collect::<Vec<_>>();
resent.sort();
assert_eq!(resent, vec!["10.100.1.0/24", "10.100.2.0/24"]);
assert_eq!(
server.registry.list().len(),
1,
"session must survive a route refresh"
);
Ok(())
}
/// Shutting down tells each peer why (Cease / Administrative Shutdown) and
/// closes the sessions, instead of leaving dead TCP connections behind.
#[tokio::test]
async fn test_shutdown_sends_cease_to_every_peer() -> Result<()> {
use bgp_packet::errors::{BgpError, CeaseSubcode};
use bgp_packet::message::Message;
let server = TestServer::start(LOCAL_ASN).await?;
let mut a = established_peer_with_id(&server, Ipv4Addr::new(10, 0, 0, 1)).await?;
let mut b = established_peer_with_id(&server, Ipv4Addr::new(10, 0, 0, 2)).await?;
wait_for_sessions(&server, 2).await?;
let left = server.registry.shutdown(Duration::from_secs(2)).await;
assert_eq!(left, 0, "sessions still open after shutdown");
assert!(server.registry.is_shutting_down());
for peer in [&mut a, &mut b] {
let notif = tokio::time::timeout(TIMEOUT, async {
loop {
match peer.recv().await? {
Message::Notification(n) => return Ok::<_, eyre::Error>(n),
Message::KeepAlive => {}
other => eyre::bail!("unexpected {other:?}"),
}
}
})
.await??;
assert!(
matches!(notif.error, BgpError::Cease(CeaseSubcode::AdminShutdown)),
"{notif:?}"
);
assert!(peer.is_closed().await);
}
Ok(())
}
// ---------------------------------------------------------------------------
// Routes from the configuration
// ---------------------------------------------------------------------------
/// Routes listed under `originate` are in the table before any peer
/// connects, and each session is sent them when it comes up.
#[tokio::test]
async fn test_configured_routes_are_announced_to_every_new_session() -> Result<()> {
let attrs = LocalRouteAttrs {
med: Some(7),
communities: vec![(65000, 100)],
..Default::default()
};
let configured = vec![
route("10.100.1.0/24", attrs),
route("10.100.2.0/24", LocalRouteAttrs::default()),
];
let server = TestServer::start_with(LOCAL_ASN, |c| c.originate = configured).await?;
assert_eq!(server.local_rib.snapshot().await.unwrap().len(), 2);
for router_id in [Ipv4Addr::new(10, 0, 0, 1), Ipv4Addr::new(10, 0, 0, 2)] {
let mut peer = established_peer_with_id(&server, router_id).await?;
let mut seen = [next_update(&mut peer).await?, next_update(&mut peer).await?];
seen.sort_by_key(|u| u.announced_routes[0].to_string());
assert_eq!(seen[0].announced_routes, vec![prefix("10.100.1.0/24")]);
assert!(
seen[0]
.path_attributes
.contains(&PathAttribute::Communities(CommunitiesPathAttribute(vec![
(65000, 100)
])))
);
assert_eq!(seen[1].announced_routes, vec![prefix("10.100.2.0/24")]);
}
Ok(())
}
/// A peer in our own AS is sent a route we originate with an empty AS path
/// and a LOCAL_PREF, whatever prepending is configured: our AS number goes
/// on the path only as the route leaves the AS. It used to be sent as to
/// an external peer, which an internal one refuses.
#[tokio::test]
async fn test_a_peer_in_our_own_as_gets_an_empty_path_and_a_local_pref() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(LOCAL_ASN, Ipv4Addr::new(10, 0, 0, 1), 90)
.await?;
// The peer reads as a router in the server's AS does: one outside it
// would drop the LOCAL_PREF.
peer.ctx.ibgp = true;
wait_for_sessions(&server, 1).await?;
assert!(server.registry.list()[0].ibgp);
server
.local_rib
.announce(route(
"10.100.1.0/24",
LocalRouteAttrs {
prepend: 2,
..LocalRouteAttrs::default()
},
))
.await?;
let update = next_update(&mut peer).await?;
assert_eq!(update.announced_routes, vec![prefix("10.100.1.0/24")]);
assert!(
update
.path_attributes
.contains(&PathAttribute::AsPath(AsPathAttribute { segments: vec![] })),
"{:?}",
update.path_attributes
);
assert!(
update
.path_attributes
.contains(&PathAttribute::LocalPref(LocalPrefPathAttribute(100))),
"{:?}",
update.path_attributes
);
Ok(())
}
@@ -2,17 +2,41 @@ 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};
use bgp_packet::errors::{BgpError, CeaseSubcode, FsmSubcode, MsgHeaderSubcode, UpdateMsgSubcode};
use bgp_packet::message::{
Message, MpReachNlriPathAttribute, MpUnreachNlriPathAttribute, NlriNextHop,
NotificationMessage, PathAttribute, UpdateMessage,
AsPathAttribute, Message, MpReachNlriPathAttribute, MpUnreachNlriPathAttribute,
NextHopPathAttribute, NlriNextHop, NotificationMessage, OriginPathAttribute, PathAttribute,
UpdateMessage,
};
use bgp_primitives::constants::{AddressFamilyId, SubsequentAfi};
use bgp_primitives::constants::{AfiSafi, IpVersion};
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::RibEvent;
use eyre::Result;
const TIMEOUT: Duration = Duration::from_secs(5);
/// The attributes every announcement of IPv4 routes must carry. An UPDATE
/// without them has its routes withdrawn instead (RFC 7606).
fn mandatory_attributes() -> Vec<PathAttribute> {
let mut attributes = with_origin_and_as_path(PathAttribute::NextHop(NextHopPathAttribute(
Ipv4Addr::new(192, 0, 2, 1),
)));
// In order of type code: ORIGIN, AS_PATH, NEXT_HOP.
attributes.rotate_left(1);
attributes
}
/// `first` followed by the attributes every announcement must carry.
/// Routes in MP_REACH_NLRI have their next hop there.
fn with_origin_and_as_path(first: PathAttribute) -> Vec<PathAttribute> {
vec![
first,
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![65001]),
]
}
// ---------------------------------------------------------------------------
// Connectivity
// ---------------------------------------------------------------------------
@@ -74,6 +98,147 @@ async fn test_bad_marker_does_not_crash_server() -> Result<()> {
Ok(())
}
/// Reads until a NOTIFICATION arrives, skipping KEEPALIVEs.
async fn expect_notification(peer: &mut TestPeer) -> Result<NotificationMessage> {
tokio::time::timeout(TIMEOUT, async {
loop {
match peer.recv().await? {
Message::Notification(n) => return Ok(n),
Message::KeepAlive => {}
other => eyre::bail!("expected NOTIFICATION, got {other:?}"),
}
}
})
.await?
}
/// A header with the given length and type, followed by `body`.
fn framed(length: u16, msg_type: u8, body: &[u8]) -> Vec<u8> {
let mut buf = vec![0xFF; 16];
buf.extend_from_slice(&length.to_be_bytes());
buf.push(msg_type);
buf.extend_from_slice(body);
buf
}
/// What the peer sent decides the NOTIFICATION it gets back (RFC 4271 §6),
/// in every state, and the session ends.
#[tokio::test]
async fn test_malformed_messages_are_answered_with_the_matching_notification() -> Result<()> {
let server = TestServer::start(65000).await?;
let header = |subcode| BgpError::MsgHeader(subcode);
let mut bad_marker = framed(19, 4, &[]);
bad_marker[0] = 0;
let cases: Vec<(&str, Vec<u8>, BgpError, Vec<u8>)> = vec![
(
"length beyond the maximum",
framed(5000, 2, &[]),
header(MsgHeaderSubcode::BadMessageLength),
5000u16.to_be_bytes().to_vec(),
),
(
"length below a header",
framed(5, 4, &[]),
header(MsgHeaderSubcode::BadMessageLength),
vec![0, 5],
),
(
"unknown message type",
framed(19, 9, &[]),
header(MsgHeaderSubcode::BadMessageType),
vec![9],
),
(
"marker not all ones",
bad_marker,
header(MsgHeaderSubcode::ConnNotSynchronized),
vec![],
),
(
"UPDATE with attributes running past its end",
framed(23, 2, &[0, 0, 0, 40]),
BgpError::UpdateMsg(UpdateMsgSubcode::MalformedAttrs),
vec![],
),
];
for (i, (what, bytes, error, data)) in cases.into_iter().enumerate() {
// Established: a distinct router ID each time, so that a session
// still closing does not collide with the next one.
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(65001, Ipv4Addr::new(10, 0, 1, i as u8 + 1), 90)
.await?;
peer.send_raw(&bytes).await?;
let notification = expect_notification(&mut peer).await?;
assert_eq!(
(notification.error, notification.data),
(error.clone(), data.clone()),
"{what}, Established"
);
// OpenSent: the same bytes instead of an OPEN.
let mut peer = TestPeer::connect(server.addr).await?;
let _ = peer.recv_open().await?;
peer.send_raw(&bytes).await?;
let notification = expect_notification(&mut peer).await?;
assert_eq!(
(notification.error, notification.data),
(error, data),
"{what}, OpenSent"
);
}
// A well-formed message at the wrong time is an FSM error, not a
// message error.
let mut peer = TestPeer::connect(server.addr).await?;
let _ = peer.recv_open().await?;
peer.send(&Message::KeepAlive).await?;
let notification = expect_notification(&mut peer).await?;
assert_eq!(
notification.error,
BgpError::FsmError(FsmSubcode::UnexpectedOpenSent)
);
Ok(())
}
/// A message may arrive in pieces, with the server's own timers firing in
/// between. The server once threw away the piece it had already read when
/// that happened, so the message was silently lost (and the stream was only
/// back in step if the remainder happened to be thrown away as well).
#[tokio::test]
async fn test_message_split_across_a_keepalive_tick_is_still_one_message() -> Result<()> {
// Hold time 3 s: the server sends a KEEPALIVE every second.
let server = TestServer::start_with_hold_time(65000, 3).await?;
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 3).await?;
let prefix = IpPrefix::new(IpVersion::Ipv4, vec![10], 8)?;
let mut update = bytes::BytesMut::new();
Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: mandatory_attributes(),
announced_routes: vec![prefix.into()],
})
.to_wire(&peer.ctx, &mut update)?;
// The first half, a pause long enough for the server's keepalive timer
// to fire, then the rest.
peer.send_raw(&update[..10]).await?;
tokio::time::sleep(Duration::from_millis(1300)).await;
peer.send_raw(&update[10..]).await?;
tokio::time::timeout(TIMEOUT, async {
while !server.received_routes().iter().any(|e| e.prefix == prefix) {
tokio::time::sleep(Duration::from_millis(20)).await;
}
})
.await
.map_err(|_| eyre::eyre!("the split UPDATE never reached the RIB"))?;
assert_eq!(server.registry.list().len(), 1, "session was dropped");
Ok(())
}
// ---------------------------------------------------------------------------
// OpenConfirm → Established
// ---------------------------------------------------------------------------
@@ -173,9 +338,9 @@ async fn test_server_accepts_update() -> Result<()> {
let update = Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![],
path_attributes: mandatory_attributes(),
// 10.0.0.0/8 — one byte of prefix for a /8.
announced_routes: vec![IpPrefix::new(AddressFamilyId::Ipv4, vec![10], 8)?],
announced_routes: vec![IpPrefix::new(IpVersion::Ipv4, vec![10], 8)?.into()],
});
peer.send(&update).await?;
@@ -193,15 +358,16 @@ async fn test_server_accepts_ipv6_update() -> Result<()> {
.await?;
// 2001:db8::/32
let prefix = IpPrefix::new(AddressFamilyId::Ipv6, vec![0x20, 0x01, 0x0d, 0xb8], 32)?;
let prefix = IpPrefix::new(IpVersion::Ipv6, vec![0x20, 0x01, 0x0d, 0xb8], 32)?;
let update = Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
afi: AddressFamilyId::Ipv6,
safi: SubsequentAfi::Unicast,
next_hop: NlriNextHop::Ipv6("2001:db8::1".parse().unwrap()),
prefixes: vec![prefix],
})],
path_attributes: with_origin_and_as_path(PathAttribute::MpReachNlri(
MpReachNlriPathAttribute {
family: AfiSafi::IPV6_UNICAST,
next_hop: NlriNextHop::Ipv6("2001:db8::1".parse().unwrap()),
prefixes: vec![prefix.into()],
},
)),
announced_routes: vec![],
});
peer.send(&update).await?;
@@ -219,26 +385,27 @@ 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).await?;
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
.await?;
let prefix = IpPrefix::new(AddressFamilyId::Ipv4, vec![10], 8)?;
let prefix = IpPrefix::new(IpVersion::Ipv4, vec![10], 8)?;
peer.send(&Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![],
announced_routes: vec![prefix.clone()],
path_attributes: mandatory_attributes(),
announced_routes: vec![prefix.into()],
}))
.await?;
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;
let snapshot = server.received_routes();
assert!(
snapshot.iter().any(|e| e.prefix == prefix),
"10.0.0.0/8 missing from RIB snapshot",
@@ -250,25 +417,26 @@ 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).await?;
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
.await?;
let prefix = IpPrefix::new(AddressFamilyId::Ipv4, vec![10], 8)?;
let prefix = IpPrefix::new(IpVersion::Ipv4, vec![10], 8)?;
// Announce first.
peer.send(&Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![],
announced_routes: vec![prefix.clone()],
path_attributes: mandatory_attributes(),
announced_routes: vec![prefix.into()],
}))
.await?;
tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
// Now withdraw.
peer.send(&Message::Update(UpdateMessage {
withdrawn_routes: vec![prefix.clone()],
withdrawn_routes: vec![prefix.into()],
path_attributes: vec![],
announced_routes: vec![],
}))
@@ -276,11 +444,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;
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",
@@ -292,31 +460,33 @@ 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).await?;
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
.await?;
let prefix = IpPrefix::new(AddressFamilyId::Ipv6, vec![0x20, 0x01, 0x0d, 0xb8], 32)?;
let prefix = IpPrefix::new(IpVersion::Ipv6, vec![0x20, 0x01, 0x0d, 0xb8], 32)?;
peer.send(&Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
afi: AddressFamilyId::Ipv6,
safi: SubsequentAfi::Unicast,
next_hop: NlriNextHop::Ipv6("2001:db8::1".parse().unwrap()),
prefixes: vec![prefix.clone()],
})],
path_attributes: with_origin_and_as_path(PathAttribute::MpReachNlri(
MpReachNlriPathAttribute {
family: AfiSafi::IPV6_UNICAST,
next_hop: NlriNextHop::Ipv6("2001:db8::1".parse().unwrap()),
prefixes: vec![prefix.into()],
},
)),
announced_routes: vec![],
}))
.await?;
let event = tokio::time::timeout(Duration::from_secs(1), events.recv()).await??;
assert!(
matches!(&event, RibEvent::Announced { prefix: p, .. } if p == &prefix),
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;
let snapshot = server.received_routes();
assert!(
snapshot.iter().any(|e| e.prefix == prefix),
"2001:db8::/32 missing from RIB snapshot",
@@ -328,22 +498,24 @@ 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).await?;
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90)
.await?;
let prefix = IpPrefix::new(AddressFamilyId::Ipv6, vec![0x20, 0x01, 0x0d, 0xb8], 32)?;
let prefix = IpPrefix::new(IpVersion::Ipv6, vec![0x20, 0x01, 0x0d, 0xb8], 32)?;
// Announce first.
peer.send(&Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
afi: AddressFamilyId::Ipv6,
safi: SubsequentAfi::Unicast,
next_hop: NlriNextHop::Ipv6("2001:db8::1".parse().unwrap()),
prefixes: vec![prefix.clone()],
})],
path_attributes: with_origin_and_as_path(PathAttribute::MpReachNlri(
MpReachNlriPathAttribute {
family: AfiSafi::IPV6_UNICAST,
next_hop: NlriNextHop::Ipv6("2001:db8::1".parse().unwrap()),
prefixes: vec![prefix.into()],
},
)),
announced_routes: vec![],
}))
.await?;
@@ -353,9 +525,8 @@ async fn test_rib_records_ipv6_withdraw() -> Result<()> {
peer.send(&Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![PathAttribute::MpUnreachNlri(MpUnreachNlriPathAttribute {
afi: AddressFamilyId::Ipv6,
safi: SubsequentAfi::Unicast,
prefixes: vec![prefix.clone()],
family: AfiSafi::IPV6_UNICAST,
prefixes: vec![prefix.into()],
})],
announced_routes: vec![],
}))
@@ -363,11 +534,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;
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, Nlri};
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: Nlri::plain(prefixes.iter().copied()),
})
}
fn withdrawal(prefixes: &[IpPrefix]) -> Message {
Message::Update(UpdateMessage {
withdrawn_routes: Nlri::plain(prefixes.iter().copied()),
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(())
}
+315
View File
@@ -0,0 +1,315 @@
//! Connection collision detection (RFC 4271 §6.8) against a live server.
//!
//! The test server's BGP Identifier is 127.0.0.1. A test peer picks an
//! identifier above or below that to steer which connection must survive.
use std::net::{Ipv4Addr, SocketAddr};
use std::time::Duration;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::TestServer;
use bgp_packet::errors::{BgpError, CeaseSubcode, OpenMsgSubcode};
use bgp_packet::message::{
AsPathAttribute, Message, NextHopPathAttribute, NotificationMessage, OriginPathAttribute,
PathAttribute, UpdateMessage,
};
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::{LocalRoute, LocalRouteAttrs};
use bgp_server::collision::Direction;
use bgp_server::config::PeerConfig;
use bgp_server::family::FamilySet;
use eyre::Result;
use tokio::net::TcpListener;
const LOCAL_ASN: u32 = 65000;
const PEER_ASN: u32 = 65001;
const TIMEOUT: Duration = Duration::from_secs(5);
/// Above the server's 127.0.0.1.
const HIGH_ID: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 1);
/// Below the server's 127.0.0.1.
const LOW_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1);
async fn server_dialing(listener: &TcpListener) -> Result<TestServer> {
let endpoint: SocketAddr = listener.local_addr()?;
TestServer::start_with(LOCAL_ASN, |c| {
c.connect_retry = Duration::from_millis(200);
c.peers = vec![PeerConfig {
name: "r1".into(),
endpoint,
remote_asn: PEER_ASN,
hold_time_secs: 90,
local_address: None,
families: FamilySet::supported(),
add_path: None,
}];
})
.await
}
/// Exchanges OPENs only, leaving the server's side in OpenConfirm (it has
/// sent its KEEPALIVE and is waiting for ours).
async fn open_only(peer: &mut TestPeer, router_id: Ipv4Addr) -> Result<()> {
peer.send_open(PEER_ASN, router_id, 90).await?;
peer.recv_open().await?;
Ok(())
}
/// Finishes the handshake begun by [`open_only`].
async fn confirm(peer: &mut TestPeer) -> Result<()> {
match tokio::time::timeout(TIMEOUT, peer.recv()).await?? {
Message::KeepAlive => {}
other => eyre::bail!("expected KEEPALIVE, got {other:?}"),
}
peer.send(&Message::KeepAlive).await
}
/// Reads until a NOTIFICATION arrives, skipping KEEPALIVEs.
async fn expect_notification(peer: &mut TestPeer) -> Result<NotificationMessage> {
tokio::time::timeout(TIMEOUT, async {
loop {
match peer.recv().await? {
Message::Notification(n) => return Ok(n),
Message::KeepAlive => {}
other => eyre::bail!("expected NOTIFICATION, got {other:?}"),
}
}
})
.await?
}
fn is_collision_cease(n: &NotificationMessage) -> bool {
matches!(
n.error,
BgpError::Cease(CeaseSubcode::ConnCollisionResolution)
)
}
async fn wait_for_sessions(server: &TestServer, n: usize) -> Result<()> {
tokio::time::timeout(TIMEOUT, async {
while server.registry.list().len() != n {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
Ok(())
}
/// Peer has the higher identifier, so the connection the *peer* initiated
/// must survive: the server closes its own outbound connection.
#[tokio::test]
async fn test_simultaneous_open_peer_higher_keeps_inbound() -> Result<()> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let server = server_dialing(&listener).await?;
// Server's outbound connection reaches OpenConfirm.
let (stream, _) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
let mut dialled = TestPeer::from_stream(stream);
open_only(&mut dialled, HIGH_ID).await?;
// The peer now also dials the server.
let mut inbound = TestPeer::connect(server.addr).await?;
open_only(&mut inbound, HIGH_ID).await?;
let notif = expect_notification(&mut dialled).await?;
assert!(is_collision_cease(&notif), "{notif:?}");
assert!(dialled.is_closed().await);
confirm(&mut inbound).await?;
wait_for_sessions(&server, 1).await?;
let session = server.registry.list().remove(0);
assert_eq!(session.direction, Direction::Inbound);
assert_eq!(session.name.as_deref(), Some("r1"));
// The survivor is fully functional.
server
.local_rib
.announce(LocalRoute::new(
IpPrefix::try_from("10.100.1.0/24")?,
LocalRouteAttrs::default(),
))
.await?;
let update = tokio::time::timeout(TIMEOUT, inbound.recv_update()).await??;
assert_eq!(update.announced_routes[0].to_string(), "10.100.1.0/24");
// The dialer notices the surviving session and does not open another.
let extra = tokio::time::timeout(Duration::from_millis(700), listener.accept()).await;
assert!(
extra.is_err(),
"dialer opened a second connection despite an established session"
);
Ok(())
}
/// Peer has the lower identifier, so the connection the *server* initiated
/// must survive: the peer's inbound connection is refused.
#[tokio::test]
async fn test_simultaneous_open_peer_lower_keeps_outbound() -> Result<()> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let server = server_dialing(&listener).await?;
let (stream, _) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
let mut dialled = TestPeer::from_stream(stream);
open_only(&mut dialled, LOW_ID).await?;
let mut inbound = TestPeer::connect(server.addr).await?;
open_only(&mut inbound, LOW_ID).await?;
let notif = expect_notification(&mut inbound).await?;
assert!(is_collision_cease(&notif), "{notif:?}");
assert!(inbound.is_closed().await);
confirm(&mut dialled).await?;
wait_for_sessions(&server, 1).await?;
assert_eq!(server.registry.list()[0].direction, Direction::Outbound);
Ok(())
}
/// The order in which the two OPENs are processed must not change the
/// outcome: here the inbound connection reaches OpenConfirm first.
#[tokio::test]
async fn test_outcome_is_independent_of_arrival_order() -> Result<()> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let server = server_dialing(&listener).await?;
let (stream, _) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
let mut dialled = TestPeer::from_stream(stream);
// Inbound first this time; peer is lower so outbound must still win.
let mut inbound = TestPeer::connect(server.addr).await?;
open_only(&mut inbound, LOW_ID).await?;
open_only(&mut dialled, LOW_ID).await?;
let notif = expect_notification(&mut inbound).await?;
assert!(is_collision_cease(&notif), "{notif:?}");
confirm(&mut dialled).await?;
wait_for_sessions(&server, 1).await?;
assert_eq!(server.registry.list()[0].direction, Direction::Outbound);
Ok(())
}
/// RFC default: a collision with an Established session closes the new
/// connection, whatever the identifiers say.
#[tokio::test]
async fn test_established_session_wins_by_default() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut first = TestPeer::connect(server.addr).await?;
first.handshake(PEER_ASN, HIGH_ID, 90).await?;
wait_for_sessions(&server, 1).await?;
let mut second = TestPeer::connect(server.addr).await?;
open_only(&mut second, HIGH_ID).await?;
let notif = expect_notification(&mut second).await?;
assert!(is_collision_cease(&notif), "{notif:?}");
// The original is untouched.
assert_eq!(server.registry.list().len(), 1);
server
.local_rib
.announce(LocalRoute::new(
IpPrefix::try_from("10.100.1.0/24")?,
LocalRouteAttrs::default(),
))
.await?;
let update = tokio::time::timeout(TIMEOUT, first.recv_update()).await??;
assert_eq!(update.announced_routes[0].to_string(), "10.100.1.0/24");
Ok(())
}
/// With collision_detect_established_state, a peer that restarts and dials
/// again replaces the stale session at once, and the routes learned over the
/// old session are flushed.
#[tokio::test]
async fn test_restarted_peer_replaces_stale_session_when_enabled() -> Result<()> {
let server =
TestServer::start_with(LOCAL_ASN, |c| c.collision_detect_established_state = true).await?;
let mut stale = TestPeer::connect(server.addr).await?;
stale.handshake(PEER_ASN, HIGH_ID, 90).await?;
wait_for_sessions(&server, 1).await?;
stale
.send(&Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![PEER_ASN]),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(10, 0, 0, 9))),
],
announced_routes: vec![IpPrefix::try_from("203.0.113.0/24")?.into()],
}))
.await?;
tokio::time::timeout(TIMEOUT, async {
while server.received_routes().is_empty() {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
let stale_addr = server.registry.list()[0].peer.addr;
// "Restart": a fresh connection from the same router.
let mut fresh = TestPeer::connect(server.addr).await?;
fresh.handshake(PEER_ASN, HIGH_ID, 90).await?;
let notif = expect_notification(&mut stale).await?;
assert!(is_collision_cease(&notif), "{notif:?}");
tokio::time::timeout(TIMEOUT, async {
loop {
let sessions = server.registry.list();
if sessions.len() == 1 && sessions[0].peer.addr != stale_addr {
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
assert!(
server.received_routes().is_empty(),
"stale routes were not flushed"
);
Ok(())
}
/// Different routers (different identifiers) never collide.
#[tokio::test]
async fn test_distinct_identifiers_do_not_collide() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut a = TestPeer::connect(server.addr).await?;
a.handshake(PEER_ASN, HIGH_ID, 90).await?;
let mut b = TestPeer::connect(server.addr).await?;
b.handshake(PEER_ASN, LOW_ID, 90).await?;
wait_for_sessions(&server, 2).await?;
Ok(())
}
/// A failed connection frees its slot: the same router can come back.
#[tokio::test]
async fn test_identifier_is_released_when_connection_ends() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut first = TestPeer::connect(server.addr).await?;
first.handshake(PEER_ASN, HIGH_ID, 90).await?;
wait_for_sessions(&server, 1).await?;
drop(first);
wait_for_sessions(&server, 0).await?;
let mut second = TestPeer::connect(server.addr).await?;
second.handshake(PEER_ASN, HIGH_ID, 90).await?;
wait_for_sessions(&server, 1).await?;
Ok(())
}
/// A peer claiming our own identifier is refused outright.
#[tokio::test]
async fn test_peer_with_our_identifier_is_refused() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = TestPeer::connect(server.addr).await?;
open_only(&mut peer, Ipv4Addr::new(127, 0, 0, 1)).await?;
let notif = expect_notification(&mut peer).await?;
assert!(
matches!(notif.error, BgpError::OpenMsg(OpenMsgSubcode::BadBgpId)),
"{notif:?}"
);
Ok(())
}
+319
View File
@@ -0,0 +1,319 @@
//! The active side: the server dials its configured peers and keeps
//! retrying.
use std::net::{Ipv4Addr, SocketAddr};
use std::time::Duration;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::TestServer;
use bgp_packet::errors::{BgpError, OpenMsgSubcode};
use bgp_packet::message::Message;
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::{LocalRoute, LocalRouteAttrs};
use bgp_server::config::PeerConfig;
use bgp_server::family::FamilySet;
use bgp_server::session::DialState;
use eyre::Result;
use tokio::net::TcpListener;
const LOCAL_ASN: u32 = 65000;
const PEER_ASN: u32 = 65001;
const TIMEOUT: Duration = Duration::from_secs(5);
/// Starts a server configured to dial `listener`'s address as peer "r1".
async fn server_dialing(listener: &TcpListener, remote_asn: u32) -> Result<TestServer> {
let endpoint: SocketAddr = listener.local_addr()?;
TestServer::start_with(LOCAL_ASN, |c| {
c.connect_retry = Duration::from_millis(200);
c.peers = vec![PeerConfig {
name: "r1".into(),
endpoint,
remote_asn,
hold_time_secs: 90,
local_address: None,
families: FamilySet::supported(),
add_path: None,
}];
})
.await
}
async fn accept(listener: &TcpListener) -> Result<TestPeer> {
let (stream, _) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
Ok(TestPeer::from_stream(stream))
}
#[tokio::test]
async fn test_server_dials_configured_peer_and_establishes() -> Result<()> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let server = server_dialing(&listener, PEER_ASN).await?;
let mut peer = accept(&listener).await?;
// Both sides send OPEN on connect; the test peer's handshake order works
// whichever side opened TCP.
let open = peer
.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
.await?;
assert_eq!(open.asn, LOCAL_ASN as u16);
let mut events = server.registry.subscribe();
// Either the Up event is still queued or the session is already listed.
let listed = tokio::time::timeout(TIMEOUT, async {
loop {
if let Some(s) = server.registry.list().first().cloned() {
return s;
}
let _ = events.recv().await;
}
})
.await?;
assert_eq!(listed.name.as_deref(), Some("r1"));
assert_eq!(listed.peer.asn, PEER_ASN);
assert_eq!(listed.peer.addr, listener.local_addr()?);
// Configured view reflects it; the dialer is not in backoff.
let (cfg, state) = server
.registry
.configured_peers()
.into_iter()
.next()
.unwrap();
assert_eq!(cfg.name, "r1");
assert!(
matches!(state, DialState::Connecting | DialState::Idle),
"{state:?}"
);
// Local routes flow over the dialled session too.
server
.local_rib
.announce(LocalRoute::new(
IpPrefix::try_from("10.100.1.0/24")?,
LocalRouteAttrs::default(),
))
.await?;
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
assert_eq!(update.announced_routes[0].to_string(), "10.100.1.0/24");
Ok(())
}
#[tokio::test]
async fn test_server_reconnects_after_peer_closes() -> Result<()> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let server = server_dialing(&listener, PEER_ASN).await?;
let mut first = accept(&listener).await?;
first
.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
.await?;
tokio::time::timeout(TIMEOUT, async {
while server.registry.list().is_empty() {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
drop(first);
// Session goes away, then the dialer comes back.
tokio::time::timeout(TIMEOUT, async {
while !server.registry.list().is_empty() {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
let (_, state) = server
.registry
.configured_peers()
.into_iter()
.next()
.unwrap();
assert!(matches!(state, DialState::Backoff { .. }), "{state:?}");
let mut second = accept(&listener).await?;
second
.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
.await?;
tokio::time::timeout(TIMEOUT, async {
while server.registry.list().is_empty() {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
Ok(())
}
#[tokio::test]
async fn test_wrong_asn_is_refused_with_bad_peer_as() -> Result<()> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let _server = server_dialing(&listener, PEER_ASN).await?;
let mut peer = accept(&listener).await?;
peer.send_open(65099, Ipv4Addr::new(10, 0, 0, 2), 90)
.await?;
let _ = peer.recv_open().await?;
let notif = tokio::time::timeout(TIMEOUT, async {
loop {
match peer.recv().await? {
Message::Notification(n) => return Ok::<_, eyre::Error>(n),
Message::KeepAlive => {}
other => eyre::bail!("unexpected {other:?}"),
}
}
})
.await??;
assert!(
matches!(notif.error, BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs)),
"{notif:?}"
);
// The dialer retries rather than giving up.
let _again = accept(&listener).await?;
Ok(())
}
/// An inbound connection from a configured peer's address is also checked
/// against the configured ASN and gets the configured name.
#[tokio::test]
async fn test_inbound_from_configured_address_uses_config() -> Result<()> {
// Configure a peer at the loopback address on a port nobody listens on,
// so the dialer never succeeds; then connect inbound from loopback.
let server = TestServer::start_with(LOCAL_ASN, |c| {
c.connect_retry = Duration::from_secs(60);
c.peers = vec![PeerConfig {
name: "r1".into(),
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1),
remote_asn: PEER_ASN,
hold_time_secs: 90,
local_address: None,
families: FamilySet::supported(),
add_path: None,
}];
})
.await?;
// Wrong ASN inbound → refused.
let mut bad = TestPeer::connect(server.addr).await?;
bad.send_open(65099, Ipv4Addr::new(10, 0, 0, 2), 90).await?;
let _ = bad.recv_open().await?;
let msg = tokio::time::timeout(TIMEOUT, bad.recv()).await??;
assert!(
matches!(&msg, Message::Notification(n) if matches!(n.error, BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs))),
"{msg:?}"
);
// Right ASN inbound → established, named.
let mut good = TestPeer::connect(server.addr).await?;
good.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
.await?;
let listed = tokio::time::timeout(TIMEOUT, async {
loop {
if let Some(s) = server.registry.list().first().cloned() {
return s;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
assert_eq!(listed.name.as_deref(), Some("r1"));
Ok(())
}
/// With `local_address` set the connection must come from that address (the
/// peer may identify us by it), and an address the host does not have is
/// reported as the reason for backing off rather than silently ignored.
#[tokio::test]
async fn test_local_address_is_used_as_the_source() -> Result<()> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let endpoint: SocketAddr = listener.local_addr()?;
let _server = TestServer::start_with(LOCAL_ASN, |c| {
c.connect_retry = Duration::from_millis(200);
c.peers = vec![PeerConfig {
name: "r1".into(),
endpoint,
remote_asn: PEER_ASN,
hold_time_secs: 90,
local_address: Some(Ipv4Addr::LOCALHOST.into()),
families: FamilySet::supported(),
add_path: None,
}];
})
.await?;
let (_stream, from) = tokio::time::timeout(TIMEOUT, listener.accept()).await??;
assert_eq!(from.ip(), std::net::IpAddr::from(Ipv4Addr::LOCALHOST));
// 192.0.2.1 is documentation space; no host has it.
let server = TestServer::start_with(LOCAL_ASN, |c| {
c.connect_retry = Duration::from_millis(200);
c.peers = vec![PeerConfig {
name: "r1".into(),
endpoint,
remote_asn: PEER_ASN,
hold_time_secs: 90,
local_address: Some(Ipv4Addr::new(192, 0, 2, 1).into()),
families: FamilySet::supported(),
add_path: None,
}];
})
.await?;
let reason = tokio::time::timeout(TIMEOUT, async {
loop {
if let Some((_, DialState::Backoff { last_error, .. })) =
server.registry.configured_peers().into_iter().next()
{
return last_error;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
})
.await?;
assert!(reason.contains("local_address 192.0.2.1"), "{reason}");
Ok(())
}
/// A peer that takes the connection and then refuses the session, here
/// because it is not the AS we are configured to expect, will refuse it
/// again. It is dialled ever more slowly, like a peer that cannot be
/// reached: connecting used to count as success and reset the wait, so
/// such a peer was dialled every few seconds for ever.
#[tokio::test]
async fn test_a_peer_that_refuses_the_session_is_dialled_ever_more_slowly() -> Result<()> {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?;
let server = server_dialing(&listener, PEER_ASN).await?;
let mut waits = Vec::new();
for _ in 0..3 {
let mut peer = accept(&listener).await?;
peer.send_open(65099, Ipv4Addr::new(10, 0, 0, 2), 90)
.await?;
let _ = peer.recv_open().await?;
let refusal = tokio::time::timeout(TIMEOUT, peer.recv()).await??;
assert!(
matches!(&refusal, Message::Notification(n) if n.error == BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs)),
"{refusal:?}"
);
drop(peer);
// How long the dialer says it will wait before trying again.
let wait = tokio::time::timeout(TIMEOUT, async {
loop {
if let Some((_, DialState::Backoff { until, last_error })) =
server.registry.configured_peers().into_iter().next()
{
assert!(last_error.contains("AS65099"), "{last_error}");
return until
.duration_since(std::time::SystemTime::now())
.unwrap_or_default();
}
tokio::time::sleep(Duration::from_millis(5)).await;
}
})
.await?;
waits.push(wait);
}
// 200 ms, then 400, then 800, less what passed before each was read.
assert!(
waits[2] > Duration::from_millis(500),
"the waits were {waits:?}"
);
Ok(())
}
+87
View File
@@ -0,0 +1,87 @@
//! What the server does when a part of it that it cannot do without fails.
//!
//! 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;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::TestServer;
use bgp_packet::errors::{BgpError, CeaseSubcode};
use bgp_packet::message::Message;
use bgp_rib::RibStopped;
use bgp_server::error::ServerError;
use eyre::{Result, bail};
const LOCAL_ASN: u32 = 65000;
const PEER_ASN: u32 = 65001;
const TIMEOUT: Duration = Duration::from_secs(5);
async fn established(server: &TestServer) -> Result<TestPeer> {
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 90)
.await?;
tokio::time::timeout(TIMEOUT, async {
while server.registry.list().is_empty() {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
Ok(peer)
}
/// Reads until the server's NOTIFICATION, skipping KEEPALIVEs.
async fn farewell(peer: &mut TestPeer) -> Result<BgpError> {
tokio::time::timeout(TIMEOUT, async {
loop {
match peer.recv().await? {
Message::KeepAlive => {}
Message::Notification(n) => return Ok(n.error),
other => bail!("expected NOTIFICATION, got {other:?}"),
}
}
})
.await?
}
#[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();
let outcome = tokio::time::timeout(TIMEOUT, server.stopped()).await?;
assert!(
matches!(outcome, Err(ServerError::RibStopped(RibStopped { rib })) if rib == "local RIB"),
"{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/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(())
}
+303
View File
@@ -0,0 +1,303 @@
//! Drives the HTTP API against a real server with a real BGP peer attached.
use std::net::Ipv4Addr;
use std::time::Duration;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::TestServer;
use bgp_packet::message::{
AsPathAttribute, CommunitiesPathAttribute, LargeCommunity, Message, NextHopPathAttribute,
OriginPathAttribute, PathAttribute, UpdateMessage,
};
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::{LocalRoute, LocalRouteAttrs};
use eyre::Result;
use serde_json::{Value, json};
const LOCAL_ASN: u32 = 65000;
const PEER_ASN: u32 = 65001;
const TIMEOUT: Duration = Duration::from_secs(5);
fn prefix(s: &str) -> IpPrefix {
IpPrefix::try_from(s).unwrap()
}
async fn established_peer(server: &TestServer) -> Result<TestPeer> {
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 1), 90)
.await?;
tokio::time::timeout(TIMEOUT, async {
while server.registry.list().is_empty() {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
Ok(peer)
}
async fn get_json(client: &reqwest::Client, url: &str) -> Result<Value> {
Ok(client
.get(url)
.send()
.await?
.error_for_status()?
.json()
.await?)
}
#[tokio::test]
async fn test_status_and_peers() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let (base, _http) = server.serve_http().await?;
let client = reqwest::Client::new();
let config = get_json(&client, &format!("{base}/api/config")).await?;
assert_eq!(config["path"], "test.textpb");
assert_eq!(config["text"], format!("local_asn: {LOCAL_ASN}\n"));
let status = get_json(&client, &format!("{base}/api/status")).await?;
assert_eq!(status["local_asn"], LOCAL_ASN);
assert_eq!(status["router_id"], "127.0.0.1");
assert_eq!(status["sessions"], 0);
assert!(status["started_at_unix"].as_u64().unwrap() > 1_700_000_000);
assert!(!status["hostname"].as_str().unwrap().is_empty());
let _peer = established_peer(&server).await?;
let status = get_json(&client, &format!("{base}/api/status")).await?;
assert_eq!(status["sessions"], 1);
let peers = get_json(&client, &format!("{base}/api/peers")).await?;
assert_eq!(peers.as_array().unwrap().len(), 1);
assert_eq!(peers[0]["peer_asn"], PEER_ASN);
assert_eq!(peers[0]["local_addr"], server.addr.to_string());
assert_eq!(peers[0]["four_octet_asn"], true);
assert_eq!(
peers[0]["address_families"],
serde_json::json!(["IPv4 unicast", "IPv6 unicast"])
);
Ok(())
}
/// Originated routes are listed with their attributes as text.
#[tokio::test]
async fn test_originated_routes_are_listed() -> Result<()> {
let attrs = LocalRouteAttrs {
prepend: 1,
med: Some(10),
communities: vec![(65000, 100), (65535, 65281)],
large_communities: vec![LargeCommunity {
global_admin: 65000,
data_1: 1,
data_2: 2,
}],
..Default::default()
};
let server = TestServer::start_with(LOCAL_ASN, |c| {
c.originate = vec![LocalRoute::new(prefix("10.100.1.0/24"), attrs)];
})
.await?;
let (base, _http) = server.serve_http().await?;
let client = reqwest::Client::new();
let local = get_json(&client, &format!("{base}/api/routes/local")).await?;
assert_eq!(local.as_array().unwrap().len(), 1);
assert_eq!(local[0]["prefix"], "10.100.1.0/24");
assert_eq!(local[0]["origin"], "IGP");
assert_eq!(local[0]["prepend"], 1);
assert_eq!(local[0]["med"], 10);
assert_eq!(local[0]["communities"], json!(["65000:100", "65535:65281"]));
assert_eq!(local[0]["large_communities"], json!(["65000:1:2"]));
assert_eq!(
get_json(&client, &format!("{base}/api/status")).await?["local_routes"],
1
);
Ok(())
}
/// The API only reports. Nothing reachable over HTTP changes what the
/// router announces.
#[tokio::test]
async fn test_api_is_read_only() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let (base, _http) = server.serve_http().await?;
let client = reqwest::Client::new();
let body = json!({ "prefix": "10.100.1.0/24" });
for path in [
"/api/routes/local",
"/api/routes/received",
"/api/peers",
"/api/status",
] {
let post = client
.post(format!("{base}{path}"))
.json(&body)
.send()
.await?;
assert_eq!(post.status(), 405, "POST {path}");
let delete = client
.delete(format!("{base}{path}"))
.json(&body)
.send()
.await?;
assert_eq!(delete.status(), 405, "DELETE {path}");
}
assert!(server.local_rib.snapshot().await.unwrap().is_empty());
Ok(())
}
/// Routes the peer sends us show up in the received table with a summary.
#[tokio::test]
async fn test_received_routes_summary() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let (base, _http) = server.serve_http().await?;
let client = reqwest::Client::new();
let mut peer = established_peer(&server).await?;
peer.send(&Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![PEER_ASN, 65002]),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(10, 0, 0, 1))),
PathAttribute::Communities(CommunitiesPathAttribute(vec![(65001, 7)])),
],
announced_routes: vec![prefix("203.0.113.0/24").into()],
}))
.await?;
let received = tokio::time::timeout(TIMEOUT, async {
loop {
let v = get_json(&client, &format!("{base}/api/routes/received")).await?;
if !v.as_array().unwrap().is_empty() {
return Ok::<_, eyre::Error>(v);
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
})
.await??;
assert_eq!(received[0]["prefix"], "203.0.113.0/24");
assert_eq!(received[0]["peer_asn"], PEER_ASN);
assert_eq!(received[0]["summary"]["as_path"], json!([PEER_ASN, 65002]));
assert_eq!(received[0]["summary"]["next_hop"], "10.0.0.1");
assert_eq!(received[0]["summary"]["communities"], json!(["65001:7"]));
Ok(())
}
/// The SSE stream reports session and route changes as they happen.
#[tokio::test]
async fn test_event_stream() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let (base, _http) = server.serve_http().await?;
let client = reqwest::Client::new();
let mut resp = client
.get(format!("{base}/api/events"))
.send()
.await?
.error_for_status()?;
assert!(
resp.headers()["content-type"]
.to_str()?
.starts_with("text/event-stream")
);
let _peer = established_peer(&server).await?;
server
.local_rib
.announce(LocalRoute::new(prefix("10.100.1.0/24"), Default::default()))
.await?;
// Collect SSE frames until we have seen both events.
let mut buf = String::new();
let mut seen_up = false;
let mut seen_announce = false;
tokio::time::timeout(TIMEOUT, async {
while !(seen_up && seen_announce) {
let chunk = resp
.chunk()
.await?
.ok_or_else(|| eyre::eyre!("stream ended"))?;
buf.push_str(std::str::from_utf8(&chunk)?);
for line in buf.lines().filter_map(|l| l.strip_prefix("data:")) {
let ev: Value = serde_json::from_str(line.trim())?;
match ev["type"].as_str() {
Some("session_up") if ev["peer"]["peer_asn"] == PEER_ASN => seen_up = true,
Some("local_announced") if ev["route"]["prefix"] == "10.100.1.0/24" => {
seen_announce = true
}
_ => {}
}
}
}
Ok::<_, eyre::Error>(())
})
.await??;
Ok(())
}
#[tokio::test]
async fn test_statusz_page_is_served() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let (base, _http) = server.serve_http().await?;
let client = reqwest::Client::new();
let resp = client.get(format!("{base}/statusz")).send().await?;
assert_eq!(resp.status(), 200);
assert!(
resp.headers()["content-type"]
.to_str()?
.starts_with("text/html")
);
let body = resp.text().await?;
assert!(body.contains("<title>Status for ferrix_bgp</title>"));
assert!(body.contains("/api/events"));
// The root redirects to the page.
let no_redirect = reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()?;
let resp = no_redirect.get(format!("{base}/")).send().await?;
assert_eq!(resp.status(), 307);
assert_eq!(resp.headers()["location"], "/statusz");
Ok(())
}
#[tokio::test]
async fn test_metricz() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let (base, _http) = server.serve_http().await?;
let client = reqwest::Client::new();
let _peer = established_peer(&server).await?;
let text = client
.get(format!("{base}/metricz"))
.send()
.await?
.error_for_status()?
.text()
.await?;
// Metrics are process-global, so other tests in this binary contribute
// too; assert on presence and a lower bound rather than exact values.
let metric = |name: &str| -> Option<i64> {
text.lines()
.find(|l| l.starts_with(name) && l[name.len()..].starts_with(' '))
.and_then(|l| l.rsplit(' ').next()?.parse().ok())
};
assert!(
metric("bgp_sessions_established").unwrap_or(0) >= 1,
"{text}"
);
assert!(
metric("bgp_messages_sent_total{type=\"open\"}").unwrap_or(0) >= 1,
"{text}"
);
assert!(
metric("bgp_messages_received_total{type=\"keepalive\"}").unwrap_or(0) >= 1,
"{text}"
);
assert!(metric("bgp_local_routes").is_some(), "{text}");
assert!(metric("bgp_rib_routes").is_some(), "{text}");
Ok(())
}
+148
View File
@@ -0,0 +1,148 @@
//! Interoperability with what real routers put on the wire.
use std::net::Ipv4Addr;
use std::time::Duration;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::TestServer;
use bgp_packet::message::{Message, PathAttribute};
use bgp_packet::open::{Capability, OpenOption};
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::{LocalRoute, LocalRouteAttrs};
use eyre::Result;
const TIMEOUT: Duration = Duration::from_secs(5);
/// The OPEN a BIRD 2 router sends for an IPv6-only session from AS210036:
/// one optional parameter carrying MP(IPv6 unicast), route refresh,
/// extended message, graceful restart (aware only), 4-octet AS, enhanced
/// route refresh and long-lived graceful restart (aware only).
#[rustfmt::skip]
const BIRD_OPEN: &[u8] = &[
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x37, 0x01, // length 55, type OPEN
0x04, // version
0x5b, 0xa0, // AS_TRANS
0x00, 0xf0, // hold time 240
0x0a, 0xff, 0x00, 0x01, // router id 10.255.0.1
0x1a, // optional parameters length 26
0x02, 0x18, // capabilities, 24 bytes
0x01, 0x04, 0x00, 0x02, 0x00, 0x01, // MP: IPv6 unicast
0x02, 0x00, // route refresh
0x06, 0x00, // extended message
0x40, 0x02, 0x00, 0x00, // graceful restart, no address families
0x41, 0x04, 0x00, 0x03, 0x34, 0x74, // 4-octet AS 210036
0x46, 0x00, // enhanced route refresh
0x47, 0x00, // long-lived graceful restart, none
];
/// IPv6 End-of-RIB: an UPDATE whose only attribute is an empty
/// MP_UNREACH_NLRI for IPv6 unicast (RFC 4724 §2).
#[rustfmt::skip]
const IPV6_END_OF_RIB: &[u8] = &[
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x1e, 0x02, // length 30, type UPDATE
0x00, 0x00, // no withdrawn routes
0x00, 0x07, // 7 bytes of path attributes
0x90, 0x0f, 0x00, 0x03, // optional + extended length, MP_UNREACH_NLRI, len 3
0x00, 0x02, 0x01, // IPv6 unicast, no prefixes
];
#[tokio::test]
async fn test_server_advertises_ipv4_and_ipv6_unicast() -> Result<()> {
let server = TestServer::start(65000).await?;
let mut peer = TestPeer::connect(server.addr).await?;
let open = peer.recv_open().await?;
let caps: Vec<&Capability> = open
.options
.iter()
.flat_map(|o| match o {
OpenOption::Capabilities(c) => c.iter(),
})
.collect();
for afi in [Afi::IPV4, Afi::IPV6] {
assert!(
caps.iter().any(|c| matches!(c, Capability::MultiProtocol { afi: a, safi: Safi::UNICAST } if *a == afi)),
"missing MP capability for {afi:?}: {caps:?}"
);
}
Ok(())
}
#[tokio::test]
async fn test_bird_open_and_end_of_rib_are_accepted() -> Result<()> {
let server = TestServer::start(4200000001).await?;
let mut peer = TestPeer::connect(server.addr).await?;
peer.send_raw(BIRD_OPEN).await?;
peer.recv_open().await?;
match tokio::time::timeout(TIMEOUT, peer.recv()).await?? {
Message::KeepAlive => {}
other => eyre::bail!("server did not accept BIRD's OPEN: {other:?}"),
}
peer.send(&Message::KeepAlive).await?;
tokio::time::timeout(TIMEOUT, async {
while server.registry.list().is_empty() {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
let session = server.registry.list().remove(0);
assert_eq!(
session.peer.asn, 210036,
"4-octet ASN must come from the capability, not AS_TRANS"
);
assert!(session.four_octet_asn);
// End-of-RIB must neither break the session nor create a route.
peer.send_raw(IPV6_END_OF_RIB).await?;
peer.send(&Message::KeepAlive).await?;
tokio::time::sleep(Duration::from_millis(200)).await;
assert_eq!(
server.registry.list().len(),
1,
"session dropped after End-of-RIB"
);
assert!(server.received_routes().is_empty());
Ok(())
}
/// Over an IPv4 test transport an IPv6 route cannot be sent (no IPv6 next
/// hop), so this only checks the builder the advertiser uses: the UPDATE for
/// an IPv6 prefix carries MP_REACH_NLRI with an IPv6 next hop and no NEXT_HOP.
#[tokio::test]
async fn test_ipv6_prefix_update_shape() -> Result<()> {
let route = LocalRoute::new(
IpPrefix::try_from("2001:db8:1::/48")?,
LocalRouteAttrs::default(),
);
let nh: std::net::Ipv6Addr = "fd00:179::2".parse()?;
let update = bgp_server::advertise::build_announce(
&route,
&bgp_server::advertise::Towards {
local_asn: 4200000001,
next_hop: nh.into(),
four_octet_asn: true,
ibgp: false,
},
)
.expect("ipv6 over ipv6");
assert!(update.announced_routes.is_empty());
assert!(
!update
.path_attributes
.iter()
.any(|a| matches!(a, PathAttribute::NextHop(_)))
);
assert!(
update
.path_attributes
.iter()
.any(|a| matches!(a, PathAttribute::MpReachNlri(m)
if m.family == AfiSafi::IPV6_UNICAST && m.prefixes[0].to_string() == "2001:db8:1::/48"))
);
let _ = Ipv4Addr::LOCALHOST;
Ok(())
}
@@ -0,0 +1,321 @@
//! End-to-end tests for capability negotiation: what the two OPENs agree on
//! decides which routes are sent and accepted, and how they are encoded.
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
use std::time::Duration;
use bgp_integration_tests::test_peer::TestPeer;
use bgp_integration_tests::test_server::TestServer;
use bgp_packet::errors::{BgpError, OpenMsgSubcode};
use bgp_packet::message::{
AsPathAttribute, Message, MpReachNlriPathAttribute, NextHopPathAttribute, NlriNextHop,
OriginPathAttribute, PathAttribute, UpdateMessage,
};
use bgp_packet::open::Capability;
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::{LocalRoute, LocalRouteAttrs};
use bgp_server::config::PeerConfig;
use bgp_server::family::{FamilySet, SUPPORTED};
use eyre::{Result, bail};
const LOCAL_ASN: u32 = 65000;
const PEER_ASN: u32 = 65001;
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1);
const TIMEOUT: Duration = Duration::from_secs(5);
fn prefix(s: &str) -> IpPrefix {
IpPrefix::try_from(s).unwrap()
}
fn only(family: AfiSafi) -> FamilySet {
[family].into_iter().collect()
}
fn four_octet() -> Capability {
Capability::FourByteAsn { asn: PEER_ASN }
}
async fn wait_for_session(server: &TestServer) -> Result<bgp_server::session::SessionHandle> {
Ok(tokio::time::timeout(TIMEOUT, async {
loop {
if let Some(session) = server.registry.list().into_iter().next() {
return session;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?)
}
/// An UPDATE announcing one IPv4 route in the body and one IPv6 route in
/// MP_REACH_NLRI.
fn dual_stack_update() -> UpdateMessage {
UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![PEER_ASN]),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
family: AfiSafi::IPV6_UNICAST,
next_hop: NlriNextHop::Ipv6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)),
prefixes: vec![prefix("2001:db8:1::/48").into()],
}),
],
announced_routes: vec![prefix("192.0.2.0/24").into()],
}
}
/// The session carries the families both OPENs offered, and the server's
/// OPEN offers every family by default.
#[tokio::test]
async fn test_families_are_the_ones_both_sides_offered() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = TestPeer::connect(server.addr).await?;
let open = peer
.handshake_with(
PEER_ASN,
PEER_ID,
90,
vec![
four_octet(),
Capability::multiprotocol(AfiSafi::IPV4_UNICAST),
],
)
.await?;
for family in SUPPORTED {
assert!(
open.capabilities()
.any(|c| *c == Capability::multiprotocol(family)),
"the server's OPEN offers {family}"
);
}
let session = wait_for_session(&server).await?;
assert_eq!(session.families, only(AfiSafi::IPV4_UNICAST));
Ok(())
}
/// A route of a family the peer did not offer is not accepted from it. The
/// rest of the UPDATE is, and the session stays up.
#[tokio::test]
async fn test_routes_of_a_family_not_negotiated_are_not_accepted() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake_with(
PEER_ASN,
PEER_ID,
90,
vec![
four_octet(),
Capability::multiprotocol(AfiSafi::IPV4_UNICAST),
],
)
.await?;
wait_for_session(&server).await?;
peer.send(&Message::Update(dual_stack_update())).await?;
let routes = tokio::time::timeout(TIMEOUT, async {
loop {
let routes = server.received_routes();
if !routes.is_empty() {
return routes;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
// One UPDATE is applied as a whole, so the IPv6 route is not on its way.
assert_eq!(routes.len(), 1);
assert_eq!(routes[0].prefix, prefix("192.0.2.0/24"));
assert_eq!(server.registry.list().len(), 1, "the session survives");
Ok(())
}
/// A route of a family the peer did not offer is not sent to it.
#[tokio::test]
async fn test_routes_of_a_family_not_negotiated_are_not_sent() -> Result<()> {
let server = TestServer::start_with(LOCAL_ASN, |c| {
c.originate = vec![LocalRoute::new(
prefix("10.100.0.0/16"),
LocalRouteAttrs::default(),
)];
})
.await?;
// This peer wants IPv6 only; the server has an IPv4 route.
let mut v6_only = TestPeer::connect(server.addr).await?;
v6_only
.handshake_with(
PEER_ASN,
PEER_ID,
90,
vec![
four_octet(),
Capability::multiprotocol(AfiSafi::IPV6_UNICAST),
],
)
.await?;
let session = wait_for_session(&server).await?;
assert_eq!(session.families, only(AfiSafi::IPV6_UNICAST));
// A second peer that takes IPv4 is sent the route. The advertiser
// handles sessions in the order they came up, so by the time this one
// has its route the first has been dealt with.
let mut dual = TestPeer::connect(server.addr).await?;
dual.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90)
.await?;
let update = tokio::time::timeout(TIMEOUT, dual.recv_update()).await??;
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
let nothing = tokio::time::timeout(Duration::from_millis(200), v6_only.recv_update()).await;
assert!(nothing.is_err(), "got {nothing:?}");
Ok(())
}
/// A configured peer is offered only the families configured for it, and a
/// peer that shares none of them is refused with the capability we needed.
#[tokio::test]
async fn test_no_family_in_common_refuses_the_session() -> Result<()> {
let server = TestServer::start_with(LOCAL_ASN, |c| {
// Inbound connections from this address inherit the peer's settings.
c.peers = vec![PeerConfig {
name: "v6-only".into(),
endpoint: SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1),
remote_asn: PEER_ASN,
hold_time_secs: 90,
local_address: None,
families: only(AfiSafi::IPV6_UNICAST),
add_path: None,
}];
// Keep the dialer out of the way.
c.connect_retry = Duration::from_secs(3600);
})
.await?;
let mut peer = TestPeer::connect(server.addr).await?;
peer.send_open_with(
PEER_ASN,
PEER_ID,
90,
vec![
four_octet(),
Capability::multiprotocol(AfiSafi::IPV4_UNICAST),
],
)
.await?;
let open = peer.recv_open().await?;
let offered: Vec<_> = open
.capabilities()
.filter(|c| matches!(c, Capability::MultiProtocol { .. }))
.collect();
assert_eq!(offered, [&Capability::multiprotocol(AfiSafi::IPV6_UNICAST)]);
match tokio::time::timeout(TIMEOUT, peer.recv()).await?? {
Message::Notification(n) => {
assert_eq!(
n.error,
BgpError::OpenMsg(OpenMsgSubcode::UnsupportedCapability)
);
// The Multiprotocol capability for IPv6 unicast.
assert_eq!(n.data, [1, 4, 0, 2, 0, 1]);
}
other => bail!("expected NOTIFICATION, got {other:?}"),
}
assert!(server.registry.list().is_empty());
Ok(())
}
/// Real routers offer families we do not support. That must not stop
/// the families we do share from working.
#[tokio::test]
async fn test_a_peer_offering_an_unsupported_family_is_still_accepted() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake_with(
PEER_ASN,
PEER_ID,
90,
vec![
four_octet(),
Capability::multiprotocol(AfiSafi::new(Afi::L2VPN, Safi::EVPN)),
Capability::multiprotocol(AfiSafi::IPV4_UNICAST),
],
)
.await?;
let session = wait_for_session(&server).await?;
assert_eq!(session.families, only(AfiSafi::IPV4_UNICAST));
assert!(
session.four_octet_asn,
"capabilities after it are still read"
);
Ok(())
}
/// A peer that offers no capabilities at all is a plain RFC 4271 speaker:
/// IPv4 unicast, two-octet AS numbers. Our four-octet AS number reaches it
/// as AS_TRANS in AS_PATH and in full in AS4_PATH (RFC 6793 §4.2.2).
#[tokio::test]
async fn test_a_peer_without_capabilities_gets_two_octet_as_paths() -> Result<()> {
const WIDE_ASN: u32 = 4_200_000_001;
let server = TestServer::start_with(WIDE_ASN, |c| {
c.originate = vec![LocalRoute::new(
prefix("10.100.0.0/16"),
LocalRouteAttrs::default(),
)];
})
.await?;
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake_with(PEER_ASN, PEER_ID, 90, vec![]).await?;
let session = wait_for_session(&server).await?;
assert!(!session.four_octet_asn);
assert_eq!(session.families, only(AfiSafi::IPV4_UNICAST));
// The test peer now decodes AS_PATH two octets wide, as such a speaker
// would; an UPDATE encoded four octets wide would not parse.
let update = tokio::time::timeout(TIMEOUT, peer.recv_update()).await??;
assert_eq!(update.announced_routes, vec![prefix("10.100.0.0/16")]);
assert!(
update
.path_attributes
.contains(&AsPathAttribute::from_asns(vec![23456])),
"{update:?}"
);
let as4_path = update.path_attributes.iter().find_map(|a| match a {
PathAttribute::As4Path(path) => Some(path.segments[0].path.clone()),
_ => None,
});
assert_eq!(as4_path, Some(vec![WIDE_ASN]));
// And its own two-octet paths are read correctly.
peer.send(&Message::Update(UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![PEER_ASN, 64512]),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
],
announced_routes: vec![prefix("192.0.2.0/24").into()],
}))
.await?;
let routes = tokio::time::timeout(TIMEOUT, async {
loop {
let routes = server.received_routes();
if !routes.is_empty() {
return routes;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
assert!(
routes[0]
.attributes
.contains(&AsPathAttribute::from_asns(vec![PEER_ASN, 64512]))
);
Ok(())
}
@@ -0,0 +1,228 @@
//! End-to-end tests for errors in UPDATE messages (RFC 7606): most of them
//! cost the routes in the UPDATE, not the session.
use std::net::Ipv4Addr;
use std::time::Duration;
use bgp_integration_tests::test_peer::TestPeer;
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_server::metrics::UPDATE_ERRORS;
use eyre::{Result, bail};
const LOCAL_ASN: u32 = 65000;
const PEER_ASN: u32 = 65001;
const TIMEOUT: Duration = Duration::from_secs(5);
const WELL_KNOWN: u8 = 0x40;
const OPTIONAL_TRANSITIVE: u8 = 0xc0;
fn attr(flags: u8, type_code: u8, value: &[u8]) -> Vec<u8> {
let mut wire = vec![flags, type_code, value.len() as u8];
wire.extend_from_slice(value);
wire
}
fn origin() -> Vec<u8> {
attr(WELL_KNOWN, 1, &[0])
}
/// AS_SEQUENCE of the peer's AS, four octets wide.
fn as_path() -> Vec<u8> {
attr(WELL_KNOWN, 2, &[2, 1, 0, 0, 0xfd, 0xe9])
}
fn next_hop() -> Vec<u8> {
attr(WELL_KNOWN, 3, &[192, 0, 2, 1])
}
/// A whole UPDATE message announcing `nlri` with `attributes`, as bytes, so
/// that it can be wrong in ways the encoder would not write.
fn update(attributes: &[Vec<u8>], nlri: &[u8]) -> Vec<u8> {
let attributes = attributes.concat();
let mut body = vec![0, 0];
body.extend_from_slice(&(attributes.len() as u16).to_be_bytes());
body.extend_from_slice(&attributes);
body.extend_from_slice(nlri);
let mut wire = vec![0xff; 16];
wire.extend_from_slice(&((19 + body.len()) as u16).to_be_bytes());
wire.push(2);
wire.extend_from_slice(&body);
wire
}
/// 10.1.0.0/16 and 10.2.0.0/16.
const ROUTE_A: [u8; 3] = [16, 10, 1];
const ROUTE_B: [u8; 3] = [16, 10, 2];
fn prefix(s: &str) -> IpPrefix {
IpPrefix::try_from(s).unwrap()
}
async fn established(server: &TestServer, asn: u32) -> Result<TestPeer> {
let mut peer = TestPeer::connect(server.addr).await?;
peer.handshake(asn, Ipv4Addr::new(10, 0, 0, 1), 90).await?;
Ok(peer)
}
/// Waits until the server's Adj-RIB-In holds exactly `prefixes`.
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.received_routes();
routes.sort_by_key(|r| r.prefix.to_string());
if routes.iter().map(|r| &r.prefix).eq(expected.iter()) {
return routes;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?)
}
/// The situation RFC 7606 exists for: a route is in the table, and then an
/// UPDATE for it arrives with a broken attribute. The route goes; the
/// session, and the other routes learned over it, stay.
#[tokio::test]
async fn test_a_malformed_attribute_withdraws_the_route_and_keeps_the_session() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = established(&server, PEER_ASN).await?;
let handled_before = UPDATE_ERRORS
.with_label_values(&["treat-as-withdraw"])
.get();
let good = [origin(), as_path(), next_hop()];
peer.send_raw(&update(&good, &[ROUTE_A, ROUTE_B].concat()))
.await?;
wait_for_routes(&server, &["10.1.0.0/16", "10.2.0.0/16"]).await?;
// ORIGIN with a value that is not defined.
let bad = [attr(WELL_KNOWN, 1, &[7]), as_path(), next_hop()];
peer.send_raw(&update(&bad, &ROUTE_A)).await?;
wait_for_routes(&server, &["10.2.0.0/16"]).await?;
// The session goes on: the route can be announced again.
peer.send_raw(&update(&good, &ROUTE_A)).await?;
wait_for_routes(&server, &["10.1.0.0/16", "10.2.0.0/16"]).await?;
assert_eq!(server.registry.list().len(), 1);
assert!(
UPDATE_ERRORS
.with_label_values(&["treat-as-withdraw"])
.get()
> handled_before
);
Ok(())
}
/// An announcement without a mandatory attribute is not a route we can use.
#[tokio::test]
async fn test_a_route_without_as_path_is_not_accepted() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = established(&server, PEER_ASN).await?;
peer.send_raw(&update(&[origin(), next_hop()], &ROUTE_A))
.await?;
// A well-formed one after it shows that the first was dealt with.
peer.send_raw(&update(&[origin(), as_path(), next_hop()], &ROUTE_B))
.await?;
wait_for_routes(&server, &["10.2.0.0/16"]).await?;
assert_eq!(server.registry.list().len(), 1);
Ok(())
}
/// A broken attribute that plays no part in choosing a route is dropped,
/// and the route is kept.
#[tokio::test]
async fn test_a_malformed_aggregator_is_dropped_and_the_route_kept() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = established(&server, PEER_ASN).await?;
let attributes = [
origin(),
as_path(),
next_hop(),
// AGGREGATOR is eight octets on this session.
attr(OPTIONAL_TRANSITIVE, 7, &[0, 0, 0xfd, 0xe9, 192]),
];
peer.send_raw(&update(&attributes, &ROUTE_A)).await?;
let routes = wait_for_routes(&server, &["10.1.0.0/16"]).await?;
assert_eq!(routes[0].attributes.len(), 3);
assert!(
!routes[0]
.attributes
.iter()
.any(|a| matches!(a, PathAttribute::Aggregator(_)))
);
Ok(())
}
/// LOCAL_PREF is a matter between routers of one AS. From an external peer
/// it is dropped; from an internal one it is kept.
#[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: &[ReceivedRoute]| {
routes[0]
.attributes
.contains(&PathAttribute::LocalPref(LocalPrefPathAttribute(200)))
};
let server = TestServer::start(LOCAL_ASN).await?;
let mut external = established(&server, PEER_ASN).await?;
let attributes = [origin(), as_path(), next_hop(), local_pref.clone()];
external.send_raw(&update(&attributes, &ROUTE_A)).await?;
let routes = wait_for_routes(&server, &["10.1.0.0/16"]).await?;
assert!(!has_local_pref(&routes));
let server = TestServer::start(LOCAL_ASN).await?;
let mut internal = established(&server, LOCAL_ASN).await?;
// An internal peer has not added its AS to the path.
let attributes = [origin(), attr(WELL_KNOWN, 2, &[]), next_hop(), local_pref];
internal.send_raw(&update(&attributes, &ROUTE_A)).await?;
let routes = wait_for_routes(&server, &["10.1.0.0/16"]).await?;
assert!(has_local_pref(&routes));
Ok(())
}
/// When the prefixes themselves cannot be read there is nothing to
/// withdraw, and the session is reset as before.
#[tokio::test]
async fn test_an_unreadable_prefix_still_resets_the_session() -> Result<()> {
let server = TestServer::start(LOCAL_ASN).await?;
let mut peer = established(&server, PEER_ASN).await?;
// An IPv4 prefix of length 33.
peer.send_raw(&update(
&[origin(), as_path(), next_hop()],
&[33, 10, 1, 2, 3, 4],
))
.await?;
loop {
match tokio::time::timeout(TIMEOUT, peer.recv()).await?? {
Message::KeepAlive => {}
Message::Notification(n) => {
assert_eq!(
n.error,
BgpError::UpdateMsg(UpdateMsgSubcode::InvalidNetworkField)
);
break;
}
other => bail!("expected NOTIFICATION, got {other:?}"),
}
}
tokio::time::timeout(TIMEOUT, async {
while !server.registry.list().is_empty() {
tokio::time::sleep(Duration::from_millis(10)).await;
}
})
.await?;
Ok(())
}
+9 -3
View File
@@ -1,7 +1,7 @@
[package]
name = "bgp-packet"
description.workspace = true
description = "Parsing and serialisation of BGP messages"
authors.workspace = true
edition.workspace = true
homepage.workspace = true
license.workspace = true
@@ -11,10 +11,16 @@ version.workspace = true
[dependencies]
bitfield.workspace = true
bytes.workspace = true
eyre.workspace = true
nom.workspace = true
serde.workspace = true
serde_repr.workspace = true
thiserror.workspace = true
strum.workspace = true
bgp-primitives.workspace = true
[dev-dependencies]
eyre.workspace = true
proptest.workspace = true
[lints]
workspace = true
+98
View File
@@ -0,0 +1,98 @@
//! Parsing and formatting of community values as text, the way they are
//! written in configuration and shown in status output.
//!
//! Standard communities are written `ASN:VALUE` (e.g. `65000:100`) or as one
//! of the well-known names from RFC 1997 / RFC 7999 / RFC 8326. Large
//! communities are written `GLOBAL:LOCAL1:LOCAL2` (RFC 8092).
use thiserror::Error;
use crate::message::LargeCommunity;
/// Text that is not a community.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum CommunityError {
#[error("community {0:?} must be ASN:VALUE with 16-bit numbers, or a well-known name")]
Standard(String),
#[error("large community {0:?} must be GLOBAL:LOCAL1:LOCAL2 with 32-bit numbers")]
Large(String),
}
/// Well-known standard communities, by name.
pub const WELL_KNOWN: &[(&str, (u16, u16))] = &[
("GRACEFUL_SHUTDOWN", (0xFFFF, 0x0000)),
("BLACKHOLE", (0xFFFF, 0x029A)),
("NO_EXPORT", (0xFFFF, 0xFF01)),
("NO_ADVERTISE", (0xFFFF, 0xFF02)),
("NO_EXPORT_SUBCONFED", (0xFFFF, 0xFF03)),
("NOPEER", (0xFFFF, 0xFF04)),
];
pub fn parse_standard(s: &str) -> Result<(u16, u16), CommunityError> {
let s = s.trim();
let invalid = || CommunityError::Standard(s.to_string());
if let Some((_, value)) = WELL_KNOWN
.iter()
.find(|(name, _)| name.eq_ignore_ascii_case(s))
{
return Ok(*value);
}
let (high, low) = s.split_once(':').ok_or_else(invalid)?;
let parse = |part: &str| part.trim().parse::<u16>().map_err(|_| invalid());
Ok((parse(high)?, parse(low)?))
}
pub fn format_standard((high, low): (u16, u16)) -> String {
format!("{high}:{low}")
}
pub fn parse_large(s: &str) -> Result<LargeCommunity, CommunityError> {
let s = s.trim();
let invalid = || CommunityError::Large(s.to_string());
let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 3 {
return Err(invalid());
}
let parse = |part: &str| part.trim().parse::<u32>().map_err(|_| invalid());
Ok(LargeCommunity {
global_admin: parse(parts[0])?,
data_1: parse(parts[1])?,
data_2: parse(parts[2])?,
})
}
pub fn format_large(c: &LargeCommunity) -> String {
format!("{}:{}:{}", c.global_admin, c.data_1, c.data_2)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn standard_roundtrip_and_names() {
assert_eq!(parse_standard("65000:100").unwrap(), (65000, 100));
assert_eq!(parse_standard(" no_export ").unwrap(), (65535, 65281));
assert_eq!(parse_standard("BLACKHOLE").unwrap(), (65535, 666));
assert_eq!(format_standard((65000, 100)), "65000:100");
assert!(parse_standard("65000").is_err());
assert!(parse_standard("70000:1").is_err());
assert!(parse_standard("bogus").is_err());
}
#[test]
fn large_roundtrip() {
let c = parse_large("65000:1:2").unwrap();
assert_eq!(
c,
LargeCommunity {
global_admin: 65000,
data_1: 1,
data_2: 2
}
);
assert_eq!(format_large(&c), "65000:1:2");
assert!(parse_large("65000:1").is_err());
assert!(parse_large("a:b:c").is_err());
}
}
+68 -95
View File
@@ -1,68 +1,79 @@
use serde::{Deserialize, Serialize};
use serde::Serialize;
use strum::FromRepr;
use thiserror::Error;
#[derive(Debug, Error)]
#[error("Unknown BGP error code {code} subcode {subcode}")]
pub struct UnknownErrorCode {
pub code: u8,
pub subcode: u8,
/// A field holds a number that is not one of the values defined for it.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("invalid {what} value {value}")]
pub struct InvalidValue {
pub what: &'static str,
pub value: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize, Deserialize)]
#[repr(u8)]
/// A message that could not be decoded, together with the NOTIFICATION that
/// reports the problem to the peer (RFC 4271 §6).
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("{detail}")]
pub struct DecodeError {
/// The error code and subcode to send.
pub notification: BgpError,
/// The Data field of that NOTIFICATION, where the RFC defines one.
pub data: Vec<u8>,
/// What was wrong, for the log.
pub detail: String,
}
/// The error a NOTIFICATION reports: its code and subcode (RFC 4271 §4.5).
#[derive(Debug, Clone, PartialEq, Eq, Error, Serialize)]
pub enum BgpError {
#[error("Message header error")]
MsgHeader(MsgHeaderSubcode) = 1,
#[error("Message header error: {0}")]
MsgHeader(MsgHeaderSubcode),
#[error("Open message error")]
OpenMsg(OpenMsgSubcode) = 2,
#[error("Open message error: {0}")]
OpenMsg(OpenMsgSubcode),
#[error("Update message error")]
UpdateMsg(UpdateMsgSubcode) = 3,
#[error("Update message error: {0}")]
UpdateMsg(UpdateMsgSubcode),
#[error("Hold timer expired")]
HoldTimer = 4,
HoldTimer,
#[error("Finite state machine error")]
FsmError(FsmSubcode) = 5,
#[error("Finite state machine error: {0}")]
FsmError(FsmSubcode),
#[error("Cease")]
Cease(CeaseSubcode) = 6,
#[error("Cease: {0}")]
Cease(CeaseSubcode),
#[error("Route refresh message error")]
RouteRefresh(RouteRefreshSubcode) = 7,
#[error("Route refresh message error: {0}")]
RouteRefresh(RouteRefreshSubcode),
#[error("Send hold timer expired")]
SendHoldTimer = 8,
SendHoldTimer,
/// A code or subcode this implementation has no name for. A peer may
/// send any: new ones are assigned, and zero ("unspecific") is always
/// allowed. The session ends the same way, and the numbers are kept
/// for the log.
#[error("error code {code} subcode {subcode}")]
Other { code: u8, subcode: u8 },
}
impl BgpError {
pub fn from_code_subcode(code: u8, subcode: u8) -> Result<Self, UnknownErrorCode> {
let err = || UnknownErrorCode { code, subcode };
match code {
1 => Ok(Self::MsgHeader(
MsgHeaderSubcode::try_from(subcode).map_err(|_| err())?,
)),
2 => Ok(Self::OpenMsg(
OpenMsgSubcode::try_from(subcode).map_err(|_| err())?,
)),
3 => Ok(Self::UpdateMsg(
UpdateMsgSubcode::try_from(subcode).map_err(|_| err())?,
)),
4 => Ok(Self::HoldTimer),
5 => Ok(Self::FsmError(
FsmSubcode::try_from(subcode).map_err(|_| err())?,
)),
6 => Ok(Self::Cease(
CeaseSubcode::try_from(subcode).map_err(|_| err())?,
)),
7 => Ok(Self::RouteRefresh(
RouteRefreshSubcode::try_from(subcode).map_err(|_| err())?,
)),
8 => Ok(Self::SendHoldTimer),
_ => Err(err()),
}
/// The error for a code and subcode off the wire. Every pair has one,
/// so that a peer's NOTIFICATION can always be read.
pub fn from_code_subcode(code: u8, subcode: u8) -> Self {
let known = match code {
1 => MsgHeaderSubcode::from_repr(subcode).map(Self::MsgHeader),
2 => OpenMsgSubcode::from_repr(subcode).map(Self::OpenMsg),
3 => UpdateMsgSubcode::from_repr(subcode).map(Self::UpdateMsg),
4 => (subcode == 0).then_some(Self::HoldTimer),
5 => FsmSubcode::from_repr(subcode).map(Self::FsmError),
6 => CeaseSubcode::from_repr(subcode).map(Self::Cease),
7 => RouteRefreshSubcode::from_repr(subcode).map(Self::RouteRefresh),
8 => (subcode == 0).then_some(Self::SendHoldTimer),
_ => None,
};
known.unwrap_or(Self::Other { code, subcode })
}
pub fn code_subcode(&self) -> (u8, u8) {
@@ -75,11 +86,12 @@ impl BgpError {
Self::Cease(s) => (6, *s as u8),
Self::RouteRefresh(s) => (7, *s as u8),
Self::SendHoldTimer => (8, 0),
Self::Other { code, subcode } => (*code, *subcode),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, Deserialize, FromRepr)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, FromRepr)]
#[repr(u8)]
pub enum MsgHeaderSubcode {
#[error("Connection not synchronized")]
@@ -92,16 +104,12 @@ pub enum MsgHeaderSubcode {
BadMessageType = 3,
}
impl TryFrom<u8> for MsgHeaderSubcode {
type Error = ();
fn try_from(v: u8) -> Result<Self, ()> {
Self::from_repr(v).ok_or(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, Deserialize, FromRepr)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, FromRepr)]
#[repr(u8)]
pub enum OpenMsgSubcode {
#[error("Unspecific")]
Unspecific = 0,
#[error("Unsupported BGP version number")]
UnsupportedVersion = 1,
@@ -124,14 +132,7 @@ pub enum OpenMsgSubcode {
RoleMismatch = 8,
}
impl TryFrom<u8> for OpenMsgSubcode {
type Error = ();
fn try_from(v: u8) -> Result<Self, ()> {
Self::from_repr(v).ok_or(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, Deserialize, FromRepr)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, FromRepr)]
#[repr(u8)]
pub enum UpdateMsgSubcode {
#[error("Malformed attribute list")]
@@ -165,14 +166,7 @@ pub enum UpdateMsgSubcode {
MalformedAsPath = 11,
}
impl TryFrom<u8> for UpdateMsgSubcode {
type Error = ();
fn try_from(v: u8) -> Result<Self, ()> {
Self::from_repr(v).ok_or(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, Deserialize, FromRepr)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, FromRepr)]
#[repr(u8)]
pub enum FsmSubcode {
#[error("Received an unexpected message in OpenSent state")]
@@ -185,14 +179,7 @@ pub enum FsmSubcode {
UnexpectedEstablished = 3,
}
impl TryFrom<u8> for FsmSubcode {
type Error = ();
fn try_from(v: u8) -> Result<Self, ()> {
Self::from_repr(v).ok_or(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, Deserialize, FromRepr)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, FromRepr)]
#[repr(u8)]
pub enum CeaseSubcode {
#[error("Maximum number of prefixes reached")]
@@ -226,23 +213,9 @@ pub enum CeaseSubcode {
BfdDown = 10,
}
impl TryFrom<u8> for CeaseSubcode {
type Error = ();
fn try_from(v: u8) -> Result<Self, ()> {
Self::from_repr(v).ok_or(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, Deserialize, FromRepr)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error, Serialize, FromRepr)]
#[repr(u8)]
pub enum RouteRefreshSubcode {
#[error("Invalid message length")]
InvalidLength = 1,
}
impl TryFrom<u8> for RouteRefreshSubcode {
type Error = ();
fn try_from(v: u8) -> Result<Self, ()> {
Self::from_repr(v).ok_or(())
}
}
+2
View File
@@ -1,3 +1,5 @@
pub mod community;
pub mod errors;
pub mod message;
pub mod open;
pub mod update_decode;
+1321 -1209
View File
File diff suppressed because it is too large. Load diff
+888 -583
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+674
View File
@@ -0,0 +1,674 @@
//! Property tests for the wire codec.
//!
//! Two things are checked for every kind of message:
//!
//! - **Round trip.** A message that can be written can be read back, comes
//! back equal.
//! - **Hostile input.** Whatever bytes arrive, decoding returns a message or
//! an error: it never panics. What it does accept can be shown, written
//! again, and read back as the same message.
//!
//! The second is what a peer can do to us, so it is run on three kinds of
//! input: plain noise, noise behind a valid header (so it reaches the body
//! parsers), and valid messages with a few bytes damaged (so it reaches deep
//! into them).
use std::net::{Ipv4Addr, Ipv6Addr};
use bgp_packet::errors::BgpError;
use bgp_packet::message::{
AggregatorPathAttribute, AsPathAttribute, AsPathSegment, AtomicAggregatePathAttribute,
BGP_HEADER_LEN, BGP_MAX_MSG_LEN, CommunitiesPathAttribute, ExtendedCommunitiesPathAttribute,
ExtendedCommunity, LargeCommunitiesPathAttribute, LargeCommunity, LocalPrefPathAttribute,
Message, MpReachNlriPathAttribute, MpUnreachNlriPathAttribute, MultiExitDiscPathAttribute,
NextHopPathAttribute, NlriNextHop, NotificationMessage, OriginPathAttribute, PathAttribute,
PathAttributeFlags, RouteRefreshMessage, UpdateMessage,
};
use bgp_packet::open::{
AddPathEntry, AddPathSendRecv, BgpRole, BgpSecHeader, Capability, ExtendedNextHop,
GracefulRestartEntry, LongLivedGracefulRestart, MultiLabelEntry, OpenMessage, OpenOption,
OrfFamily, OrfSendRecv, OrfType, OutboundRouteFilterEntry,
};
use bgp_primitives::constants::{Afi, AfiSafi, IpVersion, Safi};
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
use bgp_primitives::parser::{EncodeError, ParserContext};
use bytes::BytesMut;
use proptest::collection::vec;
use proptest::prelude::*;
/// What a session settled that the codec depends on.
#[derive(Debug, Clone, Copy)]
struct Session {
four_octet_asn: bool,
ibgp: bool,
/// Whether prefixes carry path identifiers (RFC 7911), for IPv4 and
/// for IPv6 unicast.
add_path: [bool; 2],
}
impl Session {
fn ctx(self) -> ParserContext {
ParserContext {
four_octet_asn: self.four_octet_asn,
ibgp: self.ibgp,
add_path: [AfiSafi::IPV4_UNICAST, AfiSafi::IPV6_UNICAST]
.into_iter()
.zip(self.add_path)
.filter_map(|(family, in_use)| in_use.then_some(family))
.collect(),
}
}
fn has_add_path(self, version: IpVersion, safi: Safi) -> bool {
safi == Safi::UNICAST
&& match version {
IpVersion::Ipv4 => self.add_path[0],
IpVersion::Ipv6 => self.add_path[1],
}
}
}
fn session() -> impl Strategy<Value = Session> {
any::<(bool, bool, [bool; 2])>().prop_map(|(four_octet_asn, ibgp, add_path)| Session {
four_octet_asn,
ibgp,
add_path,
})
}
// ---------------------------------------------------------------------------
// Generators
// ---------------------------------------------------------------------------
fn prefix(version: IpVersion) -> impl Strategy<Value = IpPrefix> {
let max_len = match version {
IpVersion::Ipv4 => 32u8,
IpVersion::Ipv6 => 128,
};
(0..=max_len, any::<[u8; 16]>()).prop_map(move |(length, bytes)| {
IpPrefix::new(version, bytes, length).expect("16 bytes are enough for any prefix")
})
}
/// Prefixes as an UPDATE names them. Without ADD-PATH for the family there
/// is no identifier on the wire, and it reads back as zero.
fn nlri(session: Session, version: IpVersion, safi: Safi) -> impl Strategy<Value = Vec<Nlri>> {
let with_path_id = session.has_add_path(version, safi);
vec(
(prefix(version), any::<u32>()).prop_map(move |(prefix, path_id)| Nlri {
prefix,
path_id: if with_path_id { path_id } else { 0 },
}),
0..5,
)
}
fn afi() -> impl Strategy<Value = Afi> {
any::<u16>().prop_map(Afi)
}
fn safi() -> impl Strategy<Value = Safi> {
any::<u8>().prop_map(Safi)
}
/// AS numbers that fit the session: any on a four-octet one, two-octet ones
/// otherwise (wider ones are replaced by AS_TRANS on the wire, by design).
fn asn(four_octet_asn: bool) -> BoxedStrategy<u32> {
if four_octet_asn {
any::<u32>().boxed()
} else {
any::<u16>().prop_map(u32::from).boxed()
}
}
fn as_path(asn: BoxedStrategy<u32>) -> impl Strategy<Value = AsPathAttribute> {
// A segment has at least one AS number.
vec((any::<bool>(), vec(asn, 1..6)), 0..4).prop_map(|segments| AsPathAttribute {
segments: segments
.into_iter()
.map(|(ordered, path)| AsPathSegment { ordered, path })
.collect(),
})
}
fn extended_community() -> impl Strategy<Value = ExtendedCommunity> {
any::<[u8; 8]>().prop_map(ExtendedCommunity)
}
fn mp_reach(session: Session) -> impl Strategy<Value = MpReachNlriPathAttribute> {
let ip_safi = || prop_oneof![Just(Safi::UNICAST), Just(Safi::MULTICAST)];
let v4 = ip_safi()
.prop_flat_map(move |safi| {
(
Just(safi),
any::<[u8; 4]>(),
nlri(session, IpVersion::Ipv4, safi),
)
})
.prop_map(|(safi, next_hop, prefixes)| MpReachNlriPathAttribute {
family: AfiSafi::new(Afi::IPV4, safi),
next_hop: NlriNextHop::Ipv4(Ipv4Addr::from(next_hop)),
prefixes,
});
let v6_next_hop = || {
prop_oneof![
any::<[u8; 16]>().prop_map(|a| NlriNextHop::Ipv6(Ipv6Addr::from(a))),
(any::<[u8; 16]>(), any::<[u8; 16]>()).prop_map(|(global, link_local)| {
NlriNextHop::Ipv6WithLl {
global: Ipv6Addr::from(global),
link_local: Ipv6Addr::from(link_local),
}
}),
]
};
let v6 = ip_safi()
.prop_flat_map(move |safi| {
(
Just(safi),
v6_next_hop(),
nlri(session, IpVersion::Ipv6, safi),
)
})
.prop_map(|(safi, next_hop, prefixes)| MpReachNlriPathAttribute {
family: AfiSafi::new(Afi::IPV6, safi),
next_hop,
prefixes,
});
prop_oneof![v4, v6]
}
fn mp_unreach(session: Session) -> impl Strategy<Value = MpUnreachNlriPathAttribute> {
let of = move |version: IpVersion| {
nlri(session, version, Safi::UNICAST).prop_map(move |prefixes| MpUnreachNlriPathAttribute {
family: AfiSafi::new(version.into(), Safi::UNICAST),
prefixes,
})
};
prop_oneof![of(IpVersion::Ipv4), of(IpVersion::Ipv6)]
}
/// Type codes whose definition the decoder knows; an unknown attribute has
/// none of these.
const DEFINED_ATTRIBUTES: [u8; 16] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 14, 15, 16, 17, 18, 32];
/// Unknown attributes, each with a type code of its own: a repeated
/// attribute is dropped by the decoder.
fn unknown_attributes() -> impl Strategy<Value = Vec<PathAttribute>> {
proptest::collection::btree_map(
any::<u8>().prop_filter("a type code without a definition", |code| {
!DEFINED_ATTRIBUTES.contains(code)
}),
(any::<u8>(), vec(any::<u8>(), 0..300)),
0..3,
)
.prop_map(|attributes| {
attributes
.into_iter()
.map(|(type_code, (flags, payload))| {
// Optional (bit 7): an unknown well-known attribute is an
// error. Bit 4 selects the two-octet length, which a payload
// this long needs.
let mut flags = flags | 0x80;
if payload.len() > 255 {
flags |= 0x10;
}
PathAttribute::UnknownPathAttribute {
flags: PathAttributeFlags(flags),
type_code,
payload,
}
})
.collect()
})
}
/// The attributes of a well-formed UPDATE on `session`: the mandatory ones,
/// and any of the others at most once.
fn path_attributes(session: Session) -> impl Strategy<Value = Vec<PathAttribute>> {
let mandatory = (
prop_oneof![
Just(OriginPathAttribute::IGP),
Just(OriginPathAttribute::EGP),
Just(OriginPathAttribute::INCOMPLETE)
],
as_path(asn(session.four_octet_asn)),
any::<[u8; 4]>(),
any::<u32>(),
)
.prop_map(move |(origin, as_path, next_hop, local_pref)| {
let mut attributes = vec![
PathAttribute::Origin(origin),
PathAttribute::AsPath(as_path),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::from(next_hop))),
];
// Mandatory between internal peers, dropped from external ones.
if session.ibgp {
attributes.push(PathAttribute::LocalPref(LocalPrefPathAttribute(local_pref)));
}
attributes
});
let optional = (
proptest::option::of(
any::<u32>().prop_map(|v| PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(v))),
),
proptest::option::of(Just(PathAttribute::AtomicAggregate(
AtomicAggregatePathAttribute {},
))),
proptest::option::of((asn(session.four_octet_asn), any::<[u8; 4]>()).prop_map(
|(asn, ip)| {
PathAttribute::Aggregator(AggregatorPathAttribute {
asn,
ip: Ipv4Addr::from(ip),
})
},
)),
proptest::option::of(
vec(any::<(u16, u16)>(), 1..8)
.prop_map(|c| PathAttribute::Communities(CommunitiesPathAttribute(c))),
),
proptest::option::of(mp_reach(session).prop_map(PathAttribute::MpReachNlri)),
proptest::option::of(mp_unreach(session).prop_map(PathAttribute::MpUnreachNlri)),
proptest::option::of(
vec(extended_community(), 1..6).prop_map(|extended_communities| {
PathAttribute::ExtendedCommunities(ExtendedCommunitiesPathAttribute {
extended_communities,
})
}),
),
// AS4_PATH is four octets wide, and only a two-octet speaker sends
// it.
proptest::option::of(as_path(any::<u32>().boxed()).prop_map(PathAttribute::As4Path))
.prop_map(move |as4_path| as4_path.filter(|_| !session.four_octet_asn)),
proptest::option::of(vec(any::<(u32, u32, u32)>(), 1..6).prop_map(|communities| {
PathAttribute::LargeCommunities(LargeCommunitiesPathAttribute {
communities: communities
.into_iter()
.map(|(global_admin, data_1, data_2)| LargeCommunity {
global_admin,
data_1,
data_2,
})
.collect(),
})
})),
unknown_attributes(),
);
(mandatory, optional).prop_map(|(mut attributes, optional)| {
let (med, atomic, aggregator, communities, reach, unreach, extended, as4, large, unknown) =
optional;
attributes.extend(
[
med,
atomic,
aggregator,
communities,
reach,
unreach,
extended,
as4,
large,
]
.into_iter()
.flatten(),
);
attributes.extend(unknown);
attributes
})
}
fn update(session: Session) -> impl Strategy<Value = UpdateMessage> {
(
nlri(session, IpVersion::Ipv4, Safi::UNICAST),
path_attributes(session),
nlri(session, IpVersion::Ipv4, Safi::UNICAST),
)
.prop_map(
|(withdrawn_routes, path_attributes, announced_routes)| UpdateMessage {
withdrawn_routes,
path_attributes,
announced_routes,
},
)
}
/// Capability codes that have their own variant.
const KNOWN_CAPABILITIES: [u8; 13] = [1, 2, 3, 5, 6, 7, 8, 9, 64, 65, 69, 70, 71];
fn capability() -> impl Strategy<Value = Capability> {
let orf = (
any::<u8>().prop_map(OrfType),
prop_oneof![
Just(OrfSendRecv::Receive),
Just(OrfSendRecv::Send),
Just(OrfSendRecv::Both)
],
)
.prop_map(|(r#type, send_recv)| OutboundRouteFilterEntry { r#type, send_recv });
let add_path = prop_oneof![
Just(AddPathSendRecv::Receive),
Just(AddPathSendRecv::Send),
Just(AddPathSendRecv::Both)
];
prop_oneof![
(afi(), safi()).prop_map(|(afi, safi)| Capability::MultiProtocol { afi, safi }),
Just(Capability::RouteRefresh),
vec((afi(), safi(), vec(orf, 0..4)), 0..3).prop_map(|families| {
Capability::OutboundRouteFilter(
families
.into_iter()
.map(|(afi, safi, orfs)| OrfFamily { afi, safi, orfs })
.collect(),
)
}),
vec((afi(), safi(), afi()), 1..4).prop_map(|entries| Capability::ExtendedNextHop {
entries: entries
.into_iter()
.map(|(afi, safi, nh_afi)| ExtendedNextHop { afi, safi, nh_afi })
.collect(),
}),
Just(Capability::ExtendedMessage),
(any::<u8>(), afi()).prop_map(|(header, afi)| Capability::BgpSec {
header: BgpSecHeader(header),
afi,
}),
vec((afi(), safi(), any::<u8>()), 1..4).prop_map(|entries| {
Capability::MultiLabelCompat {
entries: entries
.into_iter()
.map(|(afi, safi, count)| MultiLabelEntry { afi, safi, count })
.collect(),
}
}),
any::<u8>().prop_map(|role| Capability::BgpRole {
role: BgpRole::from(role),
}),
(
any::<bool>(),
0u16..4096,
vec((afi(), safi(), any::<bool>()), 0..4)
)
.prop_map(|(restart_state, restart_time, entries)| {
Capability::GracefulRestart {
restart_state,
restart_time,
entries: entries
.into_iter()
.map(|(afi, safi, preserve_forwarding)| GracefulRestartEntry {
afi,
safi,
preserve_forwarding,
})
.collect(),
}
}),
any::<u32>().prop_map(|asn| Capability::FourByteAsn { asn }),
vec((afi(), safi(), add_path), 1..4).prop_map(|entries| Capability::AddPath {
entries: entries
.into_iter()
.map(|(afi, safi, send_recv)| AddPathEntry {
afi,
safi,
send_recv,
})
.collect(),
}),
Just(Capability::EnhancedRouteRefresh),
vec((afi(), safi(), any::<u8>(), 0u32..(1 << 24)), 0..4).prop_map(|entries| {
Capability::LongLivedGracefulRestart(
entries
.into_iter()
.map(|(afi, safi, flags, stale_time)| LongLivedGracefulRestart {
afi,
safi,
flags,
stale_time,
})
.collect(),
)
}),
(
any::<u8>().prop_filter("a code without a variant", |code| {
!KNOWN_CAPABILITIES.contains(code)
}),
vec(any::<u8>(), 0..20)
)
.prop_map(|(type_code, value)| Capability::UnknownCapability { type_code, value }),
]
}
fn open() -> impl Strategy<Value = OpenMessage> {
(
any::<u16>(),
any::<u16>(),
any::<[u8; 4]>(),
// Enough to go past 255 octets of options now and then, which
// switches to the extended length of RFC 9072.
vec(vec(capability(), 0..10), 0..8),
)
.prop_map(|(asn, hold_time, identifier, options)| OpenMessage {
version: 4,
asn,
hold_time,
identifier: Ipv4Addr::from(identifier),
options: options.into_iter().map(OpenOption::Capabilities).collect(),
})
}
fn notification() -> impl Strategy<Value = NotificationMessage> {
// Any code and subcode: a peer may send ones we have no name for.
(any::<u8>(), any::<u8>(), vec(any::<u8>(), 0..40)).prop_map(|(code, subcode, data)| {
NotificationMessage {
error: BgpError::from_code_subcode(code, subcode),
data,
}
})
}
fn message(session: Session) -> impl Strategy<Value = Message> {
prop_oneof![
open().prop_map(Message::Open),
update(session).prop_map(Message::Update),
notification().prop_map(Message::Notification),
Just(Message::KeepAlive),
(afi(), any::<u8>(), safi()).prop_map(|(afi, subtype, safi)| {
Message::RouteRefresh(RouteRefreshMessage { afi, subtype, safi })
}),
]
}
/// A session, and a well-formed message that fits it.
fn session_and_message() -> impl Strategy<Value = (Session, Message)> {
session().prop_flat_map(|session| (Just(session), message(session)))
}
// ---------------------------------------------------------------------------
// Properties
// ---------------------------------------------------------------------------
/// Decodes `wire` and checks what must hold for anything the decoder
/// 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(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 {
for prefix in update
.withdrawn_routes
.iter()
.chain(&update.announced_routes)
{
let _ = prefix.to_string();
}
for attribute in &update.path_attributes {
match attribute {
PathAttribute::MpReachNlri(mp) => {
mp.prefixes.iter().for_each(|p| drop(p.to_string()))
}
PathAttribute::MpUnreachNlri(mp) => {
mp.prefixes.iter().for_each(|p| drop(p.to_string()))
}
_ => {}
}
}
}
let mut rewritten = BytesMut::new();
match msg.to_wire(ctx, &mut rewritten) {
Ok(()) => {}
// Withdrawing what a full-size UPDATE announced can need more room
// than one message has. The result is acted on, never sent.
Err(EncodeError::TooLong { .. }) if !repairs.is_empty() => return Ok(()),
Err(e) => {
return Err(TestCaseError::fail(format!(
"accepted but cannot be written: {e}"
)));
}
}
if repairs.is_empty() {
let again = Message::decode_with_report(ctx, &rewritten)
.map_err(|e| TestCaseError::fail(format!("written but cannot be read: {e}")))?;
prop_assert!(again.update_errors.is_empty(), "{:?}", again.update_errors);
prop_assert_eq!(again.message, msg);
}
Ok(())
}
/// Writes a generated message. `None` when it is too big for one BGP
/// message, or some part of it for its length field, which the generators
/// can produce and the encoder must refuse.
fn written(ctx: &ParserContext, msg: &Message) -> Option<BytesMut> {
let mut wire = BytesMut::new();
match msg.to_wire(ctx, &mut wire) {
Ok(()) => Some(wire),
Err(EncodeError::TooLong { .. }) => None,
}
}
fn framed(msg_type: u8, body: &[u8]) -> Vec<u8> {
let mut wire = vec![0xff; 16];
let length = u16::try_from(usize::from(BGP_HEADER_LEN) + body.len()).unwrap();
wire.extend_from_slice(&length.to_be_bytes());
wire.push(msg_type);
wire.extend_from_slice(body);
wire
}
/// Ways to damage a message.
#[derive(Debug, Clone)]
enum Damage {
Set { at: prop::sample::Index, to: u8 },
Remove { at: prop::sample::Index },
Insert { at: prop::sample::Index, byte: u8 },
Truncate { to: prop::sample::Index },
}
fn damage() -> impl Strategy<Value = Damage> {
prop_oneof![
(any::<prop::sample::Index>(), any::<u8>()).prop_map(|(at, to)| Damage::Set { at, to }),
any::<prop::sample::Index>().prop_map(|at| Damage::Remove { at }),
(any::<prop::sample::Index>(), any::<u8>())
.prop_map(|(at, byte)| Damage::Insert { at, byte }),
any::<prop::sample::Index>().prop_map(|to| Damage::Truncate { to }),
]
}
/// Applies `damage` to the body of `wire` and makes the header's length
/// right again, so that the damage is the body parsers' problem.
fn damaged(wire: &[u8], damage: &[Damage]) -> Vec<u8> {
let header = usize::from(BGP_HEADER_LEN);
let msg_type = wire[header - 1];
let mut body = wire[header..].to_vec();
for d in damage {
match d {
Damage::Set { at, to } if !body.is_empty() => {
let i = at.index(body.len());
body[i] = *to;
}
Damage::Remove { at } if !body.is_empty() => {
body.remove(at.index(body.len()));
}
Damage::Insert { at, byte } => body.insert(at.index(body.len() + 1), *byte),
Damage::Truncate { to } if !body.is_empty() => body.truncate(to.index(body.len())),
_ => {}
}
}
body.truncate(usize::from(BGP_MAX_MSG_LEN - BGP_HEADER_LEN));
framed(msg_type, &body)
}
/// 2000 cases a property, which takes about a second. For a longer hunt
/// set `PROPTEST_CASES`, e.g. `PROPTEST_CASES=1000000 cargo test --release
/// -p bgp-packet --test properties`.
fn config() -> ProptestConfig {
let cases = std::env::var("PROPTEST_CASES")
.ok()
.and_then(|cases| cases.parse().ok())
.unwrap_or(2000);
ProptestConfig {
cases,
// Failures are not saved to a file: one that is found becomes a
// named test next to the code it broke.
failure_persistence: None,
..ProptestConfig::default()
}
}
proptest! {
#![proptest_config(config())]
#[test]
fn a_message_that_can_be_written_reads_back_equal((session, msg) in session_and_message()) {
let ctx = session.ctx();
let Some(wire) = written(&ctx, &msg) else {
return Ok(());
};
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!(decoded.update_errors.is_empty(), "{:?}", decoded.update_errors);
prop_assert_eq!(&decoded.message, &msg);
}
#[test]
fn noise_is_never_more_than_an_error(session in session(), bytes in vec(any::<u8>(), 0..200)) {
check_decoding(&session.ctx(), &bytes)?;
}
#[test]
fn noise_behind_a_valid_header_is_never_more_than_an_error(
session in session(),
msg_type in 0u8..7,
body in vec(any::<u8>(), 0..300),
) {
check_decoding(&session.ctx(), &framed(msg_type, &body))?;
}
#[test]
fn a_damaged_message_is_never_more_than_an_error(
(session, msg) in session_and_message(),
damage in vec(damage(), 1..4),
) {
let ctx = session.ctx();
let Some(wire) = written(&ctx, &msg) else {
return Ok(());
};
check_decoding(&ctx, &damaged(&wire, &damage))?;
}
/// The parsers below the message level are public too.
#[test]
fn noise_is_never_more_than_an_error_for_the_parts(
session in session(),
bytes in vec(any::<u8>(), 0..120),
) {
let ctx = session.ctx();
let _ = OpenMessage::from_wire(&bytes);
let _ = Capability::from_wire(&bytes);
let _ = UpdateMessage::decode(&ctx, &bytes);
let _ = PathAttribute::from_wire(&ctx, &bytes);
}
}
+7 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "bgp-primitives"
description = "Primitive types for working with routing information"
authors.workspace = true
edition.workspace = true
homepage.workspace = true
license.workspace = true
@@ -11,9 +11,14 @@ version.workspace = true
[dependencies]
bitfield.workspace = true
bytes.workspace = true
eyre.workspace = true
nom.workspace = true
serde.workspace = true
serde_repr.workspace = true
thiserror.workspace = true
strum.workspace = true
[dev-dependencies]
proptest.workspace = true
[lints]
workspace = true
+150 -56
View File
@@ -1,80 +1,174 @@
use std::fmt::Display;
use eyre::bail;
use serde_repr::{Deserialize_repr, Serialize_repr};
use serde::Serialize;
use serde_repr::Serialize_repr;
use thiserror::Error;
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
/// An address family number this implementation has no name for.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[error("unknown address family identifier {0}")]
pub struct UnknownAfi(pub u16);
/// The IP version of a prefix. For the address family field of a capability
/// or an MP_REACH_NLRI, which can name families that are not IP, see [`Afi`].
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Serialize_repr)]
#[repr(u16)]
pub enum AddressFamilyId {
pub enum IpVersion {
Ipv4 = 1,
Ipv6 = 2,
}
impl TryFrom<u16> for AddressFamilyId {
type Error = eyre::Error;
fn try_from(value: u16) -> Result<Self, Self::Error> {
Ok(match value {
x if x == Self::Ipv4 as u16 => Self::Ipv4,
x if x == Self::Ipv6 as u16 => Self::Ipv6,
other => bail!("Unknown AddressFamily: {}", other),
})
}
}
impl Display for AddressFamilyId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl IpVersion {
/// How many bits an address of this version has.
pub fn bits(self) -> u8 {
match self {
AddressFamilyId::Ipv4 => write!(f, "Ipv4"),
AddressFamilyId::Ipv6 => write!(f, "Ipv6"),
Self::Ipv4 => 32,
Self::Ipv6 => 128,
}
}
}
/// Represents a Subsequent Address Family Identifier.
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
#[repr(u8)]
pub enum SubsequentAfi {
Unicast = 1,
Multicast = 2,
NlriWithMpls = 4,
MplsLabelledVpn = 128,
MulticastMplsVpn = 129,
impl TryFrom<u16> for IpVersion {
type Error = UnknownAfi;
fn try_from(value: u16) -> Result<Self, Self::Error> {
match value {
x if x == Self::Ipv4 as u16 => Ok(Self::Ipv4),
x if x == Self::Ipv6 as u16 => Ok(Self::Ipv6),
other => Err(UnknownAfi(other)),
}
}
}
impl TryFrom<u8> for SubsequentAfi {
type Error = eyre::Error;
impl Display for IpVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IpVersion::Ipv4 => write!(f, "Ipv4"),
IpVersion::Ipv6 => write!(f, "Ipv6"),
}
}
}
fn try_from(value: u8) -> Result<Self, Self::Error> {
Ok(match value {
x if x == Self::Unicast as u8 => Self::Unicast,
x if x == Self::Multicast as u8 => Self::Multicast,
x if x == Self::NlriWithMpls as u8 => Self::NlriWithMpls,
x if x == Self::MplsLabelledVpn as u8 => Self::MplsLabelledVpn,
x if x == Self::MulticastMplsVpn as u8 => Self::MulticastMplsVpn,
other => bail!("Unknown SubsequentAfi: {}", other),
/// An Address Family Identifier as it appears on the wire (RFC 4760).
///
/// Any number is a valid value: a peer may name a family this implementation
/// has never heard of, and that must be readable so it can be ignored. Which
/// families are *supported* is a separate question, answered where routes
/// are stored.
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(transparent)]
pub struct Afi(pub u16);
impl Afi {
pub const IPV4: Self = Self(1);
pub const IPV6: Self = Self(2);
pub const L2VPN: Self = Self(25);
pub const BGP_LS: Self = Self(16388);
}
impl From<IpVersion> for Afi {
fn from(version: IpVersion) -> Self {
Self(version as u16)
}
}
impl TryFrom<Afi> for IpVersion {
type Error = UnknownAfi;
fn try_from(afi: Afi) -> Result<Self, Self::Error> {
Self::try_from(afi.0)
}
}
/// A Subsequent Address Family Identifier as it appears on the wire. Like
/// [`Afi`], any number is a valid value.
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(transparent)]
pub struct Safi(pub u8);
impl Safi {
pub const UNICAST: Self = Self(1);
pub const MULTICAST: Self = Self(2);
pub const MPLS_LABEL: Self = Self(4);
pub const EVPN: Self = Self(70);
pub const BGP_LS: Self = Self(71);
pub const MPLS_VPN: Self = Self(128);
pub const MULTICAST_MPLS_VPN: Self = Self(129);
pub const FLOWSPEC: Self = Self(133);
}
/// An address family: the pair that says what kind of routes a capability
/// or an MP_REACH_NLRI is about.
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize)]
pub struct AfiSafi {
pub afi: Afi,
pub safi: Safi,
}
impl AfiSafi {
pub const IPV4_UNICAST: Self = Self::new(Afi::IPV4, Safi::UNICAST);
pub const IPV6_UNICAST: Self = Self::new(Afi::IPV6, Safi::UNICAST);
pub const fn new(afi: Afi, safi: Safi) -> Self {
Self { afi, safi }
}
/// The unicast family of an IP version.
pub fn unicast(version: IpVersion) -> Self {
Self::new(version.into(), Safi::UNICAST)
}
/// The usual name of the family, if it has one here.
fn name(self) -> Option<&'static str> {
Some(match (self.afi, self.safi) {
(Afi::IPV4, Safi::UNICAST) => "IPv4 unicast",
(Afi::IPV6, Safi::UNICAST) => "IPv6 unicast",
(Afi::IPV4, Safi::MULTICAST) => "IPv4 multicast",
(Afi::IPV6, Safi::MULTICAST) => "IPv6 multicast",
(Afi::IPV4, Safi::MPLS_LABEL) => "IPv4 labeled unicast",
(Afi::IPV6, Safi::MPLS_LABEL) => "IPv6 labeled unicast",
(Afi::IPV4, Safi::MPLS_VPN) => "VPNv4 unicast",
(Afi::IPV6, Safi::MPLS_VPN) => "VPNv6 unicast",
(Afi::IPV4, Safi::FLOWSPEC) => "IPv4 flowspec",
(Afi::IPV6, Safi::FLOWSPEC) => "IPv6 flowspec",
(Afi::L2VPN, Safi::EVPN) => "L2VPN EVPN",
(Afi::BGP_LS, Safi::BGP_LS) => "BGP-LS",
_ => return None,
})
}
}
/// The implementation try_from for u16 just reuses the u8 implementation since
/// we don't have any cases where the SAFI is longer than u8::MAX yet.
impl TryFrom<u16> for SubsequentAfi {
type Error = eyre::Error;
fn try_from(value: u16) -> Result<Self, Self::Error> {
TryFrom::<u8>::try_from(value as u8)
}
}
impl Display for SubsequentAfi {
impl Display for AfiSafi {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SubsequentAfi::Unicast => write!(f, "Unicast"),
SubsequentAfi::Multicast => write!(f, "Multicast"),
SubsequentAfi::NlriWithMpls => write!(f, "NlriWithMpls"),
SubsequentAfi::MplsLabelledVpn => write!(f, "MplsLabelledVpn"),
SubsequentAfi::MulticastMplsVpn => write!(f, "MulticastMplsVpn"),
match self.name() {
Some(name) => f.write_str(name),
None => write!(f, "AFI {} SAFI {}", self.afi.0, self.safi.0),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn any_family_is_a_value_and_the_common_ones_have_names() {
assert_eq!(AfiSafi::IPV6_UNICAST.to_string(), "IPv6 unicast");
assert_eq!(
AfiSafi::new(Afi::L2VPN, Safi::EVPN).to_string(),
"L2VPN EVPN"
);
assert_eq!(
AfiSafi::new(Afi(999), Safi(7)).to_string(),
"AFI 999 SAFI 7"
);
assert_eq!(AfiSafi::unicast(IpVersion::Ipv4), AfiSafi::IPV4_UNICAST);
}
#[test]
fn only_ipv4_and_ipv6_are_ip_versions() {
assert_eq!(IpVersion::try_from(Afi::IPV6), Ok(IpVersion::Ipv6));
assert_eq!(IpVersion::try_from(Afi::L2VPN), Err(UnknownAfi(25)));
assert_eq!(IpVersion::try_from(3u16), Err(UnknownAfi(3)));
}
}
+274 -111
View File
@@ -2,130 +2,187 @@ use std::fmt::Display;
use std::net::{Ipv4Addr, Ipv6Addr};
use std::str::FromStr;
use bytes::{Buf, BufMut, BytesMut};
use eyre::{Result, bail};
use serde::{Deserialize, Serialize};
use serde::Serialize;
use thiserror::Error;
use crate::constants::AddressFamilyId;
use crate::parser::{ParserContext, ToWireError};
use crate::constants::IpVersion;
/// IpPrefix represents some IP address prefix, for a specific AddressFamilyId.
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
/// Why something is not a valid prefix.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum PrefixError {
#[error("prefix {0:?} is not in address/length form")]
Format(String),
#[error("prefix {0:?} has an address that is neither IPv4 nor IPv6")]
Address(String),
#[error("prefix length {length} is longer than the {max} bits of an address")]
LengthTooLong { length: u8, max: u8 },
#[error("prefix length {length} needs {needed} address bytes, got {got}")]
TooFewBytes {
length: u8,
needed: usize,
got: usize,
},
}
/// An IPv4 or IPv6 prefix.
///
/// It is a small value with no allocation behind it, cheap to copy, compare
/// and hash: a routing table holds millions of them and looks them up by
/// value. A prefix has one representation: the bits past its length are
/// always zero, so two prefixes are equal exactly when they are the same
/// prefix.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Serialize)]
pub struct IpPrefix {
pub address_family: AddressFamilyId,
pub prefix: Vec<u8>,
pub length: u8,
address_family: IpVersion,
length: u8,
/// The address, left-aligned, zero past `length` bits.
address: [u8; 16],
}
impl IpPrefix {
pub fn new(address_family: AddressFamilyId, prefix: Vec<u8>, length: u8) -> Result<Self> {
// Ensure that the prefix we are given contains the right number of bytes corresponding to the prefix length.
if prefix.len() < ((length + 7) / 8).into() {
bail!(
"Mismatched prefix {:?} for given prefix length: {}",
prefix,
length
);
/// Makes a prefix from the leading bytes of its address. `prefix` must
/// hold at least the bytes that `length` bits cover; anything past
/// those bits is ignored (RFC 4271 §4.3: such bits are irrelevant).
pub fn new(
address_family: IpVersion,
prefix: impl AsRef<[u8]>,
length: u8,
) -> Result<Self, PrefixError> {
let prefix = prefix.as_ref();
let max = address_family.bits();
if length > max {
return Err(PrefixError::LengthTooLong { length, max });
}
let needed = usize::from(length.div_ceil(8));
if prefix.len() < needed {
return Err(PrefixError::TooFewBytes {
length,
needed,
got: prefix.len(),
});
}
let mut address = [0u8; 16];
address[..needed].copy_from_slice(&prefix[..needed]);
if let (Some(last), rem @ 1..) = (address[..needed].last_mut(), length % 8) {
*last &= u8::MAX << (8 - rem);
}
Ok(Self {
address_family,
prefix,
length,
address,
})
}
pub fn to_wire(&self, _: &ParserContext, out: &mut BytesMut) -> Result<(), ToWireError> {
// Verify that there is enough space to write the IpPrefix.
if out.remaining() < (self.prefix.len() + 1) {
Err(ToWireError::OutBufferOverflow)?;
pub fn address_family(&self) -> IpVersion {
self.address_family
}
/// The prefix length in bits.
pub fn length(&self) -> u8 {
self.length
}
/// The bytes the prefix length covers, as they go on the wire: none for
/// a default route, three for a /24.
pub fn bytes(&self) -> &[u8] {
&self.address[..usize::from(self.length.div_ceil(8))]
}
/// Returns true if `other` is equal to or more specific than `self`, i.e.
/// `self` covers `other`. Prefixes of different address families never
/// contain each other.
pub fn contains(&self, other: &IpPrefix) -> bool {
if self.address_family != other.address_family || other.length < self.length {
return false;
}
let full_bytes = usize::from(self.length / 8);
if self.address[..full_bytes] != other.address[..full_bytes] {
return false;
}
let rem_bits = self.length % 8;
if rem_bits == 0 {
return true;
}
let mask = u8::MAX << (8 - rem_bits);
(self.address[full_bytes] & mask) == (other.address[full_bytes] & mask)
}
}
// Write length and prefix.
out.put_u8(self.length);
out.put(self.prefix.as_slice());
/// A prefix as an UPDATE names it: the prefix and, on a session where
/// ADD-PATH is in use for the address family (RFC 7911), the identifier
/// that tells apart the several routes a peer may announce for it.
///
/// Whether the identifier is on the wire is a matter of what the session
/// negotiated, not of its value. Where it is not, it is zero here.
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Serialize)]
pub struct Nlri {
pub prefix: IpPrefix,
pub path_id: u32,
}
Ok(())
impl Nlri {
/// `prefixes` as they are named on a session without ADD-PATH.
pub fn plain(prefixes: impl IntoIterator<Item = IpPrefix>) -> Vec<Self> {
prefixes.into_iter().map(Self::from).collect()
}
/// The bytes this takes on the wire, with or without its identifier.
pub fn wire_len(&self, with_path_id: bool) -> usize {
1 + self.prefix.bytes().len() + if with_path_id { 4 } else { 0 }
}
}
impl From<IpPrefix> for Nlri {
fn from(prefix: IpPrefix) -> Self {
Self { prefix, path_id: 0 }
}
}
/// So that a list of these compares with a list of plain prefixes.
impl PartialEq<IpPrefix> for Nlri {
fn eq(&self, prefix: &IpPrefix) -> bool {
self.path_id == 0 && self.prefix == *prefix
}
}
impl Display for Nlri {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.path_id {
0 => write!(f, "{}", self.prefix),
id => write!(f, "{} (path {id})", self.prefix),
}
}
}
impl TryFrom<&str> for IpPrefix {
type Error = eyre::Error;
type Error = PrefixError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let parts: Vec<&str> = value.split("/").collect();
let format = || PrefixError::Format(value.to_string());
let address = || PrefixError::Address(value.to_string());
let (addr, length) = value.split_once('/').ok_or_else(format)?;
let length: u8 = length.parse().map_err(|_| format())?;
if parts.len() != 2 {
bail!(
"Expected IpPrefix in format prefix/length but got: {}",
value
);
}
let length: u8 = u8::from_str_radix(parts[1], 10).map_err(eyre::Error::from)?;
let mut octets;
let afi: AddressFamilyId;
if parts[0].contains(":") {
afi = AddressFamilyId::Ipv6;
let addr: Ipv6Addr = Ipv6Addr::from_str(parts[0]).map_err(eyre::Error::from)?;
octets = addr.octets().to_vec();
} else if parts[0].contains(".") {
afi = AddressFamilyId::Ipv4;
let addr: Ipv4Addr = Ipv4Addr::from_str(parts[0]).map_err(eyre::Error::from)?;
octets = addr.octets().to_vec();
let (afi, octets) = if addr.contains(':') {
let addr = Ipv6Addr::from_str(addr).map_err(|_| address())?;
(IpVersion::Ipv6, addr.octets().to_vec())
} else {
bail!("Could not figure out address type")
}
// Truncate the octets we have to the right number of bytes to match the prefix length..
if length % 8 == 0 {
// We can cleanly truncate the number of bytes since we are at a byte boundary.
octets.truncate((length / 8).into());
} else {
// We need to keep length % 8 bits of the last byte.
let num_bytes = (length / 8) + 1;
let mask = u8::MAX << (8 - (length % 8));
octets.truncate(num_bytes.into());
// Fix up the last byte.
let last_pos = octets.len() - 1;
octets[last_pos] &= mask;
}
Ok(IpPrefix {
address_family: afi,
prefix: octets,
length,
})
let addr = Ipv4Addr::from_str(addr).map_err(|_| address())?;
(IpVersion::Ipv4, addr.octets().to_vec())
};
IpPrefix::new(afi, octets, length)
}
}
impl Display for IpPrefix {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.address_family {
AddressFamilyId::Ipv4 => {
// Pad the prefix with 0 bytes if it's less than 4 bytes long.
let bytes = &mut self.prefix.clone();
if bytes.len() < 4 {
bytes.extend(std::iter::repeat(0).take(4 - bytes.len()));
}
let four_bytes: [u8; 4] = bytes
.as_slice()
.try_into()
.map_err(|_| std::fmt::Error {})?;
let ipv4_addr = Ipv4Addr::from(four_bytes);
write!(f, "{}/{}", ipv4_addr, self.length)
IpVersion::Ipv4 => {
let [a, b, c, d, ..] = self.address;
write!(f, "{}/{}", Ipv4Addr::new(a, b, c, d), self.length)
}
AddressFamilyId::Ipv6 => {
let bytes = &mut self.prefix.clone();
if bytes.len() < 16 {
bytes.extend(std::iter::repeat(0).take(16 - bytes.len()));
}
let sixteen_bytes: [u8; 16] = bytes
.as_slice()
.try_into()
.map_err(|_| std::fmt::Error {})?;
let ipv6_addr = Ipv6Addr::from(sixteen_bytes);
write!(f, "{}/{}", ipv6_addr, self.length)
IpVersion::Ipv6 => {
write!(f, "{}/{}", Ipv6Addr::from(self.address), self.length)
}
}
}
@@ -133,9 +190,11 @@ impl Display for IpPrefix {
#[cfg(test)]
mod tests {
use crate::constants::AddressFamilyId;
use crate::constants::IpVersion;
use super::IpPrefix;
use proptest::prelude::*;
use super::{IpPrefix, PrefixError};
macro_rules! verify_roundtrip {
($name:ident, $prefix_str:expr, $afi:expr, $bytes:expr, $length:expr) => {
@@ -143,9 +202,9 @@ mod tests {
fn $name() {
let ip_prefix = IpPrefix::try_from($prefix_str).unwrap();
assert_eq!(ip_prefix.address_family, $afi);
assert_eq!(ip_prefix.prefix, $bytes);
assert_eq!(ip_prefix.length, $length);
assert_eq!(ip_prefix.address_family(), $afi);
assert_eq!(ip_prefix.bytes(), $bytes);
assert_eq!(ip_prefix.length(), $length);
let to_str: &str = &ip_prefix.to_string();
assert_eq!(IpPrefix::try_from(to_str).unwrap(), ip_prefix);
@@ -156,7 +215,7 @@ mod tests {
verify_roundtrip!(
verify_roundtrip_ipv4_24,
"10.1.2.0/24",
AddressFamilyId::Ipv4,
IpVersion::Ipv4,
vec![10, 1, 2],
24
);
@@ -165,7 +224,7 @@ mod tests {
verify_roundtrip!(
verify_roundtrip_ipv4_19,
"10.245.123.0/19",
AddressFamilyId::Ipv4,
IpVersion::Ipv4,
vec![10, 245, 96],
19
);
@@ -174,7 +233,7 @@ mod tests {
verify_roundtrip!(
verify_roundtrip_ipv4_3,
"192.168.1.0/3",
AddressFamilyId::Ipv4,
IpVersion::Ipv4,
vec![192],
3
);
@@ -183,7 +242,7 @@ mod tests {
verify_roundtrip!(
verify_roundtrip_ipv4_0,
"0.0.0.0/0",
AddressFamilyId::Ipv4,
IpVersion::Ipv4,
vec![],
0
);
@@ -191,7 +250,7 @@ mod tests {
verify_roundtrip!(
verify_roundtrip_ipv6_48,
"2001:db8:cafe::/48",
AddressFamilyId::Ipv6,
IpVersion::Ipv6,
vec![32, 1, 13, 184, 202, 254],
48
);
@@ -200,16 +259,120 @@ mod tests {
verify_roundtrip!(
verify_roundtrip_ipv6_32,
"2001:db8:cafe::/32",
AddressFamilyId::Ipv6,
IpVersion::Ipv6,
vec![32, 1, 13, 184],
32
);
verify_roundtrip!(
verify_roundtrip_ipv6_0,
"::/0",
AddressFamilyId::Ipv6,
vec![],
0
);
#[test]
fn text_that_is_not_a_prefix_is_rejected_with_the_reason() {
for (text, expected) in [
("10.0.0.0", PrefixError::Format("10.0.0.0".into())),
("10.0.0.0/x", PrefixError::Format("10.0.0.0/x".into())),
("10.0.0/8", PrefixError::Address("10.0.0/8".into())),
("nonsense/8", PrefixError::Address("nonsense/8".into())),
(
"10.0.0.0/33",
PrefixError::LengthTooLong {
length: 33,
max: 32,
},
),
(
"2001:db8::/129",
PrefixError::LengthTooLong {
length: 129,
max: 128,
},
),
] {
assert_eq!(IpPrefix::try_from(text), Err(expected), "{text}");
}
assert!(IpPrefix::try_from("0.0.0.0/0").is_ok());
assert!(IpPrefix::try_from("2001:db8::1/128").is_ok());
assert_eq!(
IpPrefix::new(IpVersion::Ipv4, vec![10], 16),
Err(PrefixError::TooFewBytes {
length: 16,
needed: 2,
got: 1
})
);
}
#[test]
fn a_prefix_has_one_representation() {
// Extra bytes and stray bits past the length are dropped.
let noisy = IpPrefix::new(IpVersion::Ipv4, vec![10, 0xff, 3, 4], 12).unwrap();
assert_eq!(noisy, IpPrefix::try_from("10.240.0.0/12").unwrap());
assert_eq!(noisy.bytes(), [10, 0xf0]);
assert_eq!(
IpPrefix::new(IpVersion::Ipv4, vec![0xff; 13], 99),
Err(PrefixError::LengthTooLong {
length: 99,
max: 32
})
);
}
#[test]
fn a_prefix_is_a_small_value() {
assert!(size_of::<IpPrefix>() <= 20);
let prefix = IpPrefix::try_from("10.1.2.0/24").unwrap();
let copy = prefix;
assert_eq!(copy, prefix);
assert_eq!(prefix.bytes(), [10, 1, 2]);
assert_eq!(IpPrefix::try_from("0.0.0.0/0").unwrap().bytes(), [0u8; 0]);
}
proptest! {
#[test]
fn text_round_trips(version: bool, bytes: [u8; 16], length in 0u8..=128) {
let version = if version { IpVersion::Ipv6 } else { IpVersion::Ipv4 };
let length = length.min(version.bits());
let prefix = IpPrefix::new(version, bytes, length).unwrap();
let text = prefix.to_string();
prop_assert_eq!(IpPrefix::try_from(text.as_str()), Ok(prefix));
prop_assert!(prefix.contains(&prefix));
}
#[test]
fn text_that_is_noise_is_an_error_or_a_prefix_that_round_trips(text in "\\PC{0,40}") {
if let Ok(prefix) = IpPrefix::try_from(text.as_str()) {
let shown = prefix.to_string();
prop_assert_eq!(IpPrefix::try_from(shown.as_str()), Ok(prefix));
}
}
/// `contains` agrees with comparing the leading bits by hand.
#[test]
fn contains_is_a_comparison_of_leading_bits(
a: [u8; 4], a_len in 0u8..=32, b: [u8; 4], b_len in 0u8..=32,
) {
let outer = IpPrefix::new(IpVersion::Ipv4, a, a_len).unwrap();
let inner = IpPrefix::new(IpVersion::Ipv4, b, b_len).unwrap();
let mask = if a_len == 0 { 0 } else { u32::MAX << (32 - a_len) };
let expected = b_len >= a_len
&& (u32::from_be_bytes(a) & mask) == (u32::from_be_bytes(b) & mask);
prop_assert_eq!(outer.contains(&inner), expected);
}
}
#[test]
fn contains_covers_more_specifics() {
let p = |s: &str| IpPrefix::try_from(s).unwrap();
assert!(p("10.100.0.0/16").contains(&p("10.100.7.0/24")));
assert!(p("10.100.0.0/16").contains(&p("10.100.0.0/16")));
assert!(!p("10.100.0.0/16").contains(&p("10.101.0.0/24")));
assert!(!p("10.100.7.0/24").contains(&p("10.100.0.0/16")));
assert!(p("0.0.0.0/0").contains(&p("192.0.2.0/24")));
// Non-byte-aligned boundary.
assert!(p("10.100.0.0/19").contains(&p("10.100.31.0/24")));
assert!(!p("10.100.0.0/19").contains(&p("10.100.32.0/24")));
// Cross-family never matches.
assert!(!p("::/0").contains(&p("10.0.0.0/8")));
assert!(p("2001:db8::/32").contains(&p("2001:db8:cafe::/48")));
}
verify_roundtrip!(verify_roundtrip_ipv6_0, "::/0", IpVersion::Ipv6, vec![], 0);
}
+33 -25
View File
@@ -1,35 +1,43 @@
use std::fmt::Display;
use nom::error::{ErrorKind, ParseError};
use crate::constants::AddressFamilyId;
use crate::constants::AfiSafi;
use thiserror::Error;
#[derive(Debug, Default)]
/// What reading or writing a message has to know about the session it
/// belongs to. The default is a session before its OPENs are exchanged;
/// only an UPDATE depends on anything here, and there are none until then.
#[derive(Debug, Clone, Default)]
pub struct ParserContext {
/// Whether thi parser is being run with a peer that is RFC6793 compliant.
pub four_octet_asn: Option<bool>,
/// Which address family should be parsed by default with this parser.
pub address_family: Option<AddressFamilyId>,
/// Whether AS numbers are four octets wide on this session (RFC 6793).
pub four_octet_asn: bool,
/// Whether the peer is in our own AS. Some attributes mean something
/// only between internal peers and are dropped when an external one
/// sends them (RFC 7606 §7).
pub ibgp: bool,
/// The address families whose prefixes carry a path identifier in the
/// direction this context is for (RFC 7911). What a session sends and
/// what it receives are negotiated separately, so a session has one
/// context for reading and one for writing.
pub add_path: Vec<AfiSafi>,
}
#[derive(Debug)]
pub enum ToWireError {
/// There was not enough space in the output buffer to serialize the data into.
OutBufferOverflow,
/// Another error.
Other(eyre::Error),
}
impl Display for ToWireError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ToWireError::OutBufferOverflow => write!(f, "OutBufferOverflow"),
ToWireError::Other(report) => report.fmt(f),
}
impl ParserContext {
pub fn has_add_path(&self, family: AfiSafi) -> bool {
self.add_path.contains(&family)
}
}
impl std::error::Error for ToWireError {}
/// Why a value could not be written to the wire.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum EncodeError {
/// A length does not fit the field that has to carry it.
#[error("{what} is too long to encode: {len} exceeds {max}")]
TooLong {
what: &'static str,
len: usize,
max: usize,
},
}
// Custom error type for the parser.
#[derive(Debug)]
@@ -37,8 +45,8 @@ pub enum BgpParserError<I> {
/// ProtocolError encapsulates protocol level semantics problems that require
/// action from the caller of the parser.
ProtocolError(ProtocolErrorValues),
CustomText(&'static str),
Eyre(eyre::ErrReport),
/// The input does not follow the format; the text says how.
Malformed(String),
Nom(I, ErrorKind),
}
+14 -5
View File
@@ -1,3 +1,10 @@
//! A Poptrie (Asai and Ohara, SIGCOMM 2015): a compressed multiway trie for
//! longest-prefix matching.
//!
//! Work in progress. Nothing in the daemon uses it yet, and the construction
//! side has no public entry point, hence the lint expectation below.
#![expect(dead_code, reason = "not yet wired into a RIB")]
use std::collections::BTreeMap;
#[derive(Debug)]
@@ -75,7 +82,8 @@ impl<T: Clone> Poptrie<T> {
result
}
fn new(&mut self, rt: BTreeMap<(u32, u8), T>) {
/// Fills the trie from a routing table keyed by (address, prefix length).
fn build(&mut self, rt: BTreeMap<(u32, u8), T>) {
const DEPTH: u8 = 6;
let mut forest = vec![(0, rt)];
@@ -168,9 +176,10 @@ mod tests {
leaves: vec![],
};
poptrie.new(map);
println!("Success");
dbg!(poptrie);
poptrie.build(map);
assert!(
!poptrie.inners.is_empty(),
"building from a non-empty table yields nodes"
);
}
}
+16
View File
@@ -1,9 +1,25 @@
[package]
name = "bgp-rib"
authors.workspace = true
edition.workspace = true
homepage.workspace = true
license.workspace = true
repository.workspace = true
version.workspace = true
[dependencies]
bgp-packet.workspace = true
bgp-primitives.workspace = true
bytes.workspace = true
foldhash.workspace = true
hashbrown.workspace = true
serde.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["full"] }
tracing.workspace = true
[dev-dependencies]
proptest.workspace = true
[lints]
workspace = true
@@ -0,0 +1,7 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 7107cf5580fe7bb95e138f1fead386d89273532f83aa1d11808aa9586aa9da63 # shrinks to ops = [Withdraw { peer: 0, prefix: 1 }, Withdraw { peer: 0, prefix: 1 }, Poll { reader: 2, budget: 2 }, Withdraw { peer: 0, prefix: 10 }, Poll { reader: 2, budget: 4 }, Announce { peer: 2, prefix: 7, route: Route { local_pref: None, path: [1, 2], origin: 0, med: Some(0) } }, Announce { peer: 3, prefix: 6, route: Route { local_pref: None, path: [1], origin: 0, med: None } }, Announce { peer: 3, prefix: 6, route: Route { local_pref: None, path: [1, 1], origin: 0, med: None } }, Withdraw { peer: 1, prefix: 5 }, Withdraw { peer: 0, prefix: 11 }, Withdraw { peer: 3, prefix: 6 }, Announce { peer: 0, prefix: 3, route: Route { local_pref: None, path: [1, 2], origin: 1, med: None } }, Poll { reader: 2, budget: 3 }, Withdraw { peer: 0, prefix: 8 }, Flap { peer: 3 }, Withdraw { peer: 1, prefix: 1 }, Announce { peer: 2, prefix: 5, route: Route { local_pref: None, path: [1], origin: 1, med: Some(0) } }, Announce { peer: 0, prefix: 1, route: Route { local_pref: None, path: [1, 1], origin: 1, med: None } }, Withdraw { peer: 0, prefix: 1 }, Withdraw { peer: 2, prefix: 8 }, Poll { reader: 1, budget: 2 }, Flap { peer: 1 }, Poll { reader: 2, budget: 2 }, Announce { peer: 0, prefix: 9, route: Route { local_pref: None, path: [1], origin: 0, med: Some(1) } }, Announce { peer: 2, prefix: 2, route: Route { local_pref: Some(100), path: [2], origin: 0, med: Some(1) } }, Poll { reader: 1, budget: 3 }, Withdraw { peer: 3, prefix: 1 }, Announce { peer: 1, prefix: 4, route: Route { local_pref: None, path: [2], origin: 1, med: None } }, Announce { peer: 3, prefix: 7, route: Route { local_pref: None, path: [2], origin: 0, med: None } }, Announce { peer: 1, prefix: 11, route: Route { local_pref: None, path: [1], origin: 1, med: Some(1) } }, Withdraw { peer: 0, prefix: 6 }, Withdraw { peer: 0, prefix: 4 }, Announce { peer: 3, prefix: 10, route: Route { local_pref: Some(100), path: [2, 2], origin: 1, med: None } }, Announce { peer: 1, prefix: 2, route: Route { local_pref: None, path: [2], origin: 0, med: Some(2) } }, Announce { peer: 1, prefix: 3, route: Route { local_pref: None, path: [1], origin: 1, med: None } }, Withdraw { peer: 0, prefix: 2 }, Announce { peer: 1, prefix: 4, route: Route { local_pref: None, path: [1, 1], origin: 0, med: None } }, Poll { reader: 1, budget: 2 }, Poll { reader: 1, budget: 4 }, Poll { reader: 2, budget: 1 }, Poll { reader: 1, budget: 3 }, Poll { reader: 1, budget: 5 }, Poll { reader: 1, budget: 1 }, Withdraw { peer: 3, prefix: 5 }, Withdraw { peer: 3, prefix: 9 }, Withdraw { peer: 2, prefix: 10 }], shards = 3
@@ -0,0 +1,7 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 6bcfc317c976928c2fbb29e943f385127b91770a76f18e7061b80b31f4276955 # shrinks to ops = [Announce { peer: 1, prefix: 1, route: Route { local_pref: None, path: [2], origin: 0, med: None } }, Announce { peer: 2, prefix: 1, route: Route { local_pref: None, path: [1], origin: 0, med: Some(2) } }, Announce { peer: 3, prefix: 1, route: Route { local_pref: None, path: [1], origin: 0, med: None } }, Announce { peer: 3, prefix: 1, route: Route { local_pref: None, path: [1], origin: 1, med: None } }]
-96
View File
@@ -1,96 +0,0 @@
use std::collections::HashMap;
use bgp_primitives::ip_prefix::IpPrefix;
use tokio::sync::{broadcast, mpsc, oneshot};
use tracing::warn;
use crate::entry::{PeerId, RibEntry, SnapshotEntry};
use crate::event::RibEvent;
const EVENT_CHANNEL_CAPACITY: usize = 1024;
const CMD_CHANNEL_CAPACITY: usize = 256;
enum RibCommand {
Announce { peer: PeerId, prefix: IpPrefix, entry: RibEntry },
Withdraw { peer: PeerId, prefix: IpPrefix },
Snapshot { reply: oneshot::Sender<Vec<SnapshotEntry>> },
}
/// Handle to the Adj-RIB-In actor. Cheap to clone — all clones share the same
/// backing task. The actor is stopped when all handles are dropped.
#[derive(Clone)]
pub struct AdjRibIn {
tx: mpsc::Sender<RibCommand>,
events: broadcast::Sender<RibEvent>,
}
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 events_clone = events.clone();
tokio::spawn(run_actor(rx, events_clone));
Self { tx, events }
}
pub async fn announce(&self, peer: PeerId, prefix: IpPrefix, entry: RibEntry) {
let _ = self.tx.send(RibCommand::Announce { peer, prefix, entry }).await;
}
pub async fn withdraw(&self, peer: PeerId, prefix: IpPrefix) {
let _ = self.tx.send(RibCommand::Withdraw { peer, prefix }).await;
}
/// Returns a point-in-time snapshot of all routes in the RIB.
pub async fn snapshot(&self) -> Vec<SnapshotEntry> {
let (reply_tx, reply_rx) = oneshot::channel();
let _ = self.tx.send(RibCommand::Snapshot { reply: reply_tx }).await;
reply_rx.await.unwrap_or_default()
}
/// 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>) {
let mut table: HashMap<(PeerId, IpPrefix), RibEntry> = HashMap::new();
while let Some(cmd) = rx.recv().await {
match cmd {
RibCommand::Announce { peer, prefix, entry } => {
table.insert((peer, prefix.clone()), entry.clone());
if let Err(e) = events.send(RibEvent::Announced { peer, prefix, entry }) {
// No active subscribers — not an error.
let _ = e;
}
}
RibCommand::Withdraw { peer, prefix } => {
if table.remove(&(peer, prefix.clone())).is_some() {
if let Err(e) = events.send(RibEvent::Withdrawn { peer, prefix }) {
let _ = e;
}
}
}
RibCommand::Snapshot { reply } => {
let snap = table
.iter()
.map(|((peer, prefix), entry)| SnapshotEntry {
peer: *peer,
prefix: prefix.clone(),
entry: entry.clone(),
})
.collect();
if reply.send(snap).is_err() {
warn!("Snapshot reply channel closed before send");
}
}
}
}
}
-39
View File
@@ -1,39 +0,0 @@
use std::net::SocketAddr;
use std::time::SystemTime;
use bgp_packet::message::PathAttribute;
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.
#[derive(Debug, Clone)]
pub struct RibEntry {
pub path_attributes: Vec<PathAttribute>,
pub received_at: SystemTime,
}
impl RibEntry {
pub fn new(path_attributes: Vec<PathAttribute>) -> Self {
Self { path_attributes, received_at: SystemTime::now() }
}
}
/// A flattened row returned by [`crate::AdjRibIn::snapshot`].
#[derive(Debug, Clone)]
pub struct SnapshotEntry {
pub peer: PeerId,
pub prefix: IpPrefix,
pub entry: RibEntry,
}
+14
View File
@@ -0,0 +1,14 @@
/// A RIB's actor is no longer running, so the table can be neither read nor
/// 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: 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.
pub rib: &'static str,
}
-9
View File
@@ -1,9 +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 },
}
+8 -6
View File
@@ -1,7 +1,9 @@
mod adj_rib_in;
mod entry;
mod event;
//! The router's tables: the routes it has learned and chosen
//! ([`table::RibTable`]) and the routes it originates ([`LocalRib`]).
pub use adj_rib_in::AdjRibIn;
pub use entry::{PeerId, RibEntry, SnapshotEntry};
pub use event::RibEvent;
mod error;
mod local_rib;
pub mod table;
pub use error::RibStopped;
pub use local_rib::{LocalRib, LocalRibEvent, LocalRoute, LocalRouteAttrs};
+262
View File
@@ -0,0 +1,262 @@
//! Locally originated routes (the "Loc-RIB" of routes this router announces
//! on its own behalf, as opposed to routes learned from peers).
//!
//! The actor stores one route per prefix together with its attributes. It
//! does not know about peers or sessions: the server subscribes to
//! [`LocalRibEvent`]s and turns them into UPDATEs.
use std::collections::HashMap;
use std::time::SystemTime;
use bgp_packet::message::{LargeCommunity, OriginPathAttribute};
use bgp_primitives::ip_prefix::IpPrefix;
use serde::Serialize;
use tokio::sync::{broadcast, mpsc, oneshot};
use tokio::task::AbortHandle;
use crate::error::RibStopped;
const EVENT_CHANNEL_CAPACITY: usize = 1024;
const CMD_CHANNEL_CAPACITY: usize = 256;
/// Attributes of a locally originated route.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct LocalRouteAttrs {
pub origin: OriginPathAttribute,
/// Number of *additional* copies of the local ASN to prepend to the
/// AS_PATH. `0` yields the plain `[local_asn]` path.
pub prepend: u8,
pub med: Option<u32>,
/// RFC 1997 standard communities as `(high, low)` 16-bit halves.
pub communities: Vec<(u16, u16)>,
/// RFC 8092 large communities.
pub large_communities: Vec<LargeCommunity>,
}
impl Default for LocalRouteAttrs {
fn default() -> Self {
Self {
origin: OriginPathAttribute::IGP,
prepend: 0,
med: None,
communities: vec![],
large_communities: vec![],
}
}
}
/// A route originated by this router.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct LocalRoute {
pub prefix: IpPrefix,
pub attrs: LocalRouteAttrs,
pub announced_at: SystemTime,
}
impl LocalRoute {
pub fn new(prefix: IpPrefix, attrs: LocalRouteAttrs) -> Self {
Self {
prefix,
attrs,
announced_at: SystemTime::now(),
}
}
}
const STOPPED: RibStopped = RibStopped { rib: "local RIB" };
#[derive(Debug, Clone)]
pub enum LocalRibEvent {
/// A route was added or its attributes replaced.
Announced(LocalRoute),
Withdrawn {
prefix: IpPrefix,
},
}
enum Command {
Announce {
route: LocalRoute,
reply: oneshot::Sender<()>,
},
Withdraw {
prefix: IpPrefix,
reply: oneshot::Sender<Option<LocalRoute>>,
},
Snapshot {
reply: oneshot::Sender<Vec<LocalRoute>>,
},
Len {
reply: oneshot::Sender<usize>,
},
}
/// Handle to the local RIB actor. Cheap to clone; all clones share one table.
///
/// Every operation fails with [`RibStopped`] if the actor is gone; see
/// there for what that means.
#[derive(Clone)]
pub struct LocalRib {
tx: mpsc::Sender<Command>,
events: broadcast::Sender<LocalRibEvent>,
actor: AbortHandle,
}
impl LocalRib {
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 }
}
/// Adds a route, or replaces the existing route for the same prefix.
pub async fn announce(&self, route: LocalRoute) -> Result<(), RibStopped> {
self.ask(|reply| Command::Announce { route, reply }).await
}
/// Removes a route. Returns the removed route, or `None` if the prefix
/// was not present.
pub async fn withdraw(&self, prefix: IpPrefix) -> Result<Option<LocalRoute>, RibStopped> {
self.ask(|reply| Command::Withdraw { prefix, reply }).await
}
/// Point-in-time copy of every route, in no particular order.
pub async fn snapshot(&self) -> Result<Vec<LocalRoute>, RibStopped> {
self.ask(|reply| Command::Snapshot { reply }).await
}
/// How many routes the RIB holds, without copying them.
pub async fn len(&self) -> Result<usize, RibStopped> {
self.ask(|reply| Command::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 and waits for its answer.
async fn ask<T>(
&self,
command: impl FnOnce(oneshot::Sender<T>) -> Command,
) -> 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 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()
}
}
async fn run_actor(mut rx: mpsc::Receiver<Command>, events: broadcast::Sender<LocalRibEvent>) {
let mut table: HashMap<IpPrefix, LocalRoute> = HashMap::new();
while let Some(cmd) = rx.recv().await {
match cmd {
Command::Announce { route, reply } => {
table.insert(route.prefix, route.clone());
let _ = events.send(LocalRibEvent::Announced(route));
let _ = reply.send(());
}
Command::Withdraw { prefix, reply } => {
let removed = table.remove(&prefix);
if removed.is_some() {
let _ = events.send(LocalRibEvent::Withdrawn { prefix });
}
let _ = reply.send(removed);
}
Command::Snapshot { reply } => {
// A failed reply means the asker stopped waiting.
let _ = reply.send(table.values().cloned().collect());
}
Command::Len { reply } => {
let _ = reply.send(table.len());
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn prefix(s: &str) -> IpPrefix {
IpPrefix::try_from(s).unwrap()
}
fn route(p: &str) -> LocalRoute {
LocalRoute::new(prefix(p), LocalRouteAttrs::default())
}
#[tokio::test]
async fn announce_and_withdraw_emit_events() {
let rib = LocalRib::spawn();
let mut events = rib.subscribe();
rib.announce(route("10.0.0.0/24")).await.unwrap();
assert!(
matches!(events.recv().await.unwrap(), LocalRibEvent::Announced(r) if r.prefix == prefix("10.0.0.0/24"))
);
assert_eq!(rib.len().await, Ok(1));
let removed = rib.withdraw(prefix("10.0.0.0/24")).await.unwrap();
assert_eq!(removed.unwrap().prefix, prefix("10.0.0.0/24"));
assert!(
matches!(events.recv().await.unwrap(), LocalRibEvent::Withdrawn { prefix: p } if p == prefix("10.0.0.0/24"))
);
assert_eq!(rib.is_empty().await, Ok(true));
// Withdrawing an absent prefix is a no-op, not an error.
assert_eq!(rib.withdraw(prefix("10.0.0.0/24")).await.unwrap(), None);
}
#[tokio::test]
async fn announcing_a_prefix_again_replaces_its_attributes() {
let rib = LocalRib::spawn();
rib.announce(route("10.0.0.0/24")).await.unwrap();
let attrs = LocalRouteAttrs {
med: Some(50),
..Default::default()
};
rib.announce(LocalRoute::new(prefix("10.0.0.0/24"), attrs.clone()))
.await
.unwrap();
let table = rib.snapshot().await.unwrap();
assert_eq!(table.len(), 1);
assert_eq!(table[0].attrs, attrs);
}
#[tokio::test]
async fn a_stopped_actor_is_an_error_and_not_an_empty_table() {
let rib = LocalRib::spawn();
rib.announce(route("10.0.0.0/24")).await.unwrap();
rib.stop_for_testing();
rib.stopped().await;
assert_eq!(rib.announce(route("10.0.1.0/24")).await, Err(STOPPED));
assert_eq!(rib.withdraw(prefix("10.0.0.0/24")).await, Err(STOPPED));
assert_eq!(rib.snapshot().await, Err(STOPPED));
assert_eq!(rib.len().await, Err(STOPPED));
}
}
+449
View File
@@ -0,0 +1,449 @@
//! Attribute sets, stored once.
//!
//! A full table has millions of routes and far fewer distinct lists of path
//! attributes: a peer announces prefixes that share their attributes
//! together, about six to an UPDATE on the Internet today. A route in the
//! table is therefore a pointer to an [`AttrSet`], and each distinct set
//! exists once, in the [`AttrStore`].
//!
//! A set is kept as its bytes on the wire, which is a fifth the size of the
//! parsed form. What choosing between routes needs from it is worked out
//! once, when the set is stored, and kept beside the bytes.
use std::hash::BuildHasher;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use bgp_packet::message::{NlriNextHop, PathAttribute, attr_type, split_attribute};
use bgp_primitives::parser::{EncodeError, ParserContext};
use bytes::{BufMut, BytesMut};
use foldhash::fast::RandomState;
use hashbrown::HashTable;
const OPTIONAL: u8 = 0x80;
/// What route selection reads from a set of attributes (RFC 4271 §9.1.2.2).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SelectionKey {
/// LOCAL_PREF, if the set has one.
pub local_pref: Option<u32>,
/// MULTI_EXIT_DISC, if the set has one.
pub med: Option<u32>,
/// The AS that the route was learned from: the first AS of the path, or
/// zero if the path is empty or begins with a set. MEDs are comparable
/// only between routes with the same one.
pub neighbor_as: u32,
/// The length of the AS path as selection counts it: an AS_SET is one
/// hop however many AS numbers it holds.
pub as_path_len: u16,
/// ORIGIN: 0 for IGP, 1 for EGP, 2 for INCOMPLETE.
pub origin: u8,
}
/// Reads the selection key straight off attributes in the form an
/// [`AttrSet`] keeps, without building anything. `None` if the bytes are
/// not a well-formed list of attributes.
fn scan(mut wire: &[u8]) -> Option<SelectionKey> {
let mut key = SelectionKey {
local_pref: None,
med: None,
neighbor_as: 0,
as_path_len: 0,
// A route without ORIGIN does not get this far; if one did, it
// would be the least preferred.
origin: 2,
};
let number = |value: &[u8]| Some(u32::from_be_bytes(<[u8; 4]>::try_from(value).ok()?));
while !wire.is_empty() {
let (attribute, rest) = split_attribute(wire).ok()?;
wire = rest;
let value = attribute.value;
match attribute.type_code {
attr_type::ORIGIN => key.origin = *value.first()?,
attr_type::AS_PATH => (key.as_path_len, key.neighbor_as) = scan_as_path(value)?,
attr_type::MULTI_EXIT_DISC => key.med = Some(number(value)?),
attr_type::LOCAL_PREF => key.local_pref = Some(number(value)?),
_ => {}
}
}
Some(key)
}
/// The length of an AS_PATH as selection counts it, and the neighbouring
/// AS, from the attribute's value with four-octet AS numbers.
fn scan_as_path(mut segments: &[u8]) -> Option<(u16, u32)> {
const AS_SEQUENCE: u8 = 2;
let mut hops = 0usize;
let mut neighbor_as = 0;
let mut first = true;
while let [kind, count, rest @ ..] = segments {
let (asns, rest) = rest.split_at_checked(usize::from(*count) * 4)?;
let ordered = *kind == AS_SEQUENCE;
hops += if ordered { usize::from(*count) } else { 1 };
if first && ordered {
neighbor_as = asns
.first_chunk::<4>()
.map_or(0, |asn| u32::from_be_bytes(*asn));
}
first = false;
segments = rest;
}
if !segments.is_empty() {
return None;
}
Some((u16::try_from(hops).unwrap_or(u16::MAX), neighbor_as))
}
/// One distinct list of path attributes, with the next hop that goes with
/// it. Made by an [`AttrStore`]; two routes have the same attributes
/// exactly when they point to the same `AttrSet`.
#[derive(Debug)]
pub struct AttrSet {
hash: u64,
key: SelectionKey,
/// The attributes as on the wire, with four-octet AS numbers. A next
/// hop that arrived in MP_REACH_NLRI is kept as an MP_REACH_NLRI that
/// holds nothing else, the form RFC 6396 §4.3.4 uses in table dumps.
wire: Box<[u8]>,
}
impl AttrSet {
pub fn key(&self) -> &SelectionKey {
&self.key
}
/// The attributes as they are on the wire.
pub fn wire(&self) -> &[u8] {
&self.wire
}
/// The attributes, parsed, and the next hop if it came in
/// MP_REACH_NLRI.
///
/// `None` if they do not parse. Sets made from parsed attributes or
/// from an UPDATE the decoder accepted always do; one made from bytes
/// of another source ([`AttrStore::intern_wire`]) was checked only as
/// far as selection reads it.
pub fn attributes(&self) -> Option<(Vec<PathAttribute>, Option<NlriNextHop>)> {
parse(&self.wire)
}
}
/// How the attributes of a set are written: with four-octet AS numbers.
fn four_octet() -> ParserContext {
ParserContext {
four_octet_asn: true,
ibgp: true,
add_path: Vec::new(),
}
}
/// Writes `attributes` and `next_hop` in the form an [`AttrSet`] keeps.
fn encode(
attributes: &[PathAttribute],
next_hop: Option<&NlriNextHop>,
out: &mut BytesMut,
) -> Result<(), EncodeError> {
let ctx = four_octet();
for attribute in attributes {
// These name prefixes, which are not a property of the route.
if matches!(
attribute,
PathAttribute::MpReachNlri(_) | PathAttribute::MpUnreachNlri(_)
) {
continue;
}
attribute.to_wire(&ctx, out)?;
}
if let Some(next_hop) = next_hop {
encode_next_hop(next_hop, out);
}
Ok(())
}
/// Writes a next hop as an MP_REACH_NLRI that holds nothing else.
fn encode_next_hop(next_hop: &NlriNextHop, out: &mut BytesMut) {
out.put_u8(OPTIONAL);
out.put_u8(attr_type::MP_REACH_NLRI);
out.put_u8(1 + next_hop.wire_len());
out.put_u8(next_hop.wire_len());
next_hop.to_wire(out);
}
/// Parses path attributes in the form [`AttrSet`] keeps and table dumps
/// use (RFC 6396 §4.3.4): four-octet AS numbers, and an MP_REACH_NLRI that
/// holds only a next hop. `None` if they are malformed.
fn parse(mut wire: &[u8]) -> Option<(Vec<PathAttribute>, Option<NlriNextHop>)> {
let ctx = four_octet();
let mut attributes = Vec::new();
let mut next_hop = None;
while !wire.is_empty() {
let (raw, rest) = split_attribute(wire).ok()?;
if raw.type_code == attr_type::MP_REACH_NLRI {
let (length, address) = raw.value.split_first()?;
if usize::from(*length) != address.len() {
return None;
}
next_hop = Some(NlriNextHop::from_bytes(address)?);
} else {
let (_, attribute) = PathAttribute::from_wire(&ctx, raw.wire).ok()?;
attributes.push(attribute);
}
wire = rest;
}
Some((attributes, next_hop))
}
/// How many locks the store is split over. Sessions intern concurrently,
/// once per UPDATE; this many locks keep them out of each other's way.
const STRIPES: usize = 64;
/// The one place attribute sets are kept. Safe to share between threads.
pub struct AttrStore {
stripes: Box<[Mutex<HashTable<Arc<AttrSet>>>]>,
hasher: RandomState,
}
impl Default for AttrStore {
fn default() -> Self {
Self::new()
}
}
impl AttrStore {
pub fn new() -> Self {
Self {
stripes: (0..STRIPES).map(|_| Mutex::default()).collect(),
hasher: RandomState::default(),
}
}
/// The stored set equal to `attributes` and `next_hop`, added if it was
/// not there. `next_hop` is the one from MP_REACH_NLRI; routes whose
/// next hop is the NEXT_HOP attribute pass `None`.
///
/// Fails only if the attributes cannot be encoded, which attributes that
/// were decoded from a message always can.
pub fn intern(
&self,
attributes: &[PathAttribute],
next_hop: Option<&NlriNextHop>,
) -> Result<Arc<AttrSet>, EncodeError> {
let mut wire = BytesMut::with_capacity(256);
encode(attributes, next_hop, &mut wire)?;
Ok(self
.intern_wire(&wire)
.expect("attributes written here are well formed"))
}
/// As [`AttrStore::intern`], for attributes that are already in the
/// form a set keeps: on the wire, with four-octet AS numbers, no
/// MP_UNREACH_NLRI, and an MP_REACH_NLRI (if any) that holds only the
/// next hop. That is how a table dump has them (RFC 6396 §4.3.4).
///
/// Nothing is parsed: finding a set that is already stored costs a hash
/// of the bytes and a comparison. `None` if the bytes are not a
/// well-formed list of attributes.
pub fn intern_wire(&self, wire: &[u8]) -> Option<Arc<AttrSet>> {
let hash = self.hasher.hash_one(wire);
let mut stripe = self.stripe(hash);
if let Some(existing) = stripe.find(hash, |set| *set.wire == *wire) {
return Some(Arc::clone(existing));
}
let set = Arc::new(AttrSet {
hash,
key: scan(wire)?,
wire: wire.into(),
});
stripe.insert_unique(hash, Arc::clone(&set), |set| set.hash);
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_next_hop(next_hop, &mut with_next_hop);
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()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Drops the sets that no route refers to any more, and returns how
/// many that was. Sets are not dropped as their last route goes, since
/// that would mean a lock here for every route removed; this is called
/// now and then instead.
pub fn sweep(&self) -> usize {
let mut dropped = 0;
for i in 0..STRIPES {
let mut stripe = self.stripe_at(i);
let before = stripe.len();
// The store's own reference is the only one left. Nobody can
// be taking another: that is done under this lock.
stripe.retain(|set| Arc::strong_count(set) > 1);
dropped += before - stripe.len();
}
dropped
}
fn stripe(&self, hash: u64) -> MutexGuard<'_, HashTable<Arc<AttrSet>>> {
self.stripe_at(super::spread(hash, STRIPES))
}
fn stripe_at(&self, index: usize) -> MutexGuard<'_, HashTable<Arc<AttrSet>>> {
// A panic while holding the lock cannot leave the table half
// changed in a way that matters: see `IgnorePoison` in the server.
self.stripes[index]
.lock()
.unwrap_or_else(PoisonError::into_inner)
}
}
#[cfg(test)]
mod tests {
use std::net::Ipv4Addr;
use bgp_packet::message::{
AsPathAttribute, AsPathSegment, CommunitiesPathAttribute, LocalPrefPathAttribute,
MultiExitDiscPathAttribute, NextHopPathAttribute, OriginPathAttribute,
};
use super::*;
fn attributes(path: Vec<u32>) -> Vec<PathAttribute> {
vec![
PathAttribute::Origin(OriginPathAttribute::EGP),
AsPathAttribute::from_asns(path),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(50)),
PathAttribute::Communities(CommunitiesPathAttribute(vec![(65000, 1)])),
]
}
#[test]
fn equal_attributes_are_one_set() {
let store = AttrStore::new();
let a = store.intern(&attributes(vec![65001, 65002]), None).unwrap();
let b = store.intern(&attributes(vec![65001, 65002]), None).unwrap();
let c = store.intern(&attributes(vec![65001, 65003]), None).unwrap();
assert!(Arc::ptr_eq(&a, &b));
assert!(!Arc::ptr_eq(&a, &c));
assert_eq!(store.len(), 2);
// The next hop is part of what a set is.
let next_hop = NlriNextHop::Ipv6("2001:db8::1".parse().unwrap());
let d = store
.intern(&attributes(vec![65001, 65002]), Some(&next_hop))
.unwrap();
assert!(!Arc::ptr_eq(&a, &d));
assert_eq!(store.len(), 3);
}
#[test]
fn a_set_gives_back_what_went_in() {
let store = AttrStore::new();
let next_hop = NlriNextHop::Ipv6WithLl {
global: "2001:db8::1".parse().unwrap(),
link_local: "fe80::1".parse().unwrap(),
};
let original = attributes(vec![65001, 4_200_000_001]);
let set = store.intern(&original, Some(&next_hop)).unwrap();
assert_eq!(set.attributes(), Some((original, Some(next_hop))));
}
#[test]
fn the_selection_key_is_read_off_the_attributes() {
let store = AttrStore::new();
let mut attrs = vec![
PathAttribute::Origin(OriginPathAttribute::INCOMPLETE),
PathAttribute::AsPath(AsPathAttribute {
segments: vec![
AsPathSegment {
ordered: true,
path: vec![65001, 65002],
},
// A set counts as one hop.
AsPathSegment {
ordered: false,
path: vec![65003, 65004, 65005],
},
],
}),
PathAttribute::LocalPref(LocalPrefPathAttribute(200)),
];
let key = *store.intern(&attrs, None).unwrap().key();
assert_eq!(
key,
SelectionKey {
local_pref: Some(200),
med: None,
neighbor_as: 65001,
as_path_len: 3,
origin: 2,
}
);
attrs.push(PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(7)));
assert_eq!(store.intern(&attrs, None).unwrap().key().med, Some(7));
}
#[test]
fn attributes_on_the_wire_are_interned_without_parsing_them() {
let store = AttrStore::new();
let next_hop = NlriNextHop::Ipv6("2001:db8::1".parse().unwrap());
let mut attrs = attributes(vec![65001, 65002, 65003]);
attrs.push(PathAttribute::LocalPref(LocalPrefPathAttribute(300)));
let parsed = store.intern(&attrs, Some(&next_hop)).unwrap();
// The same set by its bytes is the same set, key and all.
let by_wire = store.intern_wire(parsed.wire()).unwrap();
assert!(Arc::ptr_eq(&parsed, &by_wire));
// And one that arrives as bytes first gets the same key as it would
// have had from the parsed form.
let other = AttrStore::new();
let fresh = other.intern_wire(parsed.wire()).unwrap();
assert_eq!(fresh.key(), parsed.key());
assert_eq!(fresh.key().as_path_len, 3);
assert_eq!(fresh.key().neighbor_as, 65001);
assert_eq!(fresh.attributes(), Some((attrs, Some(next_hop))));
// Bytes that are not attributes are refused.
assert!(other.intern_wire(&[0x40, 0x01, 0x05, 0x00]).is_none());
assert!(
other
.intern_wire(&[0x40, 0x02, 0x03, 0x02, 0x01, 0x00])
.is_none()
);
}
#[test]
fn sweeping_drops_only_sets_nobody_holds() {
let store = AttrStore::new();
let kept = store.intern(&attributes(vec![65001]), None).unwrap();
drop(store.intern(&attributes(vec![65002]), None).unwrap());
assert_eq!(store.len(), 2);
assert_eq!(store.sweep(), 1);
assert_eq!(store.len(), 1);
// The one that is left is still the one that is found.
let again = store.intern(&attributes(vec![65001]), None).unwrap();
assert!(Arc::ptr_eq(&kept, &again));
}
}
+755
View File
@@ -0,0 +1,755 @@
//! The journal: how the rest of the router follows the best routes.
//!
//! Whatever acts on the table's choices (the peers that are sent the best
//! routes, the forwarding table, the status page) has to learn when a best
//! route changes, at its own pace, without the table waiting for it. Each
//! shard keeps a journal for that, and each follower reads it through a
//! [`Reader`].
//!
//! # What the journal holds
//!
//! It records *that* a prefix's best route changed, not what it changed to.
//! A reader that comes to an entry looks the prefix up and is given the best
//! route as it is at that moment. A follower does not want the history; it
//! wants to end up with what the table has now.
//!
//! This is what keeps the journal small. If a prefix changes again before a
//! reader has got to its entry, the older entry no longer says anything: the
//! newer one will deliver the current route. Each row remembers the
//! sequence number of its latest change, an entry with any other number is
//! dead, and dead entries are thrown out when the journal is tidied. So a
//! prefix that flaps a thousand times costs a reader one event, and the
//! journal never has more live entries than the table has prefixes.
//!
//! # Readers that fall behind
//!
//! Entries are dropped once every reader has passed them. A reader that
//! stops reading would hold them all back, so there is a limit: when the
//! journal is too long even after tidying, its oldest half is dropped
//! regardless. A reader that was behind that point is told so the next time
//! it reads ([`Event::Reset`]) and is given the whole table again. Reading
//! too slowly costs the slow reader a resynchronisation; it costs the
//! table, and the other readers, nothing.
//!
//! A new reader starts the same way: `Reset`, every best route, `Synced`,
//! and changes from then on.
//!
//! # All routes, or only the best
//!
//! There are two journals in each shard. One records changes of best route,
//! for followers that want one route for each prefix: the forwarding table,
//! and peers that are sent the best route. The other records every change
//! to any route, for followers that want them all: peers that are sent
//! several routes for a prefix (ADD-PATH, RFC 7911), or a choice made for
//! them alone. A [`Reader`] follows one or the other. The second journal
//! costs nothing while nobody follows it.
//!
//! # Order
//!
//! For any one prefix, a reader is given its states in the order they came
//! about, and always, in the end, the latest. Between prefixes there is no
//! order worth relying on.
use std::collections::VecDeque;
use std::hash::BuildHasher;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use bgp_primitives::ip_prefix::IpPrefix;
use foldhash::fast::RandomState;
use hashbrown::{HashMap, HashTable};
use super::{AttrSet, PeerIndex, RibTable, RouteRef, Row, Shard, read, write};
/// A cursor that holds nothing back: a reader slot that is free, or a
/// reader that has not read this shard yet.
const NO_CURSOR: u64 = u64::MAX;
/// When a journal is tidied, and when it is too long.
#[derive(Debug, Clone, Copy)]
pub(super) struct Limits {
/// Below this many entries a journal is not worth tidying.
pub(super) tidy_at: usize,
/// Room allowed beyond the number of prefixes before the journal is
/// thought too long.
pub(super) slack: usize,
}
impl Default for Limits {
fn default() -> Self {
Self {
tidy_at: 1024,
slack: 64,
}
}
}
struct Entry {
seq: u64,
prefix: IpPrefix,
}
/// One journal of one shard.
struct Journal {
kind: Kind,
/// The sequence number of the latest change. Counts every change this
/// journal is for, whether or not anyone was reading.
seq: u64,
/// Entries up to and including this number were dropped without regard
/// to the readers. A reader whose cursor is below it missed something.
floor: u64,
/// In order of sequence number. Not every number is here: dead entries
/// are removed, and nothing is recorded while there are no readers.
entries: VecDeque<Entry>,
/// Prefixes that have no route any more and whose disappearance is
/// still to be read, with the sequence number of the entry that says
/// so. A row keeps this number itself; a prefix without a row cannot.
gone: HashMap<IpPrefix, u64, RandomState>,
/// Where each reader has read to, by reader slot. Written by the
/// readers themselves while they hold the shard for reading.
cursors: Vec<AtomicU64>,
/// How many reader slots are in use.
readers: usize,
limits: Limits,
}
impl Journal {
fn new(kind: Kind, limits: Limits) -> Self {
Self {
kind,
limits,
seq: 0,
floor: 0,
entries: VecDeque::new(),
gone: HashMap::default(),
cursors: Vec::new(),
readers: 0,
}
}
/// Notes a change to `prefix`, which has a row. Returns the sequence
/// number for the row to remember.
fn changed(&mut self, prefix: IpPrefix) -> u64 {
self.seq += 1;
if self.readers > 0 {
self.entries.push_back(Entry {
seq: self.seq,
prefix,
});
}
self.seq
}
/// As [`Journal::changed`], for a prefix whose row was just made:
/// if it had gone and is back, the note that it had gone is void.
fn appeared(&mut self, prefix: IpPrefix) -> u64 {
if !self.gone.is_empty() {
self.gone.remove(&prefix);
}
self.changed(prefix)
}
/// Notes that `prefix` lost its last route and has no row any more.
fn disappeared(&mut self, prefix: IpPrefix) {
self.seq += 1;
if self.readers > 0 {
self.entries.push_back(Entry {
seq: self.seq,
prefix,
});
self.gone.insert(prefix, self.seq);
}
}
/// Forgets that `entry`'s prefix had gone, if `entry` is what said so.
fn drop_entry(gone: &mut HashMap<IpPrefix, u64, RandomState>, entry: &Entry) {
if gone.get(&entry.prefix) == Some(&entry.seq) {
gone.remove(&entry.prefix);
}
}
/// What `entry` says, if it still says something: it is the latest
/// change of its prefix. The row, or `None` for a prefix that has gone.
fn live<'a>(
&self,
entry: &Entry,
rows: &'a HashTable<Row>,
hasher: &RandomState,
) -> Option<Option<&'a Row>> {
let hash = hasher.hash_one(entry.prefix);
match rows.find(hash, |row| row.prefix == entry.prefix) {
Some(row) => (row.seqs.of(self.kind) == entry.seq).then_some(Some(row)),
None => (self.gone.get(&entry.prefix) == Some(&entry.seq)).then_some(None),
}
}
/// Keeps the journal from growing. Called after every change to the
/// shard; does nothing until there is something worth doing.
fn tidy(&mut self, rows: &HashTable<Row>, hasher: &RandomState) {
if self.entries.len() <= self.limits.tidy_at {
return;
}
// What every reader has passed can go.
let passed = self
.cursors
.iter()
.map(|cursor| cursor.load(Ordering::Acquire))
.min()
.unwrap_or(NO_CURSOR);
while let Some(front) = self.entries.front() {
if front.seq > passed {
break;
}
Self::drop_entry(&mut self.gone, front);
self.entries.pop_front();
}
// Entries for prefixes that have a row are at most as many as the
// rows once the dead ones are out. Entries for prefixes that have
// gone are not counted in: they are what a reader that has stopped
// reading makes pile up.
let limit = 2 * (rows.len() + self.limits.slack);
if self.entries.len() <= limit {
return;
}
// Dead entries: their prefix has changed again since. This looks
// every entry up, but only once the journal has doubled, so the
// cost per change stays small.
let mut entries = std::mem::take(&mut self.entries);
entries.retain(|entry| self.live(entry, rows, hasher).is_some());
self.entries = entries;
if self.entries.len() <= limit {
return;
}
// Still too long: a reader is not reading while prefixes come and
// go, or more prefixes went at once than are left. The older half
// goes, and a reader that had not read it loses its place. Half,
// so that readers that are only a little behind keep theirs, and
// so that getting a long journal down to size takes few passes.
for entry in self.entries.drain(..self.entries.len() / 2) {
Self::drop_entry(&mut self.gone, &entry);
self.floor = entry.seq;
}
}
}
/// Which of a shard's two journals.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Kind {
/// Changes of best route.
Best,
/// Changes of any route.
All,
}
/// The sequence numbers a row remembers, one for each journal: of the
/// latest change of its best route, and of any of its routes.
#[derive(Debug, Clone, Copy)]
pub(super) struct Seqs {
best: u64,
all: u64,
}
impl Seqs {
fn of(self, kind: Kind) -> u64 {
match kind {
Kind::Best => self.best,
Kind::All => self.all,
}
}
}
/// The two journals of a shard, written together: every change to a row is
/// a change of one of its routes, and some are a change of its best route.
pub(super) struct Journals {
best: Journal,
all: Journal,
}
impl Journals {
pub(super) fn new(limits: Limits) -> Self {
Self {
best: Journal::new(Kind::Best, limits),
all: Journal::new(Kind::All, limits),
}
}
fn of(&self, kind: Kind) -> &Journal {
match kind {
Kind::Best => &self.best,
Kind::All => &self.all,
}
}
fn of_mut(&mut self, kind: Kind) -> &mut Journal {
match kind {
Kind::Best => &mut self.best,
Kind::All => &mut self.all,
}
}
/// Notes that `prefix` has its first route, and so a row. Returns the
/// sequence numbers for the new row.
pub(super) fn appeared(&mut self, prefix: IpPrefix) -> Seqs {
Seqs {
best: self.best.appeared(prefix),
all: self.all.appeared(prefix),
}
}
/// Notes that a route of `row` changed, and whether its best route did.
pub(super) fn changed(&mut self, row: &mut Row, best_changed: bool) {
if best_changed {
row.seqs.best = self.best.changed(row.prefix);
}
row.seqs.all = self.all.changed(row.prefix);
}
/// Notes that `prefix` lost its last route and has no row any more.
pub(super) fn disappeared(&mut self, prefix: IpPrefix) {
self.best.disappeared(prefix);
self.all.disappeared(prefix);
}
/// Keeps the journals from growing; see [`Journal::tidy`].
pub(super) fn tidy(&mut self, rows: &HashTable<Row>, hasher: &RandomState) {
self.best.tidy(rows, hasher);
self.all.tidy(rows, hasher);
}
/// Where the journals are now, to ask [`Journals::has_news_since`]
/// about later.
pub(super) fn mark(&self) -> Mark {
Mark(self.best.seq, self.all.seq)
}
/// Whether anything was written since `mark` that some reader is there
/// to read.
pub(super) fn has_news_since(&self, mark: Mark) -> bool {
(self.best.readers > 0 && self.best.seq != mark.0)
|| (self.all.readers > 0 && self.all.seq != mark.1)
}
fn len(&self) -> usize {
self.best.entries.len() + self.all.entries.len()
}
}
/// A point in a shard's journals; see [`Journals::mark`].
#[derive(Clone, Copy)]
pub(super) struct Mark(u64, u64);
/// The routes of one prefix, as a follower of all routes is given them.
#[derive(Clone, Copy)]
pub struct Routes<'a>(&'a Row);
impl<'a> Routes<'a> {
pub fn iter(&self) -> impl Iterator<Item = RouteRef<'a>> + use<'a> {
self.0.routes()
}
/// How many there are: at least one.
pub fn len(&self) -> usize {
self.0.paths.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl std::fmt::Debug for Routes<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_list().entries(self.iter()).finish()
}
}
/// What a [`Reader`] is given.
#[derive(Debug, Clone, Copy)]
pub enum Event<'a> {
/// Forget what you know. The whole table follows, as [`Event::Best`]
/// or [`Event::Routes`] for every prefix, and then [`Event::Synced`].
/// The first thing a new
/// reader is given, and what a reader that fell too far behind is
/// given in place of what it missed.
Reset,
/// This is now the best route for `prefix`. Given to a follower of the
/// best routes.
Best {
prefix: IpPrefix,
peer: PeerIndex,
/// The peer's identifier for the route: zero unless the peer
/// sends several routes for a prefix (ADD-PATH, RFC 7911).
path_id: u32,
attrs: &'a Arc<AttrSet>,
},
/// These are now all the routes for `prefix`: whatever was known of its
/// routes before is replaced. Given to a follower of all routes.
Routes {
prefix: IpPrefix,
routes: Routes<'a>,
},
/// `prefix` has no route any more.
Withdrawn { prefix: IpPrefix },
/// The table that began with [`Event::Reset`] is complete: a prefix
/// not heard of since the reset has no route.
Synced,
}
/// What a call to [`Reader::poll`] got done.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Progress {
/// How many events were given out.
pub events: usize,
/// Whether the reader has now been given everything there was when the
/// call began. If not, call again.
pub caught_up: bool,
}
enum Phase {
/// Giving out the whole table; `next` is the shard to do next, and
/// `announced` whether [`Event::Reset`] has been given.
Resync { next: usize, announced: bool },
/// Following the journals; `next` is the shard to read next.
Follow { next: usize },
}
impl Phase {
/// The beginning of a resynchronisation.
const RESYNC: Self = Self::Resync {
next: 0,
announced: false,
};
}
/// Follows the routes of a table: the best of each prefix, or all of them.
/// Made by [`RibTable::reader`] and its siblings.
///
/// A reader holds its place in every shard's journal. Dropping it gives
/// the place up, so that the journals need not keep entries for it.
pub struct Reader {
table: Arc<RibTable>,
/// Which journal it follows.
kind: Kind,
slot: usize,
/// Where this reader has read to in each shard; the copy in the shard
/// is for the shard's benefit.
cursors: Vec<u64>,
phase: Phase,
/// The table's version when the reader was last found caught up.
seen: u64,
caught_up: bool,
}
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(Kind::Best, Phase::RESYNC)
}
/// 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(Kind::Best, Phase::Follow { next: 0 });
for (index, shard) in self.shards.iter().enumerate() {
let shard = read(shard);
reader.set_cursor(index, &shard, shard.journals.best.seq);
}
reader
}
/// A new follower of every route in the table, not only the best: it
/// is told whenever any route of a prefix changes, with all the
/// prefix's routes ([`Event::Routes`]). The whole table first, like
/// [`RibTable::reader`].
pub fn reader_of_all_routes(self: &Arc<Self>) -> Reader {
self.register(Kind::All, Phase::RESYNC)
}
fn register(self: &Arc<Self>, kind: Kind, phase: Phase) -> Reader {
let mut slots = self
.reader_slots
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let slot = match slots.iter().position(|used| !used) {
Some(free) => free,
None => {
slots.push(false);
slots.len() - 1
}
};
slots[slot] = true;
for shard in &self.shards {
let mut shard = write(shard);
let journal = shard.journals.of_mut(kind);
while journal.cursors.len() <= slot {
journal.cursors.push(AtomicU64::new(NO_CURSOR));
}
journal.cursors[slot].store(NO_CURSOR, Ordering::Release);
journal.readers += 1;
}
self.readers.fetch_add(1, Ordering::AcqRel);
Reader {
table: Arc::clone(self),
kind,
slot,
cursors: vec![NO_CURSOR; self.shards.len()],
phase,
seen: 0,
caught_up: false,
}
}
/// How many entries the journals hold, over all shards. For tests and
/// for watching: it stays bounded whatever the readers do.
pub fn journal_len(&self) -> usize {
self.shards
.iter()
.map(|shard| read(shard).journals.len())
.sum()
}
/// Tells readers that are waiting that there is something to read.
pub(super) fn wake_readers(&self) {
if self.readers.load(Ordering::Acquire) > 0 {
self.version.fetch_add(1, Ordering::AcqRel);
self.wake.notify_waiters();
}
}
}
impl Drop for Reader {
fn drop(&mut self) {
for shard in &self.table.shards {
let mut shard = write(shard);
let journal = shard.journals.of_mut(self.kind);
journal.cursors[self.slot].store(NO_CURSOR, Ordering::Release);
journal.readers -= 1;
if journal.readers == 0 {
// Nobody left to read it.
journal.entries.clear();
journal.gone.clear();
}
}
self.table.readers.fetch_sub(1, Ordering::AcqRel);
let mut slots = self
.table
.reader_slots
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
slots[self.slot] = false;
}
}
enum Drained {
/// Entries were dropped that this reader had not read.
Lagged,
/// `events` were given out; `more` if the budget ran out first.
Read { events: usize, more: bool },
}
impl Reader {
/// Gives `visit` what has changed since the last call, in up to about
/// `budget` events (a shard's part of a resynchronisation is not cut
/// short, so it can be more).
///
/// `visit` is called with a shard of the table held for reading. It
/// should be quick, and it must not call into the table, even to read:
/// taking the shard's lock again can wait for ever on a writer that is
/// waiting for this hold to end.
pub fn poll(&mut self, budget: usize, mut visit: impl FnMut(Event<'_>)) -> Progress {
// A call that may give nothing out would never get anywhere.
let budget = budget.max(1);
let shards = self.table.shards.len();
let mut events = 0;
loop {
match self.phase {
Phase::Resync { next, announced } => {
if !announced {
self.forget_place();
visit(Event::Reset);
events += 1;
}
if next == shards {
visit(Event::Synced);
events += 1;
self.phase = Phase::Follow { next: 0 };
continue;
}
events += self.scan(next, &mut visit);
self.phase = Phase::Resync {
next: next + 1,
announced: true,
};
if events >= budget {
return self.progress(events, false);
}
}
Phase::Follow { next } => {
// Read before the pass: a change made during it shows as
// a newer version, and `changed` then returns at once.
let version = if next == 0 {
self.table.version.load(Ordering::Acquire)
} else {
self.seen
};
self.seen = version;
let mut shard = next;
let mut lagged = false;
while shard < shards {
if events >= budget {
self.phase = Phase::Follow { next: shard };
return self.progress(events, false);
}
match self.drain(shard, budget - events, &mut visit) {
Drained::Lagged => {
lagged = true;
break;
}
Drained::Read { events: read, more } => {
events += read;
if more {
self.phase = Phase::Follow { next: shard };
return self.progress(events, false);
}
}
}
shard += 1;
}
if lagged {
self.phase = Phase::RESYNC;
continue;
}
self.phase = Phase::Follow { next: 0 };
if next != 0 {
// This pass took up where an earlier call stopped.
// The shards before that point have not been looked
// at in this call, so go round again from the first.
continue;
}
return self.progress(events, true);
}
}
}
}
fn progress(&mut self, events: usize, caught_up: bool) -> Progress {
self.caught_up = caught_up;
Progress { events, caught_up }
}
/// Waits until there may be something to read. Returns at once if the
/// last [`Reader::poll`] did not catch up.
pub async fn changed(&mut self) {
loop {
// Registered before the check, so that a change between the
// check and the wait is not missed.
let woken = self.table.wake.notified();
tokio::pin!(woken);
woken.as_mut().enable();
if !self.caught_up || self.table.version.load(Ordering::Acquire) != self.seen {
return;
}
woken.await;
}
}
/// What this reader is told about a row.
fn event_for<'a>(&self, row: &'a Row) -> Event<'a> {
match self.kind {
Kind::Best => {
let best = row.route(row.best());
Event::Best {
prefix: row.prefix,
peer: best.peer,
path_id: best.path_id,
attrs: best.attrs,
}
}
Kind::All => Event::Routes {
prefix: row.prefix,
routes: Routes(row),
},
}
}
/// Gives up this reader's place in every journal, before a
/// resynchronisation takes a new one.
fn forget_place(&mut self) {
for (index, shard) in self.table.shards.iter().enumerate() {
read(shard).journals.of(self.kind).cursors[self.slot]
.store(NO_CURSOR, Ordering::Release);
self.cursors[index] = NO_CURSOR;
}
}
fn set_cursor(&mut self, index: usize, shard: &Shard, cursor: u64) {
self.cursors[index] = cursor;
shard.journals.of(self.kind).cursors[self.slot].store(cursor, Ordering::Release);
}
/// Gives out every prefix in a shard and takes a place at the end of
/// its journal, in one hold of the shard: what changes afterwards is in
/// the journal after that place.
fn scan(&mut self, index: usize, visit: &mut impl FnMut(Event<'_>)) -> usize {
let table = Arc::clone(&self.table);
let shard = read(&table.shards[index]);
for row in &shard.rows {
visit(self.event_for(row));
}
self.set_cursor(index, &shard, shard.journals.of(self.kind).seq);
shard.rows.len()
}
/// Reads a shard's journal from this reader's place, for at most `max`
/// events.
fn drain(&mut self, index: usize, max: usize, visit: &mut impl FnMut(Event<'_>)) -> Drained {
let table = Arc::clone(&self.table);
let shard = read(&table.shards[index]);
let journal = shard.journals.of(self.kind);
let mut cursor = self.cursors[index];
if cursor < journal.floor {
return Drained::Lagged;
}
if cursor == journal.seq {
return Drained::Read {
events: 0,
more: false,
};
}
let start = journal.entries.partition_point(|entry| entry.seq <= cursor);
let mut events = 0;
let mut more = false;
for entry in journal.entries.range(start..) {
if events == max {
more = true;
break;
}
cursor = entry.seq;
let event = match journal.live(entry, &shard.rows, &shard.hasher) {
Some(Some(row)) => self.event_for(row),
Some(None) => Event::Withdrawn {
prefix: entry.prefix,
},
// The prefix has changed again; a later entry has it.
None => continue,
};
visit(event);
events += 1;
}
if !more {
// Past the last entry there is nothing up to the latest change.
cursor = journal.seq;
}
self.set_cursor(index, &shard, cursor);
Drained::Read { events, more }
}
}
#[cfg(test)]
#[path = "journal_tests.rs"]
mod tests;
+713
View File
@@ -0,0 +1,713 @@
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use bgp_primitives::constants::IpVersion;
use bgp_primitives::ip_prefix::IpPrefix;
use proptest::prelude::*;
use super::super::tests::{Fixture, Route, external, internal, prefix, route};
use super::super::{AttrSet, PeerIndex, PeerInfo, RibTable};
use super::{Event, Limits, Reader};
/// What a follower would keep: its own copy of the best routes, brought
/// up to date from the events it is given.
#[derive(Default)]
struct Mirror {
best: HashMap<IpPrefix, (PeerIndex, Arc<AttrSet>)>,
/// What was held before a reset and has not been heard of since.
stale: HashMap<IpPrefix, (PeerIndex, Arc<AttrSet>)>,
/// For a follower of all routes: every route of every prefix, as
/// (peer, path identifier, attributes, whether best).
routes: HashMap<IpPrefix, Vec<Held>>,
resets: usize,
synced: usize,
best_events: usize,
routes_events: usize,
withdrawn_events: usize,
}
/// A route as a mirror holds it. The attributes are held, so that the
/// address used to compare them stays theirs.
type Held = (PeerIndex, u32, Arc<AttrSet>, bool);
fn same_routes(a: &[Held], b: &[Held]) -> bool {
a.len() == b.len()
&& a.iter()
.zip(b)
.all(|(a, b)| (a.0, a.1, a.3) == (b.0, b.1, b.3) && Arc::ptr_eq(&a.2, &b.2))
}
fn sorted(mut routes: Vec<Held>) -> Vec<Held> {
routes.sort_by_key(|(peer, path_id, _, _)| (*peer, *path_id));
routes
}
impl Mirror {
fn apply(&mut self, event: Event<'_>) {
match event {
Event::Reset => {
self.resets += 1;
self.stale = std::mem::take(&mut self.best);
self.routes.clear();
}
Event::Routes { prefix, routes } => {
self.routes_events += 1;
assert!(!routes.is_empty(), "a prefix with no routes is withdrawn");
self.routes.insert(
prefix,
sorted(
routes
.iter()
.map(|r| (r.peer, r.path_id, Arc::clone(r.attrs), r.best))
.collect(),
),
);
}
Event::Best {
prefix,
peer,
attrs,
..
} => {
self.best_events += 1;
self.stale.remove(&prefix);
self.best.insert(prefix, (peer, Arc::clone(attrs)));
}
Event::Withdrawn { prefix } => {
self.withdrawn_events += 1;
self.best.remove(&prefix);
self.routes.remove(&prefix);
}
Event::Synced => {
self.synced += 1;
self.stale.clear();
}
}
}
/// Reads until there is nothing more, in steps of `budget`.
fn catch_up(&mut self, reader: &mut Reader, budget: usize) {
while !reader.poll(budget, |event| self.apply(event)).caught_up {}
}
#[track_caller]
fn assert_matches(&self, table: &RibTable) {
let mut expected = HashMap::new();
table.for_each_best(|prefix, peer, attrs| {
expected.insert(*prefix, (peer, Arc::clone(attrs)));
});
assert_eq!(
self.best.len(),
expected.len(),
"the mirror has {} prefixes, the table {}",
self.best.len(),
expected.len()
);
for (prefix, (peer, attrs)) in &expected {
let Some((mirrored_peer, mirrored_attrs)) = self.best.get(prefix) else {
panic!("{prefix} is in the table and not in the mirror");
};
assert_eq!(mirrored_peer, peer, "{prefix}: from the wrong peer");
assert!(
Arc::ptr_eq(mirrored_attrs, attrs),
"{prefix}: wrong attributes"
);
}
}
}
impl Mirror {
/// For the mirror of a follower of all routes: it holds every route in
/// the table, and knows which is best.
#[track_caller]
fn assert_has_all_routes_of(&self, table: &RibTable) {
let mut expected: HashMap<IpPrefix, Vec<Held>> = HashMap::new();
table.for_each_route(|r| {
expected.entry(r.prefix).or_default().push((
r.peer,
r.path_id,
Arc::clone(r.attrs),
r.best,
));
});
assert_eq!(self.routes.len(), expected.len(), "prefixes");
for (prefix, routes) in expected {
let mirrored = self
.routes
.get(&prefix)
.unwrap_or_else(|| panic!("{prefix} is in the table and not in the mirror"));
assert!(
same_routes(mirrored, &sorted(routes)),
"{prefix}: the mirror's routes are not the table's"
);
}
}
}
fn nth_prefix(n: u32) -> IpPrefix {
IpPrefix::new(IpVersion::Ipv4, (n << 8).to_be_bytes(), 24).unwrap()
}
#[test]
fn a_new_reader_is_given_the_table_and_then_the_changes() {
let fixture = Fixture::new(&[external(1), external(2)]);
let (a, b) = (prefix("10.0.0.0/8"), prefix("2001:db8::/32"));
fixture.announce(0, &a, &Route::via(&[1, 2]));
fixture.announce(0, &b, &Route::via(&[1, 2]));
let mut reader = fixture.table.reader();
let mut mirror = Mirror::default();
let mut events = Vec::new();
let progress = reader.poll(100, |event| {
events.push(
format!("{event:?}")
.split([' ', '{'])
.next()
.unwrap()
.to_string(),
);
mirror.apply(event);
});
assert!(progress.caught_up);
assert_eq!(events, ["Reset", "Best", "Best", "Synced"]);
mirror.assert_matches(&fixture.table);
// Nothing has happened: nothing to read.
assert_eq!(reader.poll(100, |_| panic!("no event expected")).events, 0);
// A change that does not change the best route is not an event.
fixture.announce(1, &a, &Route::via(&[3, 4, 5]));
assert_eq!(reader.poll(100, |_| panic!("no event expected")).events, 0);
fixture.announce(1, &a, &Route::via(&[3]));
fixture.withdraw(0, &b);
mirror.catch_up(&mut reader, 100);
assert_eq!((mirror.best_events, mirror.withdrawn_events), (3, 1));
assert_eq!((mirror.resets, mirror.synced), (1, 1));
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 change to a route that is not the best is nothing to a follower of
/// the best routes, and is what a follower of all routes is there for.
#[test]
fn a_follower_of_all_routes_is_told_of_every_change() {
let fixture = Fixture::new(&[external(1), external(2)]);
let p = prefix("10.0.0.0/8");
fixture.announce(0, &p, &Route::via(&[1]));
let mut best = fixture.table.reader();
let mut all = fixture.table.reader_of_all_routes();
let (mut best_mirror, mut all_mirror) = (Mirror::default(), Mirror::default());
best_mirror.catch_up(&mut best, 100);
all_mirror.catch_up(&mut all, 100);
assert_eq!((best_mirror.best_events, all_mirror.routes_events), (1, 1));
// A worse route from another peer, then two more from that peer under
// other path identifiers, one of them changed, one withdrawn.
fixture.announce(1, &p, &Route::via(&[2, 3, 4]));
fixture.announce_path(1, &p, 7, &Route::via(&[2, 3, 5]));
fixture.announce_path(1, &p, 8, &Route::via(&[2, 3, 6]));
fixture.announce_path(1, &p, 7, &Route::via(&[2, 9, 5]));
fixture.withdraw_path(1, &p, 8);
assert_eq!(
best.poll(100, |event| panic!("unexpected {event:?}"))
.events,
0
);
all_mirror.catch_up(&mut all, 100);
// All of it between two reads is one event, with the routes as they
// are now.
assert_eq!(all_mirror.routes_events, 2);
assert_eq!(all_mirror.routes[&p].len(), 3);
all_mirror.assert_has_all_routes_of(&fixture.table);
// The last route going is a withdrawal for both.
fixture.table.remove_peer(fixture.peers[1]);
fixture.withdraw(0, &p);
best_mirror.catch_up(&mut best, 100);
all_mirror.catch_up(&mut all, 100);
assert_eq!(
(best_mirror.withdrawn_events, all_mirror.withdrawn_events),
(1, 1)
);
all_mirror.assert_has_all_routes_of(&fixture.table);
// Nobody following all routes: that journal keeps nothing. A change
// that is not of the best route is then recorded nowhere.
drop(all);
fixture.announce(0, &p, &Route::via(&[1]));
fixture.announce(1, &p, &Route::via(&[2, 3]));
let before = fixture.table.journal_len();
fixture.announce(1, &p, &Route::via(&[2, 4]));
fixture.announce_path(1, &p, 7, &Route::via(&[2, 5]));
assert_eq!(fixture.table.journal_len(), before);
}
/// 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() {
let fixture = Fixture::new(&[external(1), external(2)]);
let p = prefix("10.0.0.0/8");
let mut reader = fixture.table.reader();
let mut mirror = Mirror::default();
mirror.catch_up(&mut reader, 100);
for round in 0..50 {
fixture.announce(0, &p, &Route::via(&[1, 2, 3]));
fixture.announce(1, &p, &Route::via(&[4, 5]));
fixture.withdraw(1, &p);
if round % 2 == 0 {
fixture.withdraw(0, &p);
}
}
// It ended with peer 0's route in place.
mirror.catch_up(&mut reader, 100);
assert_eq!((mirror.best_events, mirror.withdrawn_events), (1, 0));
mirror.assert_matches(&fixture.table);
// And a prefix that came and went again is, at most, a withdrawal.
let q = prefix("10.1.0.0/16");
for _ in 0..50 {
fixture.announce(0, &q, &Route::via(&[1]));
fixture.withdraw(0, &q);
}
mirror.catch_up(&mut reader, 100);
assert_eq!((mirror.best_events, mirror.withdrawn_events), (1, 1));
mirror.assert_matches(&fixture.table);
}
#[test]
fn reading_in_small_steps_gives_the_same_as_reading_at_once() {
let fixture = Fixture::new(&[external(1)]);
for n in 0..500 {
fixture.announce(0, &nth_prefix(n), &Route::via(&[1, n % 7 + 1]));
}
let mut reader = fixture.table.reader();
let mut mirror = Mirror::default();
mirror.catch_up(&mut reader, 7);
mirror.assert_matches(&fixture.table);
for n in 0..500 {
if n % 3 == 0 {
fixture.withdraw(0, &nth_prefix(n));
} else {
fixture.announce(0, &nth_prefix(n), &Route::via(&[2, n % 5 + 1]));
}
}
let mut steps = 0;
while !reader.poll(7, |event| mirror.apply(event)).caught_up {
steps += 1;
}
assert!(steps > 50, "the budget was kept to: {steps} steps");
mirror.assert_matches(&fixture.table);
assert_eq!(mirror.resets, 1);
}
#[test]
fn with_no_reader_nothing_is_kept() {
let fixture = Fixture::new(&[external(1)]);
for n in 0..5000 {
fixture.announce(0, &nth_prefix(n), &Route::via(&[1]));
fixture.withdraw(0, &nth_prefix(n));
}
assert_eq!(fixture.table.journal_len(), 0);
// And what a reader was holding back goes when the reader does.
let reader = fixture.table.reader();
for n in 0..100 {
fixture.announce(0, &nth_prefix(n), &Route::via(&[1]));
}
assert_eq!(fixture.table.journal_len(), 100);
drop(reader);
assert_eq!(fixture.table.journal_len(), 0);
}
/// A reader that stops reading must not make the table keep a growing
/// journal for it. It is the one that pays: it is given the table again.
#[test]
fn a_reader_that_stops_reading_is_reset_and_holds_nothing_up() {
// One shard, so that the journal's length is that shard's.
let fixture = Fixture::on(RibTable::new(1), &[external(1)]);
fixture.announce(0, &prefix("192.0.2.0/24"), &Route::via(&[1]));
let mut stuck = fixture.table.reader();
let mut stuck_mirror = Mirror::default();
stuck_mirror.catch_up(&mut stuck, 100);
let mut diligent = fixture.table.reader();
let mut diligent_mirror = Mirror::default();
// Prefixes come and go, each a different one, so no entry is made
// dead by a later one for the same prefix.
let limits = Limits::default();
let mut longest = 0;
for n in 0..20_000 {
fixture.announce(0, &nth_prefix(n), &Route::via(&[1]));
fixture.withdraw(0, &nth_prefix(n));
longest = longest.max(fixture.table.journal_len());
if n % 100 == 0 {
diligent_mirror.catch_up(&mut diligent, 1000);
}
}
assert!(
longest <= limits.tidy_at.max(2 * (1 + limits.slack)) + 2,
"the journal reached {longest} entries"
);
// The one that kept up never noticed.
diligent_mirror.catch_up(&mut diligent, 1000);
assert_eq!(
diligent_mirror.resets, 1,
"only the reset every reader starts with"
);
diligent_mirror.assert_matches(&fixture.table);
// The one that did not is told, and ends up right all the same.
stuck_mirror.catch_up(&mut stuck, 1000);
assert_eq!(stuck_mirror.resets, 2);
assert_eq!(stuck_mirror.synced, 2);
stuck_mirror.assert_matches(&fixture.table);
}
/// When a peer that was the only source of most prefixes goes, more
/// prefixes disappear at once than are left. The journal still comes down
/// to its bound, and a reader ends up with what the table has.
#[test]
fn many_prefixes_going_at_once_leaves_the_journal_bounded() {
let fixture = Fixture::on(RibTable::new(1), &[external(1), external(2)]);
for n in 0..5000 {
fixture.announce(0, &nth_prefix(n), &Route::via(&[1]));
}
fixture.announce(1, &nth_prefix(0), &Route::via(&[2, 3]));
let mut reader = fixture.table.reader();
let mut mirror = Mirror::default();
mirror.catch_up(&mut reader, 1000);
fixture.table.remove_peer(fixture.peers[0]);
// Some later change, as there always is, lets the journal be tidied.
for _ in 0..8 {
fixture.announce(1, &nth_prefix(1), &Route::via(&[2]));
fixture.withdraw(1, &nth_prefix(1));
}
let limits = Limits::default();
assert!(
fixture.table.journal_len() <= limits.tidy_at.max(2 * (1 + limits.slack)) + 2,
"the journal holds {} entries",
fixture.table.journal_len()
);
mirror.catch_up(&mut reader, 1000);
mirror.assert_matches(&fixture.table);
assert_eq!(mirror.best.len(), 1);
}
/// Readers are woken to read. An announcement that changes nothing, or
/// nothing a reader follows, writes nothing for them and must not wake
/// them: each would look through every shard to find that out.
#[test]
fn readers_are_not_woken_when_nothing_was_written_for_them() {
let fixture = Fixture::new(&[external(1), external(2)]);
let p = prefix("10.0.0.0/8");
let version = || fixture.table.version.load(Ordering::Acquire);
// Nobody reading: nobody to wake.
fixture.announce(0, &p, &Route::via(&[1]));
assert_eq!(version(), 0);
let _best = fixture.table.reader();
let before = version();
// The same route again.
fixture.announce(0, &p, &Route::via(&[1]));
assert_eq!(version(), before);
// A worse route from another peer: the best route stands.
fixture.announce(1, &p, &Route::via(&[2, 3, 4]));
assert_eq!(version(), before);
// A better one.
fixture.announce(1, &p, &Route::via(&[]));
assert_eq!(version(), before + 1);
// A follower of all routes is told of any change to a route.
let _all = fixture.table.reader_of_all_routes();
let before = version();
fixture.announce(0, &p, &Route::via(&[1, 1, 1]));
assert_eq!(version(), before + 1);
fixture.announce(0, &p, &Route::via(&[1, 1, 1]));
assert_eq!(version(), before + 1);
}
/// A budget of nothing would let a reader ask for ever without getting
/// anywhere; it is taken as the smallest that does.
#[test]
fn a_budget_of_nothing_still_makes_progress() {
let fixture = Fixture::new(&[external(1)]);
for n in 0..20 {
fixture.announce(0, &nth_prefix(n), &Route::via(&[1]));
}
let mut reader = fixture.table.reader();
let mut mirror = Mirror::default();
mirror.catch_up(&mut reader, 0);
for n in 0..20 {
fixture.withdraw(0, &nth_prefix(n));
}
mirror.catch_up(&mut reader, 0);
mirror.assert_matches(&fixture.table);
assert!(mirror.best.is_empty());
}
#[tokio::test]
async fn a_waiting_reader_is_woken_by_a_change_of_best_route() {
let fixture = Fixture::new(&[external(1), external(2)]);
let p = prefix("10.0.0.0/8");
let mut reader = fixture.table.reader();
let mut mirror = Mirror::default();
// Not caught up yet: no waiting.
reader.changed().await;
mirror.catch_up(&mut reader, 100);
let waiting = tokio::time::timeout(std::time::Duration::from_millis(50), reader.changed());
assert!(waiting.await.is_err(), "nothing changed, so it waits");
let table = Arc::clone(&fixture.table);
let attrs = table
.attrs()
.intern(&Route::via(&[1]).attributes(), None)
.unwrap();
let peer = fixture.peers[0];
let writer = tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
table.announce(peer, &attrs, &[p]);
});
tokio::time::timeout(std::time::Duration::from_secs(5), reader.changed())
.await
.expect("woken by the announcement");
writer.await.unwrap();
mirror.catch_up(&mut reader, 100);
mirror.assert_matches(&fixture.table);
// A change made before the wait began is not missed either.
fixture.withdraw(0, &p);
tokio::time::timeout(std::time::Duration::from_secs(5), reader.changed())
.await
.expect("the change was already there");
}
/// Writers on several threads, a reader on another, reading while they
/// write. When the writers are done and the reader has caught up, it has
/// what the table has.
#[test]
fn a_reader_keeps_up_with_writers_on_other_threads() {
const WRITERS: usize = 4;
const PREFIXES: u32 = 300;
let infos: Vec<PeerInfo> = (0..WRITERS).map(|i| external(i as u8 + 1)).collect();
// Small journals, so that tidying and falling behind happen too.
let fixture = Fixture::on(
RibTable::with_journal_limits(
8,
Limits {
tidy_at: 16,
slack: 4,
},
),
&infos,
);
let table = &fixture.table;
let done = AtomicBool::new(false);
let mut mirror = Mirror::default();
std::thread::scope(|scope| {
let writers: Vec<_> = (0..WRITERS)
.map(|writer| {
let peer = fixture.peers[writer];
scope.spawn(move || {
// A simple generator; each writer its own sequence.
let mut state = 0x9e37_79b9_7f4a_7c15u64.wrapping_mul(writer as u64 + 1);
let mut next = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
for _ in 0..30_000 {
let p = nth_prefix((next() % u64::from(PREFIXES)) as u32);
if next() % 3 == 0 {
table.withdraw(peer, &[p]);
} else {
let route = Route {
med: Some((next() % 3) as u32),
..Route::via(
&[(next() % 3) as u32 + 1, 9][..(next() % 2 + 1) as usize],
)
};
let attrs = table.attrs().intern(&route.attributes(), None).unwrap();
table.announce(peer, &attrs, &[p]);
}
}
})
})
.collect();
let mirror = &mut mirror;
let done = &done;
let reading = scope.spawn(move || {
let mut reader = table.reader();
while !done.load(Ordering::Acquire) {
reader.poll(50, |event| mirror.apply(event));
}
mirror.catch_up(&mut reader, 50);
});
for writer in writers {
writer.join().unwrap();
}
done.store(true, Ordering::Release);
reading.join().unwrap();
});
mirror.assert_matches(table);
assert!(mirror.best_events > 0);
}
// ---------------------------------------------------------------------------
// Readers against the table, under any interleaving
// ---------------------------------------------------------------------------
#[derive(Debug, Clone)]
enum Op {
Announce {
peer: usize,
prefix: usize,
path_id: u32,
route: Route,
},
Withdraw {
peer: usize,
prefix: usize,
path_id: u32,
},
Flap {
peer: usize,
},
Poll {
reader: usize,
budget: usize,
},
/// The reader is dropped and a new one takes its place.
Replace {
reader: usize,
},
}
const PEERS: usize = 4;
const PREFIXES: usize = 12;
// The first three follow the best routes, the others all routes.
const READERS: usize = 5;
const OF_BEST: usize = 3;
fn op() -> impl Strategy<Value = Op> {
prop_oneof![
8 => (0..PEERS, 0..PREFIXES, 0u32..3, route()).prop_map(
|(peer, prefix, path_id, route)| Op::Announce { peer, prefix, path_id, route }
),
5 => (0..PEERS, 0..PREFIXES, 0u32..3)
.prop_map(|(peer, prefix, path_id)| Op::Withdraw { peer, prefix, path_id }),
1 => (0..PEERS).prop_map(|peer| Op::Flap { peer }),
// Reader 0 is polled rarely: it is the one that falls behind.
4 => (1..READERS, 1usize..6).prop_map(|(reader, budget)| Op::Poll { reader, budget }),
1 => (0..READERS, 1usize..6).prop_map(|(reader, budget)| Op::Poll { reader, budget }),
1 => (1..READERS).prop_map(|reader| Op::Replace { reader }),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(1500))]
/// However changes and reads are interleaved, and however small the
/// journals: a reader that has caught up holds exactly the table's best
/// routes, and every reader gets there in the end.
#[test]
fn a_reader_that_has_caught_up_has_what_the_table_has(
ops in proptest::collection::vec(op(), 1..150),
shards in 1usize..4,
) {
let infos: Vec<PeerInfo> = (0..PEERS)
.map(|i| if i == 0 { internal(9) } else { external(i as u8) })
.collect();
// Journals that are tidied, and found too long, almost at once.
let limits = Limits { tidy_at: 3, slack: 1 };
let mut fixture = Fixture::on(RibTable::with_journal_limits(shards, limits), &infos);
let prefixes: Vec<IpPrefix> = (0..PREFIXES as u32).map(nth_prefix).collect();
let new_reader = |table: &Arc<RibTable>, index: usize| {
if index < OF_BEST { table.reader() } else { table.reader_of_all_routes() }
};
let check = |index: usize, mirror: &Mirror, table: &RibTable| {
if index < OF_BEST {
mirror.assert_matches(table);
} else {
mirror.assert_has_all_routes_of(table);
}
};
let mut readers: Vec<(Reader, Mirror)> = (0..READERS)
.map(|index| (new_reader(&fixture.table, index), Mirror::default()))
.collect();
for op in ops {
match op {
Op::Announce { peer, prefix, path_id, route } => {
fixture.announce_path(peer, &prefixes[prefix], path_id, &route);
}
Op::Withdraw { peer, prefix, path_id } => {
fixture.withdraw_path(peer, &prefixes[prefix], path_id);
}
Op::Flap { peer } => {
fixture.table.remove_peer(fixture.peers[peer]);
fixture.peers[peer] = fixture.table.add_peer(infos[peer]).unwrap();
}
Op::Poll { reader: index, budget } => {
let (reader, mirror) = &mut readers[index];
let progress = reader.poll(budget, |event| mirror.apply(event));
if progress.caught_up {
check(index, mirror, &fixture.table);
}
}
Op::Replace { reader } => {
readers[reader] = (new_reader(&fixture.table, reader), Mirror::default());
}
}
// Never much longer than the table, whatever the readers do.
// Two journals to a shard.
let bound = 2 * shards * (2 * (PREFIXES + limits.slack) + limits.tidy_at + PREFIXES);
prop_assert!(fixture.table.journal_len() <= bound);
}
for (index, (reader, mirror)) in readers.iter_mut().enumerate() {
mirror.catch_up(reader, 5);
check(index, mirror, &fixture.table);
prop_assert_eq!(mirror.resets, mirror.synced);
}
}
}
+728
View File
@@ -0,0 +1,728 @@
//! The routing table: every route every peer has announced, and for each
//! prefix the one chosen as best.
//!
//! This is the Adj-RIBs-In and the Loc-RIB of RFC 4271 §3.2 in one
//! structure, organised by prefix: a prefix's row holds the routes for it
//! from all peers, side by side, and which of them is best. Choosing again
//! after a change looks at one row and nothing else.
//!
//! It is built for a table of hundreds of full feeds, hundreds of millions
//! of routes, taken in on many cores at once:
//!
//! - **A route is small**: sixteen bytes. A pointer to its attribute set,
//! which is shared with every other route that has the same attributes
//! ([`attrs`]), and one word that holds the peer it is from and the first
//! steps of route selection as a single number to compare.
//! - **The table is in shards**, by hash of the prefix, each behind its own
//! lock. A session applies an UPDATE itself, on its own thread, taking
//! the locks of the few shards its prefixes fall in. There is no queue in
//! front of the table and no thread that owns it; sessions that touch
//! different shards do not wait for each other, and one that has to wait
//! is slowed down, which is the back-pressure that is wanted.
//! - **Readers do not stop the table.** Reading takes a shard's lock for
//! reading, one shard at a time, and looks at the rows in place.
//! - **Followers go at their own pace.** What acts on the best routes reads
//! the changes from a [`journal`], and a slow reader holds nothing up.
pub mod attrs;
mod journal;
mod select;
#[cfg(test)]
mod tests;
use std::hash::BuildHasher;
use std::net::{IpAddr, Ipv4Addr};
use std::sync::atomic::{AtomicU64, AtomicUsize};
use std::sync::{Arc, Mutex, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard};
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
use foldhash::fast::RandomState;
use hashbrown::HashTable;
use hashbrown::hash_table::Entry;
use tokio::sync::Notify;
pub use attrs::{AttrSet, AttrStore, SelectionKey};
pub use journal::{Event, Progress, Reader, Routes};
/// What route selection needs to know about a peer.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PeerInfo {
pub asn: u32,
/// The peer's BGP Identifier.
pub router_id: Ipv4Addr,
pub address: IpAddr,
/// Whether the peer is in our own AS.
pub ibgp: bool,
}
/// A peer's number in a table, given out by [`RibTable::add_peer`]. Small,
/// because every route carries one.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PeerIndex(u16);
impl PeerIndex {
pub fn as_usize(self) -> usize {
self.0.into()
}
}
/// The LOCAL_PREF of a route that does not carry one: every route from an
/// external peer, until import policy says otherwise.
pub const DEFAULT_LOCAL_PREF: u32 = 100;
/// What a change to the table did.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Outcome {
/// Routes that were not in the table before.
pub added: usize,
/// Routes that took the place of one from the same peer.
pub replaced: usize,
/// Routes that were taken out.
pub removed: usize,
/// Prefixes whose best route is now another one, or none.
pub best_changed: usize,
}
impl std::ops::AddAssign for Outcome {
fn add_assign(&mut self, other: Self) {
self.added += other.added;
self.replaced += other.replaced;
self.removed += other.removed;
self.best_changed += other.best_changed;
}
}
/// One route of a prefix: sixteen bytes.
struct Path {
/// Two things in one word. The high 48 bits are the first steps of
/// route selection as one number, the larger the better; see
/// [`select::preference`]. The low 16 are the peer the route is from.
word: u64,
attrs: Arc<AttrSet>,
}
impl Path {
fn new(preference: u64, peer: PeerIndex, attrs: &Arc<AttrSet>) -> Self {
Self {
word: (preference << 16) | u64::from(peer.0),
attrs: Arc::clone(attrs),
}
}
fn preference(&self) -> u64 {
self.word >> 16
}
fn peer(&self) -> PeerIndex {
PeerIndex(self.word as u16)
}
}
/// Everything about one prefix.
struct Row {
prefix: IpPrefix,
/// Index of the best route in `paths`. A row always has at least one
/// route, and so always a best one; and the best one always has the
/// highest [`Path::preference`] in the row, which is what lets most
/// changes be settled without looking at the other routes
/// ([`select`]).
best: u32,
/// The sequence numbers of the latest change of this row's best route
/// and of any of its routes, by which the journals tell their entries
/// for it that still say something from those that do not.
seqs: journal::Seqs,
/// The routes, in one run of memory: finding a peer's route and
/// choosing the best are scans of it. One from each peer that has
/// announced the prefix, or several from a peer that sends more than one
/// (ADD-PATH, RFC 7911).
paths: Vec<Path>,
/// The path identifier of each route in `paths`, for telling apart the
/// routes of a peer that sends several. Empty while every route has
/// the identifier zero, which is all routes of peers that send one
/// each: they pay nothing for this.
path_ids: Vec<u32>,
}
impl Row {
fn new(prefix: IpPrefix, path: Path, path_id: u32, seqs: journal::Seqs) -> Self {
Self {
prefix,
best: 0,
seqs,
paths: vec![path],
path_ids: if path_id == 0 { vec![] } else { vec![path_id] },
}
}
fn path_id(&self, index: usize) -> u32 {
self.path_ids.get(index).copied().unwrap_or(0)
}
/// Where the route from `peer` with the identifier `path_id` is.
fn position(&self, peer: PeerIndex, path_id: u32) -> Option<usize> {
self.paths
.iter()
.enumerate()
.position(|(index, path)| path.peer() == peer && self.path_id(index) == path_id)
}
/// Where a route from `peer` is, whatever its identifier.
fn position_of_peer(&self, peer: PeerIndex) -> Option<usize> {
self.paths.iter().position(|path| path.peer() == peer)
}
fn push(&mut self, path: Path, path_id: u32) {
if path_id != 0 && self.path_ids.is_empty() {
self.path_ids = vec![0; self.paths.len()];
}
push_tight(&mut self.paths, path);
if !self.path_ids.is_empty() {
push_tight(&mut self.path_ids, path_id);
}
}
/// Takes out the route at `at`; the last route moves into its place.
fn swap_remove(&mut self, at: usize) -> Path {
if !self.path_ids.is_empty() {
self.path_ids.swap_remove(at);
}
self.paths.swap_remove(at)
}
fn best(&self) -> usize {
self.best as usize
}
fn set_best(&mut self, index: usize) {
// An index into `paths`, which cannot have that many: each is
// sixteen bytes.
self.best = index as u32;
}
fn route(&self, index: usize) -> RouteRef<'_> {
let path = &self.paths[index];
RouteRef {
prefix: self.prefix,
peer: path.peer(),
path_id: self.path_id(index),
attrs: &path.attrs,
best: index == self.best(),
}
}
fn routes(&self) -> impl Iterator<Item = RouteRef<'_>> {
(0..self.paths.len()).map(|index| self.route(index))
}
}
/// A route in the table, looked at in place.
#[derive(Debug, Clone, Copy)]
pub struct RouteRef<'a> {
pub prefix: IpPrefix,
/// The peer the route is from.
pub peer: PeerIndex,
/// The peer's identifier for the route: zero unless the peer sends
/// several routes for a prefix (ADD-PATH, RFC 7911).
pub path_id: u32,
pub attrs: &'a Arc<AttrSet>,
/// Whether it is the route chosen for its prefix.
pub best: bool,
}
impl RouteRef<'_> {
/// The route, to keep.
pub fn to_owned(&self) -> Route {
Route {
prefix: self.prefix,
peer: self.peer,
path_id: self.path_id,
attrs: Arc::clone(self.attrs),
best: self.best,
}
}
}
/// A route taken out of the table to keep; see [`RouteRef`].
#[derive(Debug, Clone)]
pub struct Route {
pub prefix: IpPrefix,
pub peer: PeerIndex,
pub path_id: u32,
pub attrs: Arc<AttrSet>,
pub best: bool,
}
struct Shard {
rows: HashTable<Row>,
routes: usize,
/// A copy of the peer list, so that route selection reads it under the
/// lock it already holds.
peers: Vec<Option<PeerInfo>>,
hasher: RandomState,
/// Reused between selections.
scratch: select::Scratch,
/// Which prefixes have changed, for those who follow.
journals: journal::Journals,
}
/// The routing table. Shared between threads by reference; every method
/// takes `&self`.
pub struct RibTable {
shards: Box<[RwLock<Shard>]>,
hasher: RandomState,
attrs: AttrStore,
/// Which peer numbers are in use. Changing the peer list also takes
/// every shard's lock in turn; this serialises that.
peers: Mutex<Vec<Option<PeerInfo>>>,
/// Which reader slots are in use; see [`journal`].
reader_slots: Mutex<Vec<bool>>,
/// How many readers there are, to skip waking them when there are none.
readers: AtomicUsize,
/// Goes up whenever something is written to a journal that has
/// readers.
version: AtomicU64,
wake: Notify,
}
/// No more peers can be added: every [`PeerIndex`] is in use.
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error("the table has no room for another peer")]
pub struct TooManyPeers;
impl RibTable {
/// A table in `shards` shards. More shards let more sessions write at
/// once; a few times the number of cores is plenty.
pub fn new(shards: usize) -> Self {
Self::with_journal_limits(shards, journal::Limits::default())
}
/// A table whose journals are tidied, and found too long, at the given
/// sizes. Tests use small ones to get there quickly.
fn with_journal_limits(shards: usize, limits: journal::Limits) -> Self {
let hasher = RandomState::default();
Self {
shards: (0..shards.max(1))
.map(|_| {
RwLock::new(Shard {
rows: HashTable::new(),
routes: 0,
peers: Vec::new(),
hasher: hasher.clone(),
scratch: select::Scratch::default(),
journals: journal::Journals::new(limits),
})
})
.collect(),
hasher,
attrs: AttrStore::new(),
peers: Mutex::default(),
reader_slots: Mutex::default(),
readers: AtomicUsize::new(0),
version: AtomicU64::new(0),
wake: Notify::new(),
}
}
/// Where attribute sets for this table's routes are made.
pub fn attrs(&self) -> &AttrStore {
&self.attrs
}
/// Makes room for a peer's routes and returns the number to announce
/// them under.
///
/// The number is the peer's until [`RibTable::remove_peer`] is called
/// with it, and may then be given to another. Whoever was given it must
/// not use it after that: the table cannot tell a late announcement
/// from one of the number's next owner.
pub fn add_peer(&self, info: PeerInfo) -> Result<PeerIndex, TooManyPeers> {
let mut peers = self.peers.lock().unwrap_or_else(PoisonError::into_inner);
let index = match peers.iter().position(Option::is_none) {
Some(free) => free,
None => {
// A peer number shares a word with other things; see `Path`.
if peers.len() > usize::from(u16::MAX) {
return Err(TooManyPeers);
}
peers.push(None);
peers.len() - 1
}
};
peers[index] = Some(info);
for shard in &self.shards {
let mut shard = write(shard);
if shard.peers.len() <= index {
shard.peers.resize(index + 1, None);
}
shard.peers[index] = Some(info);
}
Ok(PeerIndex(index as u16))
}
/// Takes out every route from `peer` and gives its number back for
/// reuse (RFC 4271 §8.2.2: the routes of a session that ended are
/// deleted).
///
/// This looks at every prefix in the table. The list of peers is not
/// held while it does, so other peers can come and go meanwhile.
pub fn remove_peer(&self, peer: PeerIndex) -> Outcome {
let mut outcome = Outcome::default();
let mut news = false;
for shard in &self.shards {
let mut shard = write(shard);
let before = shard.journals.mark();
outcome += shard.remove_peer(peer);
// From here on the shard takes no more routes from the peer.
if let Some(slot) = shard.peers.get_mut(peer.as_usize()) {
*slot = None;
}
shard.tidy_journals();
news |= shard.journals.has_news_since(before);
}
// Only now can the number be given out again: no shard knows it.
let mut peers = self.peers.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(slot) = peers.get_mut(peer.as_usize()) {
*slot = None;
}
drop(peers);
if news {
self.wake_readers();
}
outcome
}
/// Announces `routes` from `peer`, all with the attributes `attrs`:
/// what one UPDATE does. A route is known by its prefix and path
/// identifier together ([`Nlri`]; a plain [`IpPrefix`] is the route
/// with identifier zero, which is every route of a peer that sends one
/// for each prefix). Announcing a route the peer had announced before
/// replaces it.
pub fn announce<N>(&self, peer: PeerIndex, attrs: &Arc<AttrSet>, routes: &[N]) -> Outcome
where
N: Copy + Into<Nlri>,
{
let mut outcome = Outcome::default();
self.by_shard(routes, |shard, placed| {
let Some(info) = shard.peers.get(peer.as_usize()).copied().flatten() else {
// The peer was removed while this was on its way.
return;
};
let preference = select::preference(attrs.key(), &info);
for (hash, nlri) in placed {
outcome += shard.announce(*hash, *nlri, peer, preference, attrs);
}
});
outcome
}
/// Withdraws `routes` of `peer`. Withdrawing a route that is not there
/// does nothing.
pub fn withdraw<N>(&self, peer: PeerIndex, routes: &[N]) -> Outcome
where
N: Copy + Into<Nlri>,
{
let mut outcome = Outcome::default();
self.by_shard(routes, |shard, placed| {
for (hash, nlri) in placed {
outcome += shard.withdraw(*hash, nlri, peer);
}
});
outcome
}
fn shard_of(&self, hash: u64) -> usize {
spread(hash, self.shards.len())
}
/// Calls `apply` once for each shard that some of `routes` fall in,
/// with that shard locked and those routes. Wakes the readers if a
/// journal one of them follows was written to.
fn by_shard<N>(&self, routes: &[N], mut apply: impl FnMut(&mut Shard, &[(u64, Nlri)]))
where
N: Copy + Into<Nlri>,
{
let Some(first) = routes.first() else {
return;
};
let hashed = |route: &N| {
let nlri: Nlri = (*route).into();
(self.hasher.hash_one(nlri.prefix), nlri)
};
// An UPDATE has a handful of prefixes; they are sorted on the stack.
const INLINE: usize = 32;
let mut inline = [hashed(first); INLINE];
let mut spilled = Vec::new();
let placed: &mut [(u64, Nlri)] = if routes.len() <= INLINE {
for (slot, route) in inline.iter_mut().zip(routes) {
*slot = hashed(route);
}
&mut inline[..routes.len()]
} else {
spilled.extend(routes.iter().map(hashed));
&mut spilled
};
placed.sort_unstable_by_key(|(hash, _)| self.shard_of(*hash));
let mut news = false;
for run in placed.chunk_by(|a, b| self.shard_of(a.0) == self.shard_of(b.0)) {
let mut shard = write(&self.shards[self.shard_of(run[0].0)]);
let before = shard.journals.mark();
apply(&mut shard, run);
shard.tidy_journals();
news |= shard.journals.has_news_since(before);
}
if news {
self.wake_readers();
}
}
/// How many routes the table holds, from all peers.
pub fn len(&self) -> usize {
self.shards.iter().map(|shard| read(shard).routes).sum()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// How many prefixes have at least one route.
pub fn prefix_count(&self) -> usize {
self.shards.iter().map(|shard| read(shard).rows.len()).sum()
}
/// The best route for `prefix`: who it is from, and its attributes.
pub fn best(&self, prefix: &IpPrefix) -> Option<(PeerIndex, Arc<AttrSet>)> {
self.with_row(prefix, |row| {
let best = &row.paths[row.best()];
(best.peer(), Arc::clone(&best.attrs))
})
}
/// Every route for `prefix`.
pub fn routes(&self, prefix: &IpPrefix) -> Vec<Route> {
self.with_row(prefix, |row| row.routes().map(|r| r.to_owned()).collect())
.unwrap_or_default()
}
fn with_row<T>(&self, prefix: &IpPrefix, read_row: impl FnOnce(&Row) -> T) -> Option<T> {
let hash = self.hasher.hash_one(prefix);
let shard = read(&self.shards[self.shard_of(hash)]);
shard
.rows
.find(hash, |row| row.prefix == *prefix)
.map(read_row)
}
/// Calls `visit` with the best route of every prefix, in no particular
/// order. The table is not stopped: one shard at a time is held for
/// reading, so the routes seen are not all of one moment.
///
/// `visit` is called with a shard held, and must not call into the
/// table itself: taking the shard's lock again can wait for ever on a
/// writer that is waiting for this hold to end.
pub fn for_each_best(&self, mut visit: impl FnMut(&IpPrefix, PeerIndex, &Arc<AttrSet>)) {
for shard in &self.shards {
for row in &read(shard).rows {
let best = &row.paths[row.best()];
visit(&row.prefix, best.peer(), &best.attrs);
}
}
}
/// Up to `limit` routes: 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<Route> {
let mut routes = Vec::new();
for shard in &self.shards {
for row in &read(shard).rows {
for route in row.routes() {
if routes.len() == limit {
return routes;
}
routes.push(route.to_owned());
}
}
}
routes
}
/// Calls `visit` with every route in the table. As
/// [`RibTable::for_each_best`]: the table is not stopped, and `visit`
/// must not call into it.
pub fn for_each_route(&self, mut visit: impl FnMut(RouteRef<'_>)) {
for shard in &self.shards {
for row in &read(shard).rows {
row.routes().for_each(&mut visit);
}
}
}
}
fn read(shard: &RwLock<Shard>) -> RwLockReadGuard<'_, Shard> {
// A panic under the lock cannot leave a row half changed in a way that
// is unsafe to read; the poison is ignored, as for the server's tables.
shard.read().unwrap_or_else(PoisonError::into_inner)
}
fn write(shard: &RwLock<Shard>) -> RwLockWriteGuard<'_, Shard> {
shard.write().unwrap_or_else(PoisonError::into_inner)
}
/// Picks one of `count` buckets by a hash, for spreading keys over shards
/// or lock stripes that each hold a hash table keyed by the same hash.
///
/// That table takes a key's slot from the low bits of the hash and, from
/// the top seven, the tag it compares before it looks at the key itself
/// (hashbrown). The bucket is taken from bits in between: taken from
/// either end, the keys of one bucket would all share those bits, and the
/// table inside would lose them.
fn spread(hash: u64, count: usize) -> usize {
let middle = (hash << 7) >> 32;
((middle * count as u64) >> 32) as usize
}
/// Adds to a vector without the doubling that `push` does: a row of a few
/// hundred routes should not hold room for as many again. Grows by a
/// quarter, which keeps adding cheap on average.
fn push_tight<T>(vec: &mut Vec<T>, value: T) {
if vec.len() == vec.capacity() {
vec.reserve_exact((vec.len() / 4).max(1));
}
vec.push(value);
}
impl Shard {
fn tidy_journals(&mut self) {
self.journals.tidy(&self.rows, &self.hasher);
}
fn announce(
&mut self,
hash: u64,
nlri: Nlri,
peer: PeerIndex,
preference: u64,
attrs: &Arc<AttrSet>,
) -> Outcome {
let Nlri { prefix, path_id } = nlri;
let hasher = &self.hasher;
let entry = self.rows.entry(
hash,
|row| row.prefix == prefix,
|row| hasher.hash_one(row.prefix),
);
let path = Path::new(preference, peer, attrs);
let row = match entry {
Entry::Vacant(vacant) => {
let seqs = self.journals.appeared(prefix);
vacant.insert(Row::new(prefix, path, path_id, seqs));
self.routes += 1;
return Outcome {
added: 1,
best_changed: 1,
..Outcome::default()
};
}
Entry::Occupied(occupied) => occupied.into_mut(),
};
let (outcome, best_changed) = match row.position(peer, path_id) {
None => {
row.push(path, path_id);
self.routes += 1;
let outcome = Outcome {
added: 1,
..Outcome::default()
};
(
outcome,
select::after_add(row, &self.peers, &mut self.scratch),
)
}
Some(at) => {
let before = std::mem::replace(&mut row.paths[at], path);
let outcome = Outcome {
replaced: 1,
..Outcome::default()
};
if Arc::ptr_eq(&before.attrs, attrs) && before.preference() == preference {
// The same route again: nothing changed at all.
return outcome;
}
let best_changed = select::after_replace(
row,
at,
before.preference(),
&self.peers,
&mut self.scratch,
);
(outcome, best_changed)
}
};
self.journals.changed(row, best_changed);
Outcome {
best_changed: best_changed.into(),
..outcome
}
}
fn withdraw(&mut self, hash: u64, nlri: &Nlri, peer: PeerIndex) -> Outcome {
let prefix = nlri.prefix;
let Ok(mut entry) = self.rows.find_entry(hash, |row| row.prefix == prefix) else {
return Outcome::default();
};
let row = entry.get_mut();
let Some(at) = row.position(peer, nlri.path_id) else {
return Outcome::default();
};
self.routes -= 1;
if row.paths.len() == 1 {
entry.remove();
self.journals.disappeared(prefix);
return Outcome {
removed: 1,
best_changed: 1,
..Outcome::default()
};
}
let best_changed = select::after_remove(row, at, &self.peers, &mut self.scratch);
self.journals.changed(row, best_changed);
Outcome {
removed: 1,
best_changed: best_changed.into(),
..Outcome::default()
}
}
fn remove_peer(&mut self, peer: PeerIndex) -> Outcome {
let mut outcome = Outcome::default();
let peers = &self.peers;
let scratch = &mut self.scratch;
let journals = &mut self.journals;
self.rows.retain(|row| {
let mut removed = 0;
let mut best_changed = false;
// A peer that sends several routes for a prefix has several
// here.
while let Some(at) = row.position_of_peer(peer) {
removed += 1;
if row.paths.len() == 1 {
outcome.removed += removed;
outcome.best_changed += 1;
journals.disappeared(row.prefix);
return false;
}
best_changed |= select::after_remove(row, at, peers, scratch);
}
if removed > 0 {
outcome.removed += removed;
outcome.best_changed += usize::from(best_changed);
journals.changed(row, best_changed);
}
true
});
self.routes -= outcome.removed;
outcome
}
}
+249
View File
@@ -0,0 +1,249 @@
//! Choosing the best route for a prefix (RFC 4271 §9.1.2.2).
//!
//! The steps, in order. A route wins at the first step where it differs:
//!
//! 1. highest LOCAL_PREF;
//! 2. shortest AS path;
//! 3. lowest ORIGIN;
//! 4. lowest MULTI_EXIT_DISC, among routes from the same neighbouring AS;
//! 5. a route from an external peer over one from an internal peer;
//! 6. the peer with the lowest BGP Identifier;
//! 7. the peer with the lowest address.
//!
//! (The RFC's step between 5 and 6, the interior cost to the next hop, has
//! nothing to compare until there is an IGP to ask.)
//!
//! Steps 1 to 3 are put into one number per route when it is stored,
//! [`preference`]. Most of the time that number alone settles it: a new
//! route with a larger one than the best is the new best, with a smaller
//! one changes nothing, and neither needs a look at any other route. Only a
//! tie goes on to [`choose`], which looks at the whole row, and that begins
//! with a pass over the numbers to find the routes still in the running.
//!
//! [`after_add`], [`after_replace`] and [`after_remove`] are what the table
//! calls when a row has changed. Each says whether the best route is now
//! another, and takes the short way where there is one. They rely on, and
//! keep, one fact about a row: its best route has the highest preference
//! of all its routes.
use super::attrs::SelectionKey;
use super::{DEFAULT_LOCAL_PREF, PeerInfo, Row};
/// Steps 1 to 3 as one number of 48 bits: the larger, the better.
///
/// LOCAL_PREF fills the high 32 bits. Below it come the AS path length in
/// 12 bits and the ORIGIN in 2, each turned upside down so that shorter and
/// lower compare as larger. Twelve bits are enough for any path: a BGP
/// message of 4096 octets has room for about a thousand AS numbers.
pub(super) fn preference(key: &SelectionKey, peer: &PeerInfo) -> u64 {
// LOCAL_PREF is a matter between routers of one AS. A route from an
// external peer has the default until import policy gives it another.
let local_pref = if peer.ibgp {
key.local_pref.unwrap_or(DEFAULT_LOCAL_PREF)
} else {
DEFAULT_LOCAL_PREF
};
const LONGEST: u16 = (1 << 12) - 1;
(u64::from(local_pref) << 16)
| (u64::from(LONGEST - key.as_path_len.min(LONGEST)) << 4)
| (u64::from(3 - key.origin.min(3)) << 2)
}
/// Buffers that [`choose`] reuses.
#[derive(Default)]
pub(super) struct Scratch {
/// Routes still in the running: (neighbouring AS, MED, index in row).
candidates: Vec<(u32, u32, u32)>,
}
/// The best route changed after a route was added at the end of `row`?
pub(super) fn after_add(row: &mut Row, peers: &[Option<PeerInfo>], scratch: &mut Scratch) -> bool {
let added = row.paths.len() - 1;
let preference = row.paths[added].preference();
let best = row.paths[row.best()].preference();
// Better than the best in the first steps of selection is better than
// everything; worse than it changes nothing. A tie has to be settled by
// the later steps.
if preference > best {
row.set_best(added);
true
} else if preference < best {
false
} else if let Some(wins) = settles_tie(row, added, peers) {
if wins {
row.set_best(added);
}
wins
} else {
choose(row, peers, scratch)
}
}
/// The best route changed after the route at `at` was replaced by another
/// from the same peer? `before` is the preference of the route it replaced.
/// The two are known to differ.
pub(super) fn after_replace(
row: &mut Row,
at: usize,
before: u64,
peers: &[Option<PeerInfo>],
scratch: &mut Scratch,
) -> bool {
let preference = row.paths[at].preference();
if at == row.best() {
// The best route itself changed. If it got better in the first
// steps it is still the best, but it is a different route now.
if preference <= before {
choose(row, peers, scratch);
}
return true;
}
let best = row.paths[row.best()].preference();
if preference > best {
row.set_best(at);
true
} else if preference < best && before < best {
// It was not in the running and still is not.
false
} else {
choose(row, peers, scratch)
}
}
/// Takes the route at `at` out of `row`, which has others, and returns
/// whether the best route changed.
pub(super) fn after_remove(
row: &mut Row,
at: usize,
peers: &[Option<PeerInfo>],
scratch: &mut Scratch,
) -> bool {
let best = row.best();
let best_preference = row.paths[best].preference();
let last = row.paths.len() - 1;
let removed = row.swap_remove(at);
if at == best {
choose(row, peers, scratch);
return true;
}
// The last route moved into the gap.
if best == last {
row.set_best(at);
}
// A route that tied with the best in the first steps may have been
// what kept another from being best, by its MED.
removed.preference() == best_preference && choose(row, peers, scratch)
}
/// Chooses the best route of `row` from all its routes, records it in the
/// row, and returns whether it is another route than before.
///
/// The row must have at least one route.
fn choose(row: &mut Row, peers: &[Option<PeerInfo>], scratch: &mut Scratch) -> bool {
let before = row.best;
let top = row
.paths
.iter()
.map(|path| path.preference())
.max()
.unwrap_or(0);
let candidates = &mut scratch.candidates;
candidates.clear();
for (index, path) in row.paths.iter().enumerate() {
if path.preference() == top {
let key = path.attrs.key();
// A route without a MED is taken to have the lowest (RFC 4271
// §9.1.2.2 c).
candidates.push((key.neighbor_as, key.med.unwrap_or(0), index as u32));
}
}
// Step 4. MEDs are compared only between routes from the same
// neighbouring AS, so "lower MED" is not an order over all the routes,
// and comparing them pair by pair can go round in circles. Instead:
// within each neighbouring AS only the routes with its lowest MED stay
// in, and the steps after are an order over what is left.
candidates.sort_unstable();
let mut best: Option<(TieBreak, u32)> = None;
let mut group = None;
for &(neighbor_as, med, index) in candidates.iter() {
// Sorted by AS and then MED: the first of each AS has its lowest.
let lowest = match group {
Some((group_as, lowest)) if group_as == neighbor_as => lowest,
_ => {
group = Some((neighbor_as, med));
med
}
};
if med != lowest {
continue;
}
let tie_break = TieBreak::of(row, index as usize, peers);
if best.as_ref().is_none_or(|(least, _)| tie_break < *least) {
best = Some((tie_break, index));
}
}
row.best = best.map_or(0, |(_, index)| index);
row.best != before
}
/// For a route just added at `added` that ties with the best in steps 1 to
/// 3: whether it is the new best, if that can be said from the two routes
/// alone. `None` if the whole row has to be looked at.
///
/// It can be said when MED cannot knock either of them out. The new route
/// has the lowest MED there is, so nothing knocks it out. It knocks out
/// only routes of its own neighbouring AS with a higher MED, and if the
/// best is not one of those, the best is still the best of all the others.
/// So the better of the two is the best of all. This is the usual case:
/// most routes have no MED.
fn settles_tie(row: &Row, added: usize, peers: &[Option<PeerInfo>]) -> Option<bool> {
let new = row.paths[added].attrs.key();
let best = row.paths[row.best()].attrs.key();
if new.med.unwrap_or(0) != 0 {
return None;
}
if best.neighbor_as == new.neighbor_as && best.med.unwrap_or(0) != 0 {
return None;
}
Some(TieBreak::of(row, added, peers) < TieBreak::of(row, row.best(), peers))
}
/// Steps 5 to 7, then the peer's number, and then the path identifier for
/// a peer that sent several routes: so that the choice never depends on the
/// order routes arrived in. The least wins.
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct TieBreak {
internal: bool,
router_id: u32,
address: std::net::IpAddr,
peer: u16,
path_id: u32,
}
impl TieBreak {
fn of(row: &Row, index: usize, peers: &[Option<PeerInfo>]) -> Self {
let peer = row.paths[index].peer();
let path_id = row.path_id(index);
match peers.get(peer.as_usize()).copied().flatten() {
Some(info) => Self {
internal: info.ibgp,
router_id: info.router_id.into(),
address: info.address,
peer: peer.0,
path_id,
},
// Not expected: a peer is forgotten only once its routes are
// gone. If it happened, anything known would be preferred.
None => Self {
internal: true,
router_id: u32::MAX,
address: std::net::Ipv6Addr::from(u128::MAX).into(),
peer: peer.0,
path_id,
},
}
}
}
+759
View File
@@ -0,0 +1,759 @@
use std::collections::{BTreeMap, HashMap};
use std::net::{IpAddr, Ipv4Addr};
use std::sync::Arc;
use bgp_packet::message::{
AsPathAttribute, LocalPrefPathAttribute, MultiExitDiscPathAttribute, NextHopPathAttribute,
OriginPathAttribute, PathAttribute,
};
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
use proptest::prelude::*;
use super::*;
pub(super) fn prefix(s: &str) -> IpPrefix {
IpPrefix::try_from(s).unwrap()
}
/// What a test says about a route; everything selection looks at.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct Route {
pub(super) local_pref: Option<u32>,
pub(super) path: Vec<u32>,
pub(super) origin: u8,
pub(super) med: Option<u32>,
}
impl Route {
pub(super) fn via(path: &[u32]) -> Self {
Self {
local_pref: None,
path: path.to_vec(),
origin: 0,
med: None,
}
}
pub(super) fn attributes(&self) -> Vec<PathAttribute> {
let mut attributes = vec![
PathAttribute::Origin(match self.origin {
0 => OriginPathAttribute::IGP,
1 => OriginPathAttribute::EGP,
_ => OriginPathAttribute::INCOMPLETE,
}),
AsPathAttribute::from_asns(self.path.clone()),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
];
attributes.extend(
self.med
.map(|med| PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(med))),
);
attributes.extend(
self.local_pref
.map(|lp| PathAttribute::LocalPref(LocalPrefPathAttribute(lp))),
);
attributes
}
}
pub(super) fn external(number: u8) -> PeerInfo {
PeerInfo {
asn: 65000 + u32::from(number),
router_id: Ipv4Addr::new(10, 0, 0, number),
address: IpAddr::from(Ipv4Addr::new(192, 0, 2, number)),
ibgp: false,
}
}
pub(super) fn internal(number: u8) -> PeerInfo {
PeerInfo {
asn: 64999,
ibgp: true,
..external(number)
}
}
/// A table and the peers added to it, for tests that speak of peers by the
/// order they were added in.
pub(super) struct Fixture {
pub(super) table: Arc<RibTable>,
pub(super) peers: Vec<PeerIndex>,
}
impl Fixture {
pub(super) fn new(peers: &[PeerInfo]) -> Self {
// More than one shard, so that a test's prefixes are spread.
Self::on(RibTable::new(4), peers)
}
pub(super) fn on(table: RibTable, peers: &[PeerInfo]) -> Self {
let table = Arc::new(table);
let peers = peers.iter().map(|p| table.add_peer(*p).unwrap()).collect();
Self { table, peers }
}
pub(super) fn announce(&self, peer: usize, prefix: &IpPrefix, route: &Route) -> Outcome {
let attrs = self
.table
.attrs()
.intern(&route.attributes(), None)
.unwrap();
self.table.announce(self.peers[peer], &attrs, &[*prefix])
}
pub(super) fn withdraw(&self, peer: usize, prefix: &IpPrefix) -> Outcome {
self.table.withdraw(self.peers[peer], &[*prefix])
}
/// As `announce`, from a peer that sends several routes for a prefix
/// and calls this one `path_id`.
pub(super) fn announce_path(
&self,
peer: usize,
prefix: &IpPrefix,
path_id: u32,
route: &Route,
) -> Outcome {
let attrs = self
.table
.attrs()
.intern(&route.attributes(), None)
.unwrap();
let nlri = Nlri {
prefix: *prefix,
path_id,
};
self.table.announce(self.peers[peer], &attrs, &[nlri])
}
pub(super) fn withdraw_path(&self, peer: usize, prefix: &IpPrefix, path_id: u32) -> Outcome {
let nlri = Nlri {
prefix: *prefix,
path_id,
};
self.table.withdraw(self.peers[peer], &[nlri])
}
/// The best route for `prefix`: which peer (by order added) it is from
/// and that peer's identifier for it.
pub(super) fn best_path(&self, prefix: &IpPrefix) -> Option<(usize, u32)> {
let best = self.table.routes(prefix).into_iter().find(|r| r.best)?;
let peer = self.peers.iter().position(|p| *p == best.peer)?;
Some((peer, best.path_id))
}
/// The routes held for `prefix`, as (peer, path identifier), in order.
pub(super) fn held(&self, prefix: &IpPrefix) -> Vec<(usize, u32)> {
let mut held: Vec<(usize, u32)> = self
.table
.routes(prefix)
.iter()
.map(|r| {
let peer = self.peers.iter().position(|p| *p == r.peer).unwrap();
(peer, r.path_id)
})
.collect();
held.sort_unstable();
held
}
/// Which peer (by order added) the best route for `prefix` is from.
fn best(&self, prefix: &IpPrefix) -> Option<usize> {
let (peer, _) = self.table.best(prefix)?;
self.peers.iter().position(|p| *p == peer)
}
}
#[test]
fn each_step_of_selection_decides_when_the_ones_before_tie() {
let p = prefix("10.0.0.0/8");
// Peer 0 has the lower identifier, so it wins whenever nothing else
// decides; each case makes peer 1's route better in one step.
let cases: [(&str, [PeerInfo; 2], Route, Route); 6] = [
(
"higher LOCAL_PREF, despite a longer path",
[internal(1), internal(2)],
Route {
local_pref: Some(100),
..Route::via(&[1])
},
Route {
local_pref: Some(200),
..Route::via(&[1, 2, 3])
},
),
(
"shorter AS path",
[external(1), external(2)],
Route::via(&[1, 2, 3]),
Route::via(&[4, 5]),
),
(
"lower ORIGIN",
[external(1), external(2)],
Route {
origin: 2,
..Route::via(&[1, 2])
},
Route {
origin: 0,
..Route::via(&[3, 4])
},
),
(
"lower MED from the same neighbouring AS",
[external(1), external(2)],
Route {
med: Some(20),
..Route::via(&[7, 2])
},
Route {
med: Some(10),
..Route::via(&[7, 3])
},
),
(
"no MED counts as the lowest",
[external(1), external(2)],
Route {
med: Some(1),
..Route::via(&[7, 2])
},
Route::via(&[7, 3]),
),
(
"external over internal",
[internal(1), external(2)],
Route::via(&[7, 2]),
Route::via(&[8, 3]),
),
];
for (case, peers, first, second) in cases {
// In both orders of arrival: the choice must not depend on it.
for order in [[0, 1], [1, 0]] {
let fixture = Fixture::new(&peers);
for peer in order {
fixture.announce(peer, &p, [&first, &second][peer]);
}
assert_eq!(fixture.best(&p), Some(1), "{case}, arriving {order:?}");
}
}
// And when nothing decides: the lowest identifier.
let fixture = Fixture::new(&[external(2), external(1)]);
fixture.announce(0, &p, &Route::via(&[1, 2]));
fixture.announce(1, &p, &Route::via(&[3, 4]));
assert_eq!(fixture.best(&p), Some(1));
}
/// MEDs are compared only within a neighbouring AS. Taken pair by pair
/// that is not an order: A beats B, B beats C, C beats A. Whatever the
/// order of arrival, the answer must be the same.
#[test]
fn med_is_compared_only_within_a_neighbouring_as_and_never_goes_in_circles() {
let p = prefix("10.0.0.0/8");
let routes = [
// From AS 1, MED 10, and the highest identifier.
Route {
med: Some(10),
..Route::via(&[1, 9])
},
// From AS 2: its MED is not comparable with the others'.
Route {
med: Some(50),
..Route::via(&[2, 9])
},
// From AS 1, MED 20: loses to the first on MED.
Route {
med: Some(20),
..Route::via(&[1, 8])
},
];
// Identifiers: the third route's peer lowest, then the second's, then
// the first's. Without MED the third would win; it is knocked out by
// the first, and of the two left the second has the lower identifier.
let peers = [external(3), external(2), external(1)];
for order in [
[0, 1, 2],
[0, 2, 1],
[1, 0, 2],
[1, 2, 0],
[2, 0, 1],
[2, 1, 0],
] {
let fixture = Fixture::new(&peers);
for peer in order {
fixture.announce(peer, &p, &routes[peer]);
}
assert_eq!(fixture.best(&p), Some(1), "arriving {order:?}");
}
// Take away the route that knocked the third out, and the third wins.
let fixture = Fixture::new(&peers);
for (peer, route) in routes.iter().enumerate() {
fixture.announce(peer, &p, route);
}
let outcome = fixture.withdraw(0, &p);
assert_eq!(outcome.best_changed, 1);
assert_eq!(fixture.best(&p), Some(2));
}
/// A route that is not the best can still decide which is: by its MED it
/// keeps another route of its neighbouring AS out. When it gets worse or
/// goes, that other route is back in the running. The shortcuts must not
/// skip this.
#[test]
fn a_route_that_was_keeping_another_out_matters_when_it_changes() {
let p = prefix("10.0.0.0/8");
// Peer 0: from AS 1 with the lowest identifier, but MED 1.
// Peer 1: from AS 2. Best while peer 0's route is kept out.
// Peer 2: from AS 1 with MED 0, which keeps peer 0's route out.
let peers = [external(1), external(2), external(3)];
let held_out = Route {
med: Some(1),
..Route::via(&[1, 9])
};
let best = Route::via(&[2, 9]);
let holding_out = Route {
med: Some(0),
..Route::via(&[1, 8])
};
let set_up = || {
let fixture = Fixture::new(&peers);
fixture.announce(0, &p, &held_out);
fixture.announce(1, &p, &best);
fixture.announce(2, &p, &holding_out);
assert_eq!(fixture.best(&p), Some(1));
fixture
};
// It is replaced by a route with a longer path.
let fixture = set_up();
let outcome = fixture.announce(2, &p, &Route::via(&[1, 8, 7]));
assert_eq!(fixture.best(&p), Some(0));
assert_eq!(outcome.best_changed, 1);
// It is withdrawn.
let fixture = set_up();
let outcome = fixture.withdraw(2, &p);
assert_eq!(fixture.best(&p), Some(0));
assert_eq!(outcome.best_changed, 1);
// Its peer goes away.
let fixture = set_up();
let outcome = fixture.table.remove_peer(fixture.peers[2]);
assert_eq!(fixture.best(&p), Some(0));
assert_eq!(outcome.best_changed, 1);
// Its MED gets worse than the one it was keeping out.
let fixture = set_up();
fixture.announce(
2,
&p,
&Route {
med: Some(5),
..Route::via(&[1, 8])
},
);
assert_eq!(fixture.best(&p), Some(0));
}
#[test]
fn what_a_change_did_is_reported() {
let fixture = Fixture::new(&[external(1), external(2)]);
let p = prefix("10.0.0.0/8");
let long = Route::via(&[1, 2, 3]);
let short = Route::via(&[4]);
let first = fixture.announce(0, &p, &long);
assert_eq!((first.added, first.best_changed), (1, 1));
let worse = fixture.announce(1, &p, &Route::via(&[1, 2, 3, 4]));
assert_eq!((worse.added, worse.best_changed), (1, 0));
// The same route again changes nothing.
let again = fixture.announce(0, &p, &long);
assert_eq!((again.replaced, again.best_changed), (1, 0));
// The best route replaced by a better one is still a change of best.
let better = fixture.announce(0, &p, &short);
assert_eq!((better.replaced, better.best_changed), (1, 1));
assert_eq!((fixture.table.len(), fixture.table.prefix_count()), (2, 1));
assert_eq!(fixture.withdraw(1, &p).best_changed, 0);
let last = fixture.withdraw(0, &p);
assert_eq!((last.removed, last.best_changed), (1, 1));
assert_eq!(fixture.withdraw(0, &p), Outcome::default(), "nothing there");
assert!(fixture.table.is_empty());
assert_eq!(fixture.best(&p), None);
}
#[test]
fn removing_a_peer_takes_its_routes_and_frees_its_number() {
let fixture = Fixture::new(&[external(1), external(2)]);
let prefixes: Vec<IpPrefix> = (0..200u32)
.map(|i| {
IpPrefix::new(
bgp_primitives::constants::IpVersion::Ipv4,
(i << 8).to_be_bytes(),
24,
)
.unwrap()
})
.collect();
let attrs = |route: &Route| {
fixture
.table
.attrs()
.intern(&route.attributes(), None)
.unwrap()
};
// Peer 0 is best everywhere; peer 1 has only the first hundred.
fixture
.table
.announce(fixture.peers[0], &attrs(&Route::via(&[1])), &prefixes);
fixture.table.announce(
fixture.peers[1],
&attrs(&Route::via(&[2, 3])),
&prefixes[..100],
);
assert_eq!(fixture.table.len(), 300);
let outcome = fixture.table.remove_peer(fixture.peers[0]);
assert_eq!(outcome.removed, 200);
assert_eq!(outcome.best_changed, 200);
assert_eq!(
(fixture.table.len(), fixture.table.prefix_count()),
(100, 100)
);
assert_eq!(fixture.best(&prefixes[0]), Some(1));
assert_eq!(fixture.best(&prefixes[150]), None);
// Its routes are gone, so its attribute set can be dropped.
assert_eq!(fixture.table.attrs().sweep(), 1);
// And its number is given to the next peer.
assert_eq!(fixture.table.add_peer(external(3)), Ok(fixture.peers[0]));
}
#[test]
fn readers_see_every_route_and_which_is_best() {
let fixture = Fixture::new(&[external(1), external(2)]);
let (a, b) = (prefix("10.0.0.0/8"), prefix("2001:db8::/32"));
fixture.announce(0, &a, &Route::via(&[1, 2]));
fixture.announce(1, &a, &Route::via(&[3]));
fixture.announce(0, &b, &Route::via(&[1, 2]));
let mut best = Vec::new();
fixture
.table
.for_each_best(|prefix, peer, _| best.push((*prefix, peer)));
best.sort_by_key(|(prefix, _)| prefix.to_string());
assert_eq!(best, [(a, fixture.peers[1]), (b, fixture.peers[0])]);
let mut all = 0;
let mut marked_best = 0;
fixture.table.for_each_route(|route| {
all += 1;
marked_best += usize::from(route.best);
});
assert_eq!((all, marked_best), (3, 2));
let routes = fixture.table.routes(&a);
assert_eq!(routes.len(), 2);
assert!(
routes
.iter()
.any(|route| route.peer == fixture.peers[1] && route.best)
);
// Routes with the same attributes share one set.
let shared = &fixture.table.routes(&b)[0].attrs;
assert!(routes.iter().any(|route| Arc::ptr_eq(&route.attrs, shared)));
}
/// With ADD-PATH a peer sends several routes for one prefix and tells them
/// apart by a path identifier. Each is a route of its own: kept, replaced
/// and withdrawn by prefix and identifier together.
#[test]
fn a_peer_can_have_several_routes_for_a_prefix() {
let fixture = Fixture::new(&[external(1), external(2)]);
let p = prefix("10.0.0.0/8");
fixture.announce_path(0, &p, 1, &Route::via(&[1, 2, 3]));
let second = fixture.announce_path(0, &p, 2, &Route::via(&[1, 4]));
assert_eq!(
(second.added, second.replaced, second.best_changed),
(1, 0, 1)
);
fixture.announce(1, &p, &Route::via(&[2, 5, 6, 7]));
assert_eq!(fixture.held(&p), [(0, 1), (0, 2), (1, 0)]);
assert_eq!(fixture.best_path(&p), Some((0, 2)), "the shorter path");
assert_eq!((fixture.table.len(), fixture.table.prefix_count()), (3, 1));
// Announcing a path again replaces that path and no other.
let replaced = fixture.announce_path(0, &p, 1, &Route::via(&[1]));
assert_eq!((replaced.added, replaced.replaced), (0, 1));
assert_eq!(fixture.held(&p), [(0, 1), (0, 2), (1, 0)]);
assert_eq!(fixture.best_path(&p), Some((0, 1)));
// Withdrawing one leaves the peer's other.
assert_eq!(fixture.withdraw_path(0, &p, 1).removed, 1);
assert_eq!(fixture.held(&p), [(0, 2), (1, 0)]);
assert_eq!(fixture.best_path(&p), Some((0, 2)));
// A path that is not there is not withdrawn: not the same prefix
// under another identifier, and not the peer's plain route.
assert_eq!(fixture.withdraw_path(0, &p, 9), Outcome::default());
assert_eq!(fixture.withdraw(0, &p), Outcome::default());
// When the session ends, all of the peer's routes go.
fixture.announce_path(0, &p, 3, &Route::via(&[1, 8, 9]));
let outcome = fixture.table.remove_peer(fixture.peers[0]);
assert_eq!((outcome.removed, outcome.best_changed), (2, 1));
assert_eq!(fixture.held(&p), [(1, 0)]);
}
/// Two routes from one peer that nothing else tells apart: the lower path
/// identifier is chosen, whichever arrived first.
#[test]
fn equal_routes_from_one_peer_are_chosen_by_path_identifier() {
let p = prefix("10.0.0.0/8");
for order in [[5, 3], [3, 5]] {
let fixture = Fixture::new(&[external(1)]);
for path_id in order {
fixture.announce_path(0, &p, path_id, &Route::via(&[1, path_id]));
}
assert_eq!(fixture.best_path(&p), Some((0, 3)), "arriving {order:?}");
}
}
// ---------------------------------------------------------------------------
// The table against a model
// ---------------------------------------------------------------------------
/// Route selection written the plain way: all routes of a prefix, step by
/// step, with no shortcuts. The table takes shortcuts and must agree.
/// Routes are known by peer and path identifier.
fn chosen_plainly(
routes: &BTreeMap<(usize, u32), Route>,
peers: &[PeerInfo],
) -> Option<(usize, u32)> {
let local_pref = |peer: usize, route: &Route| {
if peers[peer].ibgp {
route.local_pref.unwrap_or(DEFAULT_LOCAL_PREF)
} else {
DEFAULT_LOCAL_PREF
}
};
let mut left: Vec<((usize, u32), &Route)> =
routes.iter().map(|(k, route)| (*k, route)).collect();
let most = left.iter().map(|(k, r)| local_pref(k.0, r)).max()?;
left.retain(|(k, r)| local_pref(k.0, r) == most);
let shortest = left.iter().map(|(_, r)| r.path.len()).min()?;
left.retain(|(_, r)| r.path.len() == shortest);
let lowest = left.iter().map(|(_, r)| r.origin).min()?;
left.retain(|(_, r)| r.origin == lowest);
let neighbor = |route: &Route| route.path.first().copied().unwrap_or(0);
let med = |route: &Route| route.med.unwrap_or(0);
let mut lowest_med: HashMap<u32, u32> = HashMap::new();
for (_, route) in &left {
let entry = lowest_med.entry(neighbor(route)).or_insert(u32::MAX);
*entry = (*entry).min(med(route));
}
left.retain(|(_, r)| med(r) == lowest_med[&neighbor(r)]);
left.into_iter()
.map(|(key, _)| key)
.min_by_key(|&(peer, path_id)| {
(
peers[peer].ibgp,
peers[peer].router_id,
peers[peer].address,
peer,
path_id,
)
})
}
#[derive(Debug, Clone)]
enum Op {
Announce {
peer: usize,
prefix: usize,
path_id: u32,
route: Route,
},
Withdraw {
peer: usize,
prefix: usize,
path_id: u32,
},
/// The session goes down and comes straight back.
Flap { peer: usize },
}
const PEERS: usize = 6;
// Few prefixes, so that each gathers routes from most peers.
const PREFIXES: usize = 2;
// Most routes have the identifier zero, as without ADD-PATH; some peers
// send a second and third route.
const PATH_IDS: u32 = 3;
pub(super) fn route() -> impl Strategy<Value = Route> {
// Few values of everything, so that ties at every step are common:
// ties are where the shortcuts could be wrong.
(
proptest::option::of(100u32..102),
proptest::collection::vec(1u32..3, 1..3),
0u8..2,
proptest::option::of(0u32..3),
)
.prop_map(|(local_pref, path, origin, med)| Route {
local_pref,
path,
origin,
med,
})
}
fn path_id() -> impl Strategy<Value = u32> {
prop_oneof![3 => Just(0), 2 => 1..PATH_IDS]
}
fn op() -> impl Strategy<Value = Op> {
prop_oneof![
6 => (0..PEERS, 0..PREFIXES, path_id(), route()).prop_map(
|(peer, prefix, path_id, route)| Op::Announce { peer, prefix, path_id, route }
),
3 => (0..PEERS, 0..PREFIXES, path_id())
.prop_map(|(peer, prefix, path_id)| Op::Withdraw { peer, prefix, path_id }),
1 => (0..PEERS).prop_map(|peer| Op::Flap { peer }),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(2000))]
/// After any sequence of changes the table holds exactly the routes it
/// should, has chosen for every prefix what plain selection chooses,
/// and has reported a change of best route exactly when there was one.
#[test]
fn the_table_agrees_with_plain_selection(ops in proptest::collection::vec(op(), 1..80)) {
// Internal and external peers, and identifiers that are not in the
// order the peers are added in.
let infos: Vec<PeerInfo> = (0..PEERS)
.map(|i| {
let number = (PEERS - i) as u8;
if i % 3 == 0 { internal(number) } else { external(number) }
})
.collect();
let mut fixture = Fixture::new(&infos);
let prefixes: Vec<IpPrefix> = (0..PREFIXES)
.map(|i| prefix(&format!("10.{i}.0.0/16")))
.collect();
let mut model: Vec<BTreeMap<(usize, u32), Route>> = vec![BTreeMap::new(); PREFIXES];
for op in ops {
let before: Vec<Option<((usize, u32), Route)>> = model
.iter()
.map(|routes| chosen_plainly(routes, &infos).map(|key| (key, routes[&key].clone())))
.collect();
let outcome = match &op {
Op::Announce { peer, prefix, path_id, route } => {
model[*prefix].insert((*peer, *path_id), route.clone());
fixture.announce_path(*peer, &prefixes[*prefix], *path_id, route)
}
Op::Withdraw { peer, prefix, path_id } => {
model[*prefix].remove(&(*peer, *path_id));
fixture.withdraw_path(*peer, &prefixes[*prefix], *path_id)
}
Op::Flap { peer } => {
for routes in &mut model {
routes.retain(|(from, _), _| from != peer);
}
let outcome = fixture.table.remove_peer(fixture.peers[*peer]);
fixture.peers[*peer] = fixture.table.add_peer(infos[*peer]).unwrap();
outcome
}
};
let mut best_changes = 0;
for (i, routes) in model.iter().enumerate() {
let expected = chosen_plainly(routes, &infos);
prop_assert_eq!(fixture.best_path(&prefixes[i]), expected, "after {:?}", &op);
let now = expected.map(|key| (key, routes[&key].clone()));
best_changes += usize::from(now != before[i]);
prop_assert_eq!(
fixture.held(&prefixes[i]),
routes.keys().copied().collect::<Vec<_>>()
);
}
prop_assert_eq!(outcome.best_changed, best_changes, "after {:?}", &op);
prop_assert_eq!(
fixture.table.len(),
model.iter().map(BTreeMap::len).sum::<usize>()
);
}
}
}
/// Shards and lock stripes are picked by bits of the hash that the hash
/// table inside does not use, so that keys which share a shard still
/// differ where that table looks.
#[test]
fn keys_of_one_shard_differ_in_the_bits_their_table_uses() {
let hasher = RandomState::default();
let shards = 64;
let mut counts = vec![0usize; shards];
let mut tags = vec![std::collections::HashSet::new(); shards];
for n in 0..64_000u32 {
let hash = hasher.hash_one(n);
let shard = spread(hash, shards);
counts[shard] += 1;
// The seven bits hashbrown keeps as a tag.
tags[shard].insert(hash >> 57);
}
for shard in 0..shards {
assert!(
(700..1300).contains(&counts[shard]),
"shard {shard} has {} of 64000 keys",
counts[shard]
);
assert_eq!(tags[shard].len(), 128, "shard {shard}");
}
assert_eq!(spread(u64::MAX, 3), 2);
assert_eq!(spread(0, 3), 0);
}
/// A peer that sends several routes for a prefix can send more than fit a
/// sixteen-bit index. The best route was kept as one, and wrapped.
#[test]
fn a_row_can_hold_more_routes_than_a_sixteen_bit_index_counts() {
let store = AttrStore::new();
let worse = store
.intern(&Route::via(&[1, 2]).attributes(), None)
.unwrap();
let better = store.intern(&Route::via(&[1]).attributes(), None).unwrap();
let info = external(1);
let peers = [Some(info)];
let peer = PeerIndex(0);
let path =
|attrs: &Arc<AttrSet>| Path::new(select::preference(attrs.key(), &info), peer, attrs);
let seqs = journal::Journals::new(journal::Limits::default()).appeared(prefix("10.0.0.0/8"));
let mut row = Row::new(prefix("10.0.0.0/8"), path(&worse), 1, seqs);
let mut scratch = select::Scratch::default();
let count = usize::from(u16::MAX) + 10;
for path_id in 2..=count as u32 {
row.push(path(&worse), path_id);
}
// Equal routes from one peer: the lowest identifier is the best.
assert!(!select::after_add(&mut row, &peers, &mut scratch));
assert_eq!(row.best(), 0);
row.push(path(&better), count as u32 + 1);
assert!(select::after_add(&mut row, &peers, &mut scratch));
assert_eq!(row.best(), count);
assert_eq!(row.route(row.best()).path_id, count as u32 + 1);
// With the better one gone, the choice among all the rest is the
// lowest identifier again.
assert!(select::after_remove(&mut row, count, &peers, &mut scratch));
assert_eq!(row.route(row.best()).path_id, 1);
}
+13 -1
View File
@@ -1,6 +1,11 @@
[package]
name = "bgp-server"
authors.workspace = true
edition.workspace = true
homepage.workspace = true
license.workspace = true
repository.workspace = true
version.workspace = true
[dependencies]
bgp-packet.workspace = true
@@ -9,12 +14,19 @@ bgp-rib.workspace = true
bytes.workspace = true
clap.workspace = true
futures.workspace = true
eyre.workspace = true
prometheus.workspace = true
socket2.workspace = true
tokio.workspace = true
tracing-subscriber.workspace = true
tracing.workspace = true
protobuf.workspace = true
thiserror.workspace = true
[build-dependencies]
protobuf-codegen.workspace = true
[dev-dependencies]
proptest.workspace = true
[lints]
workspace = true
+4 -1
View File
@@ -2,8 +2,11 @@ use protobuf_codegen::Codegen;
fn main() {
Codegen::new()
// Parse the .proto in pure Rust so building needs no `protoc` binary
// on the machine (routers and minimal containers rarely have one).
.pure()
.input("proto/config.proto")
.includes(&["proto"])
.includes(["proto"])
.cargo_out_dir("protos")
.run_from_script();
}
+86
View File
@@ -17,6 +17,56 @@ message BgpConfig {
// Peers configured on this server.
repeated Peer peers = 4;
// Routes this router originates and announces to every peer.
repeated OriginatedRoute originate = 5;
// Where to serve the HTTP API, status page and metrics. The port is
// required. Unset disables the HTTP server.
TcpEndpoint http_endpoint = 6;
// Initial delay between attempts to connect to a configured peer, in
// seconds. Doubles after each failure up to two minutes. Defaults to 5.
uint32 connect_retry_seconds = 7;
// Connection collisions (RFC 4271 §6.8) against a session that is already
// Established are, by default, resolved by closing the new connection.
// When true they are resolved like any other collision: the connection
// initiated by the higher BGP Identifier survives, and a second connection
// opened by the same side replaces the first. This lets a peer that
// restarted take over from a session we have not yet noticed is dead,
// instead of being refused until the hold timer expires.
bool collision_detect_established_state = 8;
// Connections are accepted only from the addresses of configured peers;
// from anywhere else they are closed before a byte is read. When true,
// any host that can reach a listening endpoint may open a session, as
// any AS it cares to name, and have its routes taken. That is for
// testing on a closed network and nothing else.
bool accept_unconfigured_peers = 9;
}
// A route originated by this router. It is announced with an AS_PATH of the
// local AS and, on each session, that session's local address as next hop.
message OriginatedRoute {
// The prefix in CIDR notation, e.g. "192.0.2.0/24" or "2001:db8::/32".
string prefix = 1;
// ORIGIN attribute: "IGP" (the default), "EGP" or "INCOMPLETE".
string origin = 2;
// Extra copies of the local AS to prepend to the AS_PATH. At most 16.
uint32 as_path_prepend = 3;
// MULTI_EXIT_DISC. Left out of the announcement when unset.
optional uint32 med = 4;
// Standard communities (RFC 1997): "ASN:VALUE", or a well-known name such
// as "NO_EXPORT".
repeated string communities = 5;
// Large communities (RFC 8092): "GLOBAL:LOCAL1:LOCAL2".
repeated string large_communities = 6;
}
// An IP address and port pair.
@@ -41,4 +91,40 @@ message Peer {
// Hold time to advertise in the OPEN message, in seconds (RFC 4271 §4.2).
// Must be 0 or >= 3. Defaults to 90 when unset.
uint32 hold_time_seconds = 4;
// Source IP address for connections we open to this peer. It is also the
// next hop of the routes we send it. Required when the peer identifies us
// by address and the host has several, e.g. when both speakers run on the
// same machine. Must be the same address family as the endpoint. Unset
// lets the operating system choose.
string local_address = 5;
// The address families to exchange with this peer. Each is offered to the
// peer in our OPEN, and is in use on the session only if the peer offers
// it too (RFC 4760). Defaults to all of them when empty. A session with
// no family in common is refused.
repeated AddressFamily address_families = 6;
// ADD-PATH (RFC 7911): whether to take several routes for one prefix
// from this peer, to send it several, or both. It is in use, for each
// direction and address family, only if the peer agrees. Defaults to
// off: one route for each prefix, as in plain BGP.
AddPath add_path = 7;
}
// The directions ADD-PATH can be offered for.
enum AddPath {
ADD_PATH_OFF = 0;
// Accept several routes for a prefix from the peer.
ADD_PATH_RECEIVE = 1;
// Send the peer several routes for a prefix.
ADD_PATH_SEND = 2;
ADD_PATH_BOTH = 3;
}
// An address family routes can be exchanged for.
enum AddressFamily {
ADDRESS_FAMILY_UNSPECIFIED = 0;
IPV4_UNICAST = 1;
IPV6_UNICAST = 2;
}
+814
View File
@@ -0,0 +1,814 @@
//! Announces the routes this router originates to every established
//! session.
//!
//! Each session has an exporter of its own: a task that sends the session
//! the originated routes and then every change to them. An exporter waits
//! when its session's queue is full, which is what a peer that reads slowly
//! or not at all comes to, and it is the only thing that waits: the other
//! sessions have their own exporters and are not held up.
//!
//! An exporter remembers what it has announced. When it has waited so long
//! that it missed changes, it compares that with the routes as they now
//! are, announces those, and withdraws what is no longer among them. A peer
//! that was slow ends up with what every other peer has.
//!
//! The advertiser itself only starts an exporter when a session comes up
//! and stops it when the session goes.
use std::collections::{HashMap, HashSet};
use std::net::IpAddr;
use std::sync::Arc;
use bgp_packet::message::{
AsPathAttribute, AsPathSegment, CommunitiesPathAttribute, LargeCommunitiesPathAttribute,
LocalPrefPathAttribute, Message, MpReachNlriPathAttribute, MpUnreachNlriPathAttribute,
MultiExitDiscPathAttribute, NextHopPathAttribute, NlriNextHop, PathAttribute, UpdateMessage,
};
use bgp_primitives::constants::{AfiSafi, IpVersion};
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
use bgp_rib::table::DEFAULT_LOCAL_PREF;
use bgp_rib::{LocalRib, LocalRibEvent, LocalRoute};
use tokio::sync::Notify;
use tokio::sync::broadcast::Receiver;
use tokio::sync::broadcast::error::RecvError;
use tokio::task::JoinHandle;
use tracing::{debug, info, warn};
use crate::session::{PeerId, SessionClosed, SessionEvent, SessionHandle, SessionRegistry};
/// Starts the advertiser. It hears of every session that comes up after
/// this returns, so call it before anything that can bring one up.
pub fn spawn_advertiser(
local_rib: LocalRib,
registry: SessionRegistry,
local_asn: u32,
) -> JoinHandle<()> {
let advertiser = Advertiser {
session_events: registry.subscribe(),
local_rib,
registry,
local_asn,
exporters: HashMap::new(),
};
tokio::spawn(advertiser.run())
}
/// An exporter, as the advertiser holds it.
struct Running {
session: SessionHandle,
/// Tells the exporter to send everything again.
resend: Arc<Notify>,
task: JoinHandle<()>,
}
impl Drop for Running {
fn drop(&mut self) {
self.task.abort();
}
}
struct Advertiser {
local_rib: LocalRib,
registry: SessionRegistry,
local_asn: u32,
session_events: Receiver<SessionEvent>,
exporters: HashMap<PeerId, Running>,
}
impl Advertiser {
/// Runs for as long as the server does. The server watches for this
/// task ending: without it nothing is announced.
async fn run(mut self) {
loop {
match self.session_events.recv().await {
Ok(SessionEvent::Up(session)) => self.start(session),
Ok(SessionEvent::Down(peer)) => {
self.exporters.remove(&peer);
}
Ok(SessionEvent::RefreshRequested(session)) => {
if let Some(exporter) = self.exporters.get(&session.peer) {
info!(peer = %session.peer, "Route refresh requested, re-sending local routes");
exporter.resend.notify_one();
}
}
Err(RecvError::Lagged(missed)) => {
warn!(
missed,
"Advertiser lagged on session events, checking every session"
);
self.reconcile();
}
// The registry is gone, and the server with it.
Err(RecvError::Closed) => return,
}
}
}
/// Starts an exporter for a session that has come up.
fn start(&mut self, session: SessionHandle) {
let resend = Arc::new(Notify::new());
let exporter = Exporter {
// Subscribed here, before the exporter first looks at the
// routes, so that no change between the two is missed.
changes: self.local_rib.subscribe(),
local_rib: self.local_rib.clone(),
session: session.clone(),
local_asn: self.local_asn,
resend: Arc::clone(&resend),
announced: HashSet::new(),
};
let running = Running {
session: session.clone(),
resend,
task: tokio::spawn(exporter.run()),
};
// A session with the same peer that this one replaces has gone,
// whether or not we have heard so yet; its exporter goes with it.
self.exporters.insert(session.peer, running);
}
/// Makes the exporters match the sessions there are, after events were
/// missed: sessions may have come and gone unheard, and peers may have
/// asked for their routes again.
fn reconcile(&mut self) {
let sessions = self.registry.list();
self.exporters.retain(|_, exporter| {
sessions
.iter()
.any(|session| session.is_same_session(&exporter.session))
});
for session in sessions {
match self.exporters.get(&session.peer) {
Some(exporter) => exporter.resend.notify_one(),
None => self.start(session),
}
}
}
}
/// Keeps one session's peer up to date with the routes we originate.
struct Exporter {
local_rib: LocalRib,
changes: Receiver<LocalRibEvent>,
session: SessionHandle,
local_asn: u32,
resend: Arc<Notify>,
/// The prefixes announced to the peer and not withdrawn since: our
/// Adj-RIB-Out for it (RFC 4271 §3.2), as far as it needs keeping.
announced: HashSet<IpPrefix>,
}
/// Why an exporter has nothing more to do.
struct Finished;
impl From<SessionClosed> for Finished {
fn from(_: SessionClosed) -> Self {
Self
}
}
impl Exporter {
/// Runs until the session closes or the local RIB stops; the
/// advertiser stops it when it hears the session has gone.
async fn run(mut self) {
info!(peer = %self.session.peer, "Session up, sending local routes");
let _ = self.export().await;
}
async fn export(&mut self) -> Result<(), Finished> {
self.send_all().await?;
loop {
tokio::select! {
change = self.changes.recv() => match change {
Ok(LocalRibEvent::Announced(route)) => self.announce(&route).await?,
Ok(LocalRibEvent::Withdrawn { prefix }) => self.withdraw(prefix).await?,
Err(RecvError::Lagged(missed)) => {
info!(
peer = %self.session.peer,
missed,
"Peer fell behind on local routes, bringing it up to date"
);
self.send_all().await?;
}
Err(RecvError::Closed) => return Err(Finished),
},
() = self.resend.notified() => self.send_all().await?,
}
}
}
/// Brings the peer to the routes as they now are: announces every one
/// of them, and withdraws what was announced and is no longer there.
///
/// Changes made while this runs are in `changes`, to be handled after.
/// Some of those may be older than the routes read here; handling them
/// again, in order, ends in the same place.
async fn send_all(&mut self) -> Result<(), Finished> {
let routes = self.local_rib.snapshot().await.map_err(|_| Finished)?;
let current: HashSet<IpPrefix> = routes.iter().map(|route| route.prefix).collect();
let gone: Vec<IpPrefix> = self.announced.difference(&current).copied().collect();
for prefix in gone {
self.withdraw(prefix).await?;
}
for route in &routes {
self.announce(route).await?;
}
Ok(())
}
async fn announce(&mut self, route: &LocalRoute) -> Result<(), Finished> {
let session = &self.session;
if !session.families.carries(&route.prefix) {
debug!(
peer = %session.peer,
prefix = %route.prefix,
"Skipping route: its address family was not negotiated"
);
return Ok(());
}
let towards = Towards {
local_asn: self.local_asn,
next_hop: session.local_addr.ip(),
four_octet_asn: session.four_octet_asn,
ibgp: session.ibgp,
};
let Some(update) = build_announce(route, &towards) else {
debug!(
peer = %session.peer,
prefix = %route.prefix,
"Skipping route: address family does not match session transport"
);
return Ok(());
};
debug!(peer = %session.peer, prefix = %route.prefix, "Announcing local route");
session.send(Message::Update(update)).await?;
self.announced.insert(route.prefix);
Ok(())
}
async fn withdraw(&mut self, prefix: IpPrefix) -> Result<(), Finished> {
if !self.announced.remove(&prefix) {
// The peer was never told of it.
return Ok(());
}
debug!(peer = %self.session.peer, %prefix, "Withdrawing local route");
self.session
.send(Message::Update(build_withdraw(&prefix)))
.await?;
Ok(())
}
}
/// What about a session decides how a route is announced on it.
#[derive(Debug, Clone, Copy)]
pub struct Towards {
pub local_asn: u32,
/// Our address on the session, which is the routes' next hop.
pub next_hop: IpAddr,
/// Whether the session negotiated four-octet AS numbers. If it did
/// not, and an AS number in the path needs four octets, the path is
/// also sent as AS4_PATH so that it survives the two-octet speaker
/// (RFC 6793 §4.2.2).
pub four_octet_asn: bool,
/// Whether the peer is in our own AS.
pub ibgp: bool,
}
/// Builds the UPDATE announcing `route` on a session.
///
/// Returns `None` when the prefix family does not match the next-hop family:
/// carrying IPv4 NLRI over an IPv6 next hop (RFC 8950) or vice versa is not
/// supported yet.
pub fn build_announce(route: &LocalRoute, towards: &Towards) -> Option<UpdateMessage> {
let attrs = &route.attrs;
// Our AS number goes on the path as a route leaves the AS, and not
// before (RFC 4271 §5.1.2): a route we originate has an empty path
// inside it. A peer in our AS that found our AS number on the path
// would take the route for one that had looped.
let as_path = AsPathAttribute {
segments: if towards.ibgp {
vec![]
} else {
vec![AsPathSegment {
ordered: true,
path: vec![towards.local_asn; 1 + usize::from(attrs.prepend)],
}]
},
};
let as4_path = if towards.four_octet_asn {
None
} else {
as_path.as4_path()
};
// Path attributes in ascending type-code order, as most implementations
// expect them.
let mut path_attributes = vec![
PathAttribute::Origin(attrs.origin),
PathAttribute::AsPath(as_path),
];
// IPv4 routes go in the body of the UPDATE with their next hop in
// NEXT_HOP; IPv6 routes go in MP_REACH_NLRI, which carries its own.
let mut announced_routes = vec![];
let mp_reach = match (route.prefix.address_family(), towards.next_hop) {
(IpVersion::Ipv4, IpAddr::V4(next_hop)) => {
path_attributes.push(PathAttribute::NextHop(NextHopPathAttribute(next_hop)));
announced_routes.push(route.prefix.into());
None
}
(IpVersion::Ipv6, IpAddr::V6(next_hop)) => {
Some(PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
family: AfiSafi::IPV6_UNICAST,
next_hop: NlriNextHop::Ipv6(next_hop),
prefixes: vec![route.prefix.into()],
}))
}
_ => return None,
};
if let Some(med) = attrs.med {
path_attributes.push(PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(
med,
)));
}
// LOCAL_PREF is sent to peers in our AS and to no others (RFC 4271
// §5.1.5).
if towards.ibgp {
path_attributes.push(PathAttribute::LocalPref(LocalPrefPathAttribute(
DEFAULT_LOCAL_PREF,
)));
}
if !attrs.communities.is_empty() {
path_attributes.push(PathAttribute::Communities(CommunitiesPathAttribute(
attrs.communities.clone(),
)));
}
path_attributes.extend(mp_reach);
path_attributes.extend(as4_path);
if !attrs.large_communities.is_empty() {
path_attributes.push(PathAttribute::LargeCommunities(
LargeCommunitiesPathAttribute {
communities: attrs.large_communities.clone(),
},
));
}
Some(UpdateMessage {
withdrawn_routes: vec![],
path_attributes,
announced_routes,
})
}
/// Builds the UPDATE withdrawing `prefix`.
pub fn build_withdraw(prefix: &IpPrefix) -> UpdateMessage {
match prefix.address_family() {
IpVersion::Ipv4 => UpdateMessage {
withdrawn_routes: vec![Nlri::from(*prefix)],
path_attributes: vec![],
announced_routes: vec![],
},
IpVersion::Ipv6 => UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![PathAttribute::MpUnreachNlri(MpUnreachNlriPathAttribute {
family: AfiSafi::IPV6_UNICAST,
prefixes: vec![Nlri::from(*prefix)],
})],
announced_routes: vec![],
},
}
}
#[cfg(test)]
mod tests {
use std::net::{Ipv4Addr, Ipv6Addr};
use bgp_packet::message::{LargeCommunity, OriginPathAttribute};
use bgp_primitives::parser::ParserContext;
use bgp_rib::LocalRouteAttrs;
use bytes::BytesMut;
use super::*;
fn prefix(s: &str) -> IpPrefix {
IpPrefix::try_from(s).unwrap()
}
/// An external peer.
fn towards(local_asn: u32, next_hop: IpAddr, four_octet_asn: bool) -> Towards {
Towards {
local_asn,
next_hop,
four_octet_asn,
ibgp: false,
}
}
#[test]
fn ipv4_announce_has_expected_attributes_and_roundtrips() {
let route = LocalRoute::new(
prefix("10.100.1.0/24"),
LocalRouteAttrs {
origin: OriginPathAttribute::IGP,
prepend: 2,
med: Some(50),
communities: vec![(65000, 100), (65535, 65281)],
large_communities: vec![LargeCommunity {
global_admin: 65000,
data_1: 1,
data_2: 2,
}],
},
);
let update = build_announce(
&route,
&towards(65000, IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)), true),
)
.unwrap();
assert_eq!(update.announced_routes, vec![prefix("10.100.1.0/24")]);
assert!(update.withdrawn_routes.is_empty());
assert_eq!(
update.path_attributes,
vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
AsPathAttribute::from_asns(vec![65000, 65000, 65000]),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(50)),
PathAttribute::Communities(CommunitiesPathAttribute(vec![
(65000, 100),
(65535, 65281)
])),
PathAttribute::LargeCommunities(LargeCommunitiesPathAttribute {
communities: vec![LargeCommunity {
global_admin: 65000,
data_1: 1,
data_2: 2
}],
}),
]
);
// Serialise and parse back to make sure the wire encoding is sound.
let ctx = ParserContext {
four_octet_asn: true,
ibgp: false,
add_path: Vec::new(),
};
let msg = Message::Update(update.clone());
let mut buf = BytesMut::new();
msg.to_wire(&ctx, &mut buf).unwrap();
let Message::Update(parsed) = Message::decode(&ctx, &buf).unwrap() else {
panic!("expected UPDATE")
};
assert_eq!(parsed, update);
}
#[test]
fn ipv6_announce_uses_mp_reach() {
let route = LocalRoute::new(prefix("2001:db8:1::/48"), LocalRouteAttrs::default());
let nh = Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1);
let update = build_announce(&route, &towards(65000, IpAddr::V6(nh), true)).unwrap();
assert!(update.announced_routes.is_empty());
assert!(
!update
.path_attributes
.iter()
.any(|a| matches!(a, PathAttribute::NextHop(_)))
);
assert!(update.path_attributes.iter().any(|a| matches!(
a,
PathAttribute::MpReachNlri(MpReachNlriPathAttribute { next_hop: NlriNextHop::Ipv6(h), prefixes, .. })
if *h == nh && *prefixes == vec![prefix("2001:db8:1::/48")]
)));
}
/// A two-octet session cannot carry a four-octet AS number in AS_PATH:
/// it goes out as AS_TRANS there, and in full in AS4_PATH.
#[test]
fn a_wide_as_number_reaches_a_two_octet_peer_in_as4_path() {
let route = LocalRoute::new(
prefix("10.100.1.0/24"),
LocalRouteAttrs {
prepend: 1,
..LocalRouteAttrs::default()
},
);
let next_hop = IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1));
let path = |update: &UpdateMessage, as4: bool| {
update.path_attributes.iter().find_map(|a| match a {
PathAttribute::As4Path(p) if as4 => Some(p.segments[0].path.clone()),
PathAttribute::AsPath(p) if !as4 => Some(p.segments[0].path.clone()),
_ => None,
})
};
let update = build_announce(&route, &towards(4_200_000_001, next_hop, false)).unwrap();
assert_eq!(path(&update, true), Some(vec![4_200_000_001; 2]));
// What the peer reads from the wire.
let ctx = ParserContext {
four_octet_asn: false,
ibgp: false,
add_path: Vec::new(),
};
let mut buf = BytesMut::new();
Message::Update(update).to_wire(&ctx, &mut buf).unwrap();
let Message::Update(seen) = Message::decode(&ctx, &buf).unwrap() else {
panic!("expected UPDATE")
};
assert_eq!(path(&seen, false), Some(vec![23456; 2]), "AS_TRANS");
assert_eq!(path(&seen, true), Some(vec![4_200_000_001; 2]));
// No AS4_PATH when it would say nothing new: the peer handles
// four-octet numbers, or ours fits in two.
let update = build_announce(&route, &towards(4_200_000_001, next_hop, true)).unwrap();
assert_eq!(path(&update, true), None);
let update = build_announce(&route, &towards(65000, next_hop, false)).unwrap();
assert_eq!(path(&update, true), None);
assert_eq!(path(&update, false), Some(vec![65000; 2]));
}
#[test]
fn family_mismatch_is_skipped() {
let v6 = LocalRoute::new(prefix("2001:db8::/32"), LocalRouteAttrs::default());
assert!(
build_announce(&v6, &towards(65000, IpAddr::V4(Ipv4Addr::LOCALHOST), true)).is_none()
);
let v4 = LocalRoute::new(prefix("10.0.0.0/8"), LocalRouteAttrs::default());
assert!(
build_announce(&v4, &towards(65000, IpAddr::V6(Ipv6Addr::LOCALHOST), true)).is_none()
);
}
#[test]
fn withdraw_by_family() {
let v4 = build_withdraw(&prefix("10.0.0.0/8"));
assert_eq!(v4.withdrawn_routes, vec![prefix("10.0.0.0/8")]);
assert!(v4.path_attributes.is_empty());
let v6 = build_withdraw(&prefix("2001:db8::/32"));
assert!(v6.withdrawn_routes.is_empty());
assert!(
matches!(&v6.path_attributes[..], [PathAttribute::MpUnreachNlri(m)] if m.prefixes == vec![prefix("2001:db8::/32")])
);
}
/// Inside our own AS a route we originate has an empty AS path and a
/// LOCAL_PREF. With our AS number on the path, a peer in our AS takes
/// the route for a loop; without LOCAL_PREF it treats the route as
/// withdrawn, as our own decoder does.
#[test]
fn a_route_announced_inside_our_as_has_an_empty_path_and_a_local_pref() {
let route = LocalRoute::new(
prefix("10.100.1.0/24"),
LocalRouteAttrs {
prepend: 3,
..LocalRouteAttrs::default()
},
);
let internal = Towards {
ibgp: true,
..towards(65000, IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)), true)
};
let update = build_announce(&route, &internal).unwrap();
assert_eq!(
update.path_attributes,
vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
PathAttribute::AsPath(AsPathAttribute { segments: vec![] }),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
PathAttribute::LocalPref(LocalPrefPathAttribute(100)),
]
);
// An internal peer accepts it as it stands.
let ctx = ParserContext {
four_octet_asn: true,
ibgp: true,
add_path: Vec::new(),
};
let mut buf = BytesMut::new();
Message::Update(update.clone())
.to_wire(&ctx, &mut buf)
.unwrap();
let decoded = Message::decode_with_report(&ctx, &buf).unwrap();
assert!(
decoded.update_errors.is_empty(),
"{:?}",
decoded.update_errors
);
assert_eq!(decoded.message, Message::Update(update));
// To an external peer the same route has our AS on the path, as
// many times as configured, and no LOCAL_PREF.
let external = Towards {
ibgp: false,
..internal
};
let update = build_announce(&route, &external).unwrap();
assert_eq!(
update.path_attributes[1],
AsPathAttribute::from_asns(vec![65000; 4])
);
assert!(
!update
.path_attributes
.iter()
.any(|a| matches!(a, PathAttribute::LocalPref(_)))
);
}
// -- Exporters --
use std::time::Duration;
use bgp_rib::table::{PeerInfo, RibTable};
use tokio::sync::mpsc;
use crate::collision::Direction;
use crate::family::FamilySet;
use crate::fsm::Negotiated;
use crate::session::SessionGuard;
/// A registry and local RIB with an advertiser running, and a table to
/// take peer numbers from.
struct Fixture {
registry: SessionRegistry,
local_rib: LocalRib,
table: RibTable,
}
/// The far end of a session's queue: what the session's task would
/// write to the peer.
struct Queue {
outbound: mpsc::Receiver<Message>,
/// What the peer holds, going by what it has been sent.
routes: HashSet<IpPrefix>,
_registration: SessionGuard,
}
impl Fixture {
fn new() -> Self {
let registry = SessionRegistry::new();
let local_rib = LocalRib::spawn();
spawn_advertiser(local_rib.clone(), registry.clone(), 65000);
Self {
registry,
local_rib,
table: RibTable::new(1),
}
}
/// A session with peer number `n`, whose queue holds `capacity`
/// messages.
fn session(&self, n: u8, capacity: usize) -> Queue {
let address = IpAddr::V4(Ipv4Addr::new(192, 0, 2, n));
let negotiated = Negotiated {
peer_asn: 65000 + u32::from(n),
ibgp: false,
peer_identifier: Ipv4Addr::new(10, 0, 0, n),
hold_time_secs: 90,
four_octet_asn: true,
families: FamilySet::supported(),
add_path_receive: FamilySet::default(),
add_path_send: FamilySet::default(),
};
let rib_peer = self
.table
.add_peer(PeerInfo {
asn: negotiated.peer_asn,
router_id: negotiated.peer_identifier,
address,
ibgp: false,
})
.unwrap();
let (tx, outbound) = mpsc::channel(capacity);
let handle = SessionHandle::new(
PeerId {
addr: (address, 179).into(),
asn: negotiated.peer_asn,
},
None,
Direction::Inbound,
(Ipv4Addr::new(192, 0, 2, 100), 179).into(),
&negotiated,
rib_peer,
tx,
);
Queue {
outbound,
routes: HashSet::new(),
_registration: self.registry.register(handle),
}
}
async fn announce(&self, n: u32) {
let route = LocalRoute::new(nth_prefix(n), LocalRouteAttrs::default());
self.local_rib.announce(route).await.unwrap();
}
async fn routes(&self) -> HashSet<IpPrefix> {
let routes = self.local_rib.snapshot().await.unwrap();
routes.iter().map(|route| route.prefix).collect()
}
}
fn nth_prefix(n: u32) -> IpPrefix {
let bytes = (0x0a00_0000 + (n << 8)).to_be_bytes();
IpPrefix::new(IpVersion::Ipv4, &bytes[..3], 24).unwrap()
}
impl Queue {
/// Reads what has been queued for the peer until it holds exactly
/// `expected`, as a peer would that takes every message in turn.
async fn read_until_it_has(&mut self, expected: &HashSet<IpPrefix>) {
let reading = async {
while self.routes != *expected {
let Some(Message::Update(update)) = self.outbound.recv().await else {
panic!("expected an UPDATE");
};
for nlri in &update.withdrawn_routes {
self.routes.remove(&nlri.prefix);
}
for nlri in &update.announced_routes {
self.routes.insert(nlri.prefix);
}
}
};
if tokio::time::timeout(Duration::from_secs(10), reading)
.await
.is_err()
{
panic!(
"the peer has {} routes, {} of them right, and should have {}",
self.routes.len(),
self.routes.intersection(expected).count(),
expected.len()
);
}
}
}
/// One peer that takes nothing must not keep the others from being
/// sent their routes. There used to be one task announcing to every
/// session in turn, and it waited for whichever queue was full.
#[tokio::test]
async fn a_peer_that_reads_nothing_holds_up_nobody_else() {
let fixture = Fixture::new();
let _stuck = fixture.session(1, 4);
let mut reading = fixture.session(2, 4);
for n in 0..500 {
fixture.announce(n).await;
}
reading.read_until_it_has(&fixture.routes().await).await;
assert_eq!(reading.routes.len(), 500);
}
/// A peer that reads so slowly that changes pass it by is brought up
/// to date when it reads again: with the routes there now are, and
/// without the ones that went while it was not looking. Those used to
/// stay announced to it for good.
#[tokio::test]
async fn a_peer_that_fell_behind_ends_up_with_the_routes_there_are() {
let fixture = Fixture::new();
let mut slow = fixture.session(1, 4);
for n in 0..10 {
fixture.announce(n).await;
}
slow.read_until_it_has(&fixture.routes().await).await;
// Far more changes than are kept for a subscriber that is not
// reading, among them the withdrawal of what the peer has.
for n in 0..10 {
fixture.local_rib.withdraw(nth_prefix(n)).await.unwrap();
}
for n in 10..3000 {
fixture.announce(n).await;
}
for n in 2000..3000 {
fixture.local_rib.withdraw(nth_prefix(n)).await.unwrap();
}
let routes = fixture.routes().await;
assert_eq!(routes.len(), 1990);
slow.read_until_it_has(&routes).await;
}
/// When a peer's session is replaced by a new one, the new session is
/// sent the routes from the beginning.
#[tokio::test]
async fn a_new_session_with_the_same_peer_starts_afresh() {
let fixture = Fixture::new();
fixture.announce(1).await;
let routes = fixture.routes().await;
let mut first = fixture.session(1, 4);
first.read_until_it_has(&routes).await;
drop(first);
let mut second = fixture.session(1, 4);
second.read_until_it_has(&routes).await;
}
}
+304
View File
@@ -0,0 +1,304 @@
//! Connection collision detection (RFC 4271 §6.8).
//!
//! Two speakers that dial each other can end up with two TCP connections.
//! Exactly one must survive and both ends must pick the same one: the
//! connection *initiated by the speaker with the higher BGP Identifier*.
//! The other is closed with Cease / Connection Collision Resolution.
//!
//! This cannot be a check local to one connection: the rival connection is
//! usually still mid-handshake (so it is not in the session registry yet),
//! and sometimes it is the rival, running in another task, that must close.
//! Hence one small shared map, holding the single connection currently
//! preferred for each peer and a way to tell it to stop.
//!
//! A peer here is an address together with the BGP Identifier it announced.
//! The identifier alone would not do: a router commonly has one session
//! over IPv4 and another over IPv6 with the same neighbour, which are two
//! sessions and not a collision; and it would let any host that can reach
//! us displace a peer's connection by claiming the peer's identifier.
//!
//! A collision with an *Established* connection closes the new one, unless
//! `detect_established` is set (the RFC's optional
//! `CollisionDetectEstablishedState`), which lets a restarted peer replace a
//! session we have not yet noticed is dead. If both connections were opened
//! by the same side the newer wins: the initiator has abandoned the older.
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use tokio::sync::oneshot;
use crate::metrics::COLLISIONS;
use crate::sync::IgnorePoison;
/// Which side opened the TCP connection.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
/// The peer connected to us.
Inbound,
/// We connected to the peer.
Outbound,
}
impl Direction {
pub fn as_str(&self) -> &'static str {
match self {
Direction::Inbound => "inbound",
Direction::Outbound => "outbound",
}
}
}
struct Entry {
id: u64,
direction: Direction,
established: bool,
kill: Option<oneshot::Sender<()>>,
}
/// True if the new connection is the one to close.
fn new_loses(
local_id: Ipv4Addr,
remote_id: Ipv4Addr,
existing: &Entry,
new: Direction,
detect_established: bool,
) -> bool {
if existing.established && !detect_established {
return true;
}
if existing.direction == new {
return false;
}
// Identifiers compare as unsigned 32-bit integers.
let keep = if u32::from(local_id) > u32::from(remote_id) {
Direction::Outbound
} else {
Direction::Inbound
};
new != keep
}
/// Who a connection is with: the address at the other end, and the BGP
/// Identifier announced from it.
type Peer = (IpAddr, Ipv4Addr);
/// Shared by every connection of one server.
#[derive(Clone)]
pub struct CollisionTable {
entries: Arc<Mutex<HashMap<Peer, Entry>>>,
next_id: Arc<AtomicU64>,
detect_established: bool,
}
impl CollisionTable {
pub fn new(detect_established: bool) -> Self {
Self {
entries: Default::default(),
next_id: Default::default(),
detect_established,
}
}
/// Call when a connection receives the peer's OPEN. `None` means this
/// connection lost and must be closed with Cease / Connection Collision
/// Resolution. Otherwise any rival has been told to close and this
/// connection holds the slot until the guard drops.
pub fn admit(
&self,
local_id: Ipv4Addr,
remote_addr: IpAddr,
remote_id: Ipv4Addr,
direction: Direction,
) -> Option<ConnectionGuard> {
let peer = (remote_addr, remote_id);
let mut entries = self.entries.locked();
if let Some(existing) = entries.get_mut(&peer) {
if new_loses(
local_id,
remote_id,
existing,
direction,
self.detect_established,
) {
COLLISIONS.with_label_values(&["new"]).inc();
return None;
}
if let Some(kill) = existing.kill.take() {
COLLISIONS.with_label_values(&["existing"]).inc();
let _ = kill.send(());
}
}
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
let (kill, killed) = oneshot::channel();
entries.insert(
peer,
Entry {
id,
direction,
established: false,
kill: Some(kill),
},
);
Some(ConnectionGuard {
table: self.clone(),
peer,
id,
killed,
})
}
}
/// Holds a connection's slot in the table; releases it on drop.
pub struct ConnectionGuard {
table: CollisionTable,
peer: Peer,
id: u64,
killed: oneshot::Receiver<()>,
}
impl ConnectionGuard {
pub fn set_established(&self) {
let mut entries = self.table.entries.locked();
if let Some(e) = entries.get_mut(&self.peer).filter(|e| e.id == self.id) {
e.established = true;
}
}
/// Completes when a later connection has won against this one; the
/// owner must then send Cease / Connection Collision Resolution and
/// close. Cancel-safe.
pub async fn killed(&mut self) {
// The sending half is dropped only when it has been used: the entry
// that holds it is removed by this guard alone, or replaced by a
// connection that signals first. Either way, the wait is over.
let _ = (&mut self.killed).await;
}
}
impl Drop for ConnectionGuard {
fn drop(&mut self) {
// A connection that lost has already been replaced in the map; only
// remove the slot if it is still ours.
let mut entries = self.table.entries.locked();
if entries.get(&self.peer).is_some_and(|e| e.id == self.id) {
entries.remove(&self.peer);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use Direction::*;
const LOW: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1);
const HIGH: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 1);
fn loses(
local: Ipv4Addr,
remote: Ipv4Addr,
existing: Direction,
established: bool,
new: Direction,
detect: bool,
) -> bool {
let e = Entry {
id: 0,
direction: existing,
established,
kill: None,
};
new_loses(local, remote, &e, new, detect)
}
#[test]
fn higher_identifier_keeps_the_connection_it_initiated() {
// We are HIGH: our outbound connection survives.
assert!(loses(HIGH, LOW, Outbound, false, Inbound, false));
assert!(!loses(HIGH, LOW, Inbound, false, Outbound, false));
// We are LOW: the peer's connection (inbound to us) survives.
assert!(!loses(LOW, HIGH, Outbound, false, Inbound, false));
assert!(loses(LOW, HIGH, Inbound, false, Outbound, false));
}
#[test]
fn both_ends_agree() {
// A and B dial each other. Each end holds its own dial as the
// existing (Outbound) connection and sees the other's dial arrive as
// new (Inbound). Exactly one of the two dials may be closed, so the
// ends must give opposite answers to "close the other side's dial?".
for (a, b) in [(LOW, HIGH), (HIGH, LOW)] {
let a_closes_dial_from_b = loses(a, b, Outbound, false, Inbound, false);
let b_closes_dial_from_a = loses(b, a, Outbound, false, Inbound, false);
assert_ne!(
a_closes_dial_from_b, b_closes_dial_from_a,
"ends disagree for a={a} b={b}"
);
}
}
#[test]
fn established_wins_unless_enabled() {
for new in [Inbound, Outbound] {
for existing in [Inbound, Outbound] {
assert!(loses(LOW, HIGH, existing, true, new, false));
assert!(loses(HIGH, LOW, existing, true, new, false));
}
}
assert!(!loses(LOW, HIGH, Outbound, true, Inbound, true));
assert!(loses(HIGH, LOW, Outbound, true, Inbound, true));
}
#[test]
fn same_direction_newer_wins() {
assert!(!loses(LOW, HIGH, Inbound, false, Inbound, false));
assert!(!loses(HIGH, LOW, Inbound, false, Inbound, false));
assert!(loses(LOW, HIGH, Inbound, true, Inbound, false));
assert!(!loses(LOW, HIGH, Inbound, true, Inbound, true));
}
#[tokio::test]
async fn table_signals_loser_and_releases_slots() {
let table = CollisionTable::new(false);
let addr: IpAddr = Ipv4Addr::new(192, 0, 2, 1).into();
// We are LOW: outbound first, then the peer's inbound wins.
let mut out = table.admit(LOW, addr, HIGH, Outbound).unwrap();
let inb = table.admit(LOW, addr, HIGH, Inbound).unwrap();
assert!(out.killed.try_recv().is_ok());
// The loser's guard dropping must not free the winner's slot.
drop(out);
inb.set_established();
assert!(table.admit(LOW, addr, HIGH, Outbound).is_none());
// A different identifier never collides.
assert!(
table
.admit(LOW, addr, Ipv4Addr::new(192, 0, 2, 2), Inbound)
.is_some()
);
// Once the survivor goes away the identifier is free again.
drop(inb);
assert!(table.admit(LOW, addr, HIGH, Outbound).is_some());
}
/// A neighbour reached over IPv4 and over IPv6 announces the same
/// identifier on both sessions. They are two sessions, and neither
/// closes the other. Nor can a connection from elsewhere that claims a
/// peer's identifier take the place of the peer's own.
#[tokio::test]
async fn the_same_identifier_from_another_address_is_another_peer() {
let table = CollisionTable::new(true);
let v4: IpAddr = Ipv4Addr::new(192, 0, 2, 1).into();
let v6: IpAddr = "2001:db8::1".parse().unwrap();
let mut over_v4 = table.admit(LOW, v4, HIGH, Outbound).unwrap();
over_v4.set_established();
let over_v6 = table.admit(LOW, v6, HIGH, Inbound);
assert!(over_v6.is_some());
assert!(over_v4.killed.try_recv().is_err(), "the first is untouched");
}
}
+451 -43
View File
@@ -1,12 +1,22 @@
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::str::FromStr;
use std::time::Duration;
use eyre::{Result, bail, eyre};
use bgp_packet::community;
use bgp_packet::message::OriginPathAttribute;
use bgp_packet::open::AddPathSendRecv;
use bgp_primitives::constants::AfiSafi;
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rib::{LocalRoute, LocalRouteAttrs};
use crate::proto::config::BgpConfig;
use crate::error::{ConfigError, OriginateProblem, PeerProblem};
use crate::family::FamilySet;
use crate::proto::config::{self as proto, BgpConfig, OriginatedRoute, Peer};
const DEFAULT_BGP_PORT: u16 = 179;
const DEFAULT_HOLD_TIME_SECS: u16 = 90;
const DEFAULT_CONNECT_RETRY: Duration = Duration::from_secs(5);
const MAX_AS_PATH_PREPEND: u32 = 16;
/// Validated, Rust-native server configuration derived from the proto config.
pub struct Config {
@@ -16,79 +26,477 @@ pub struct Config {
pub hold_time_secs: u16,
pub listen_endpoints: Vec<SocketAddr>,
pub peers: Vec<PeerConfig>,
/// Initial delay between attempts to connect to a configured peer;
/// doubles on each failure.
pub connect_retry: Duration,
/// Resolve collisions against Established sessions by BGP Identifier
/// instead of always closing the new connection (RFC 4271 §8.1.1
/// `CollisionDetectEstablishedState`).
pub collision_detect_established_state: bool,
/// Take connections from addresses that are not those of configured
/// peers. Off unless asked for: with it, any host that can reach us can
/// open a session and announce routes.
pub accept_unconfigured_peers: bool,
/// Routes this router originates.
pub originate: Vec<LocalRoute>,
/// Address for the HTTP API; `None` disables it.
pub http_endpoint: Option<SocketAddr>,
}
#[derive(Debug, Clone)]
pub struct PeerConfig {
pub name: String,
pub endpoint: SocketAddr,
pub remote_asn: u32,
pub hold_time_secs: u16,
/// Source address for connections we open to this peer; `None` lets the
/// operating system choose.
pub local_address: Option<IpAddr>,
/// The address families offered to this peer.
pub families: FamilySet,
/// The directions ADD-PATH (RFC 7911) is offered to this peer for, on
/// every one of `families`. `None` offers it for neither.
pub add_path: Option<AddPathSendRecv>,
}
impl Config {
pub fn from_proto(proto: BgpConfig) -> Result<Self> {
/// The configured peer at `address`, if there is one.
pub fn peer_at(&self, address: IpAddr) -> Option<&PeerConfig> {
self.peers.iter().find(|peer| peer.endpoint.ip() == address)
}
pub fn from_proto(proto: BgpConfig) -> Result<Self, ConfigError> {
// Peers refuse an identifier of 0.0.0.0, as we do theirs.
let router_id = Ipv4Addr::from_str(&proto.router_id)
.map_err(|_| eyre!("Invalid router_id {:?}: must be a dotted-quad IPv4 address", proto.router_id))?;
.ok()
.filter(|id| !id.is_unspecified())
.ok_or_else(|| ConfigError::RouterId(proto.router_id.clone()))?;
if proto.local_asn == 0 {
bail!("local_asn must be non-zero");
return Err(ConfigError::LocalAsn);
}
let listen_endpoints = if proto.listen_endpoints.is_empty() {
vec![
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), DEFAULT_BGP_PORT),
SocketAddr::new(IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED), DEFAULT_BGP_PORT),
SocketAddr::new(
IpAddr::V6(std::net::Ipv6Addr::UNSPECIFIED),
DEFAULT_BGP_PORT,
),
]
} else {
proto.listen_endpoints
.into_iter()
proto
.listen_endpoints
.iter()
.map(|ep| parse_endpoint(&ep.address, ep.port))
.collect::<Result<_>>()?
.collect::<Result<_, _>>()?
};
let peers = proto.peers
.into_iter()
.map(|p| {
let ep = p.endpoint.as_ref()
.ok_or_else(|| eyre!("Peer {:?} is missing endpoint", p.name))?;
let endpoint = parse_endpoint(&ep.address, ep.port)?;
let mut peers: Vec<PeerConfig> = Vec::with_capacity(proto.peers.len());
for peer in proto.peers {
let name = peer.name.clone();
let rejected = |problem| ConfigError::Peer {
peer: name.clone(),
problem,
};
let peer = parse_peer(peer).map_err(rejected)?;
// A peer is told from the others by its name in what the
// dialer reports, and by its address when it connects to us.
if peers.iter().any(|earlier| earlier.name == peer.name) {
return Err(rejected(PeerProblem::DuplicateName));
}
if peers
.iter()
.any(|earlier| earlier.endpoint.ip() == peer.endpoint.ip())
{
return Err(rejected(PeerProblem::DuplicateAddress));
}
peers.push(peer);
}
if p.remote_asn == 0 {
bail!("Peer {:?} remote_asn must be non-zero", p.name);
}
let mut originate: Vec<LocalRoute> = Vec::with_capacity(proto.originate.len());
for entry in &proto.originate {
let rejected = |problem| ConfigError::Originate {
prefix: entry.prefix.clone(),
problem,
};
let route = parse_originated_route(entry).map_err(rejected)?;
if originate
.iter()
.any(|earlier| earlier.prefix == route.prefix)
{
return Err(rejected(OriginateProblem::Duplicate));
}
originate.push(route);
}
let hold_time_secs = if p.hold_time_seconds == 0 {
DEFAULT_HOLD_TIME_SECS
} else if p.hold_time_seconds < 3 {
bail!("Peer {:?} hold_time_seconds must be 0 or >= 3", p.name);
} else {
p.hold_time_seconds as u16
};
let http_endpoint = match proto.http_endpoint.into_option() {
Some(ep) if ep.port == 0 => return Err(ConfigError::HttpPort),
Some(ep) => Some(parse_endpoint(&ep.address, ep.port)?),
None => None,
};
Ok(PeerConfig {
name: p.name,
endpoint,
remote_asn: p.remote_asn,
hold_time_secs,
})
})
.collect::<Result<_>>()?;
let connect_retry = if proto.connect_retry_seconds == 0 {
DEFAULT_CONNECT_RETRY
} else {
Duration::from_secs(proto.connect_retry_seconds as u64)
};
Ok(Self { router_id, local_asn: proto.local_asn, hold_time_secs: DEFAULT_HOLD_TIME_SECS, listen_endpoints, peers })
Ok(Self {
router_id,
local_asn: proto.local_asn,
hold_time_secs: DEFAULT_HOLD_TIME_SECS,
listen_endpoints,
peers,
connect_retry,
collision_detect_established_state: proto.collision_detect_established_state,
accept_unconfigured_peers: proto.accept_unconfigured_peers,
originate,
http_endpoint,
})
}
}
fn parse_endpoint(address: &str, port: u32) -> Result<SocketAddr> {
let port = if port == 0 {
DEFAULT_BGP_PORT
} else if port > u16::MAX as u32 {
bail!("Port {port} is out of range");
} else {
port as u16
fn parse_peer(peer: Peer) -> Result<PeerConfig, PeerProblem> {
let ep = peer.endpoint.as_ref().ok_or(PeerProblem::MissingEndpoint)?;
let endpoint =
parse_endpoint(&ep.address, ep.port).map_err(|e| PeerProblem::Endpoint(Box::new(e)))?;
if peer.remote_asn == 0 {
return Err(PeerProblem::RemoteAsn);
}
let hold_time_secs = match peer.hold_time_seconds {
0 => DEFAULT_HOLD_TIME_SECS,
// RFC 4271 §4.2: zero, or at least three seconds.
1 | 2 => return Err(PeerProblem::HoldTime),
secs => u16::try_from(secs).map_err(|_| PeerProblem::HoldTime)?,
};
let ip = IpAddr::from_str(address)
.map_err(|_| eyre!("Invalid IP address {address:?}"))?;
let local_address = if peer.local_address.is_empty() {
None
} else {
let ip = IpAddr::from_str(&peer.local_address)
.map_err(|_| PeerProblem::LocalAddress(peer.local_address.clone()))?;
if ip.is_ipv4() != endpoint.is_ipv4() {
return Err(PeerProblem::AddressFamilyMismatch);
}
Some(ip)
};
let families = if peer.address_families.is_empty() {
FamilySet::supported()
} else {
peer.address_families
.iter()
.map(|family| match family.enum_value() {
Ok(proto::AddressFamily::IPV4_UNICAST) => Ok(AfiSafi::IPV4_UNICAST),
Ok(proto::AddressFamily::IPV6_UNICAST) => Ok(AfiSafi::IPV6_UNICAST),
Ok(proto::AddressFamily::ADDRESS_FAMILY_UNSPECIFIED) | Err(_) => {
Err(PeerProblem::AddressFamily)
}
})
.collect::<Result<_, _>>()?
};
let add_path = match peer.add_path.enum_value() {
Ok(proto::AddPath::ADD_PATH_OFF) => None,
Ok(proto::AddPath::ADD_PATH_RECEIVE) => Some(AddPathSendRecv::Receive),
Ok(proto::AddPath::ADD_PATH_SEND) => Some(AddPathSendRecv::Send),
Ok(proto::AddPath::ADD_PATH_BOTH) => Some(AddPathSendRecv::Both),
Err(_) => return Err(PeerProblem::AddPath),
};
Ok(PeerConfig {
// A peer without a name goes by its address.
name: if peer.name.is_empty() {
endpoint.ip().to_string()
} else {
peer.name
},
endpoint,
remote_asn: peer.remote_asn,
hold_time_secs,
local_address,
families,
add_path,
})
}
fn parse_endpoint(address: &str, port: u32) -> Result<SocketAddr, ConfigError> {
let port = match port {
0 => DEFAULT_BGP_PORT,
port => u16::try_from(port).map_err(|_| ConfigError::Port(port))?,
};
let ip = IpAddr::from_str(address).map_err(|_| ConfigError::Address(address.to_string()))?;
Ok(SocketAddr::new(ip, port))
}
fn parse_originated_route(route: &OriginatedRoute) -> Result<LocalRoute, OriginateProblem> {
let prefix = IpPrefix::try_from(route.prefix.as_str())?;
let origin = match route.origin.to_ascii_uppercase().as_str() {
"" | "IGP" => OriginPathAttribute::IGP,
"EGP" => OriginPathAttribute::EGP,
"INCOMPLETE" => OriginPathAttribute::INCOMPLETE,
_ => return Err(OriginateProblem::Origin(route.origin.clone())),
};
let prepend = u8::try_from(route.as_path_prepend)
.ok()
.filter(|&n| u32::from(n) <= MAX_AS_PATH_PREPEND)
.ok_or(OriginateProblem::Prepend {
max: MAX_AS_PATH_PREPEND,
})?;
let communities = route
.communities
.iter()
.map(|c| community::parse_standard(c))
.collect::<Result<_, _>>()?;
let large_communities = route
.large_communities
.iter()
.map(|c| community::parse_large(c))
.collect::<Result<_, _>>()?;
Ok(LocalRoute::new(
prefix,
LocalRouteAttrs {
origin,
prepend,
med: route.med,
communities,
large_communities,
},
))
}
#[cfg(test)]
mod tests {
use bgp_primitives::ip_prefix::PrefixError;
use super::*;
fn parse(text: &str) -> Result<Config, ConfigError> {
let proto = protobuf::text_format::parse_from_str::<BgpConfig>(text)
.expect("the test's textproto is well formed");
Config::from_proto(proto)
}
fn rejected(text: &str) -> ConfigError {
parse(text).err().expect(text)
}
const BASE: &str = "router_id: \"192.0.2.1\" local_asn: 65000\n";
#[test]
fn originated_routes_are_parsed_with_their_attributes() {
let config = parse(&format!(
"{BASE}originate {{ prefix: \"192.0.2.0/24\" }}
originate {{
prefix: \"2001:db8::/32\" origin: \"incomplete\" as_path_prepend: 2 med: 0
communities: \"65000:100\" communities: \"NO_EXPORT\"
large_communities: \"65000:1:2\"
}}"
))
.unwrap();
assert_eq!(config.originate.len(), 2);
let plain = &config.originate[0];
assert_eq!(plain.prefix, IpPrefix::try_from("192.0.2.0/24").unwrap());
assert_eq!(plain.attrs, LocalRouteAttrs::default());
let full = &config.originate[1].attrs;
assert_eq!(full.origin, OriginPathAttribute::INCOMPLETE);
assert_eq!(full.prepend, 2);
assert_eq!(
full.med,
Some(0),
"an explicit MED of 0 is not the same as no MED"
);
assert_eq!(full.communities, vec![(65000, 100), (65535, 65281)]);
assert_eq!(full.large_communities.len(), 1);
}
#[test]
fn bad_originated_routes_are_rejected_with_the_entry_and_the_reason() {
let problem = |body: &str| match rejected(&format!("{BASE}originate {{ {body} }}")) {
ConfigError::Originate { problem, .. } => problem,
other => panic!("{body}: {other}"),
};
assert_eq!(
problem("prefix: \"not-a-prefix\""),
OriginateProblem::Prefix(PrefixError::Format("not-a-prefix".into()))
);
assert_eq!(
problem("prefix: \"192.0.2.0/24\" origin: \"static\""),
OriginateProblem::Origin("static".into())
);
assert_eq!(
problem("prefix: \"192.0.2.0/24\" as_path_prepend: 17"),
OriginateProblem::Prepend { max: 16 }
);
assert!(matches!(
problem("prefix: \"192.0.2.0/24\" communities: \"70000:1\""),
OriginateProblem::Community(_)
));
assert!(matches!(
problem("prefix: \"192.0.2.0/24\" large_communities: \"1:2\""),
OriginateProblem::Community(_)
));
let duplicate = rejected(&format!(
"{BASE}originate {{ prefix: \"192.0.2.0/24\" }} originate {{ prefix: \"192.0.2.0/24\" med: 5 }}"
));
assert_eq!(
duplicate,
ConfigError::Originate {
prefix: "192.0.2.0/24".into(),
problem: OriginateProblem::Duplicate
}
);
// The message names the entry, so the operator can find it.
assert_eq!(
duplicate.to_string(),
"originate \"192.0.2.0/24\": listed more than once"
);
}
#[test]
fn a_peer_is_offered_every_address_family_unless_some_are_listed() {
let families = |body: &str| {
let config = parse(&format!(
"{BASE}peers {{ name: \"r1\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 1 {body} }}"
))
.unwrap();
config.peers[0].families.clone()
};
assert_eq!(families(""), FamilySet::supported());
assert_eq!(
families("address_families: IPV6_UNICAST"),
[AfiSafi::IPV6_UNICAST].into_iter().collect()
);
assert_eq!(
families("address_families: IPV4_UNICAST address_families: IPV6_UNICAST"),
FamilySet::supported()
);
}
#[test]
fn add_path_is_off_unless_a_peer_asks_for_it() {
let add_path = |body: &str| {
let config = parse(&format!(
"{BASE}peers {{ name: \"r1\" endpoint {{ address: \"192.0.2.2\" }} remote_asn: 1 {body} }}"
))
.unwrap();
config.peers[0].add_path
};
assert_eq!(add_path(""), None);
assert_eq!(add_path("add_path: ADD_PATH_OFF"), None);
assert_eq!(
add_path("add_path: ADD_PATH_RECEIVE"),
Some(AddPathSendRecv::Receive)
);
assert_eq!(
add_path("add_path: ADD_PATH_BOTH"),
Some(AddPathSendRecv::Both)
);
}
#[test]
fn bad_peers_and_endpoints_are_rejected_with_the_peer_named() {
assert_eq!(
rejected("router_id: \"nope\" local_asn: 1"),
ConfigError::RouterId("nope".into())
);
assert_eq!(rejected("router_id: \"192.0.2.1\""), ConfigError::LocalAsn);
let problem = |body: &str| match rejected(&format!("{BASE}peers {{ name: \"r1\" {body} }}"))
{
ConfigError::Peer { peer, problem } => {
assert_eq!(peer, "r1");
problem
}
other => panic!("{body}: {other}"),
};
let endpoint = "endpoint { address: \"192.0.2.2\" }";
assert_eq!(problem("remote_asn: 1"), PeerProblem::MissingEndpoint);
assert_eq!(problem(endpoint), PeerProblem::RemoteAsn);
assert_eq!(
problem(&format!("{endpoint} remote_asn: 1 hold_time_seconds: 2")),
PeerProblem::HoldTime
);
assert_eq!(
problem(&format!(
"{endpoint} remote_asn: 1 hold_time_seconds: 70000"
)),
PeerProblem::HoldTime
);
assert_eq!(
problem(&format!("{endpoint} remote_asn: 1 local_address: \"::1\"")),
PeerProblem::AddressFamilyMismatch
);
assert_eq!(
problem(&format!(
"{endpoint} remote_asn: 1 address_families: ADDRESS_FAMILY_UNSPECIFIED"
)),
PeerProblem::AddressFamily
);
assert_eq!(
problem("endpoint { address: \"192.0.2.2\" port: 70000 } remote_asn: 1"),
PeerProblem::Endpoint(Box::new(ConfigError::Port(70000)))
);
assert_eq!(
rejected(&format!("{BASE}http_endpoint {{ address: \"127.0.0.1\" }}")),
ConfigError::HttpPort
);
}
/// Peers are told apart by name where the dialer reports on them and by
/// address when they connect to us; two that share either would be
/// taken for each other.
#[test]
fn peers_must_differ_in_name_and_address() {
let peer = |name: &str, address: &str| {
format!(
"peers {{ name: \"{name}\" endpoint {{ address: \"{address}\" }} remote_asn: 1 }}\n"
)
};
let problem = |peers: String| match rejected(&format!("{BASE}{peers}")) {
ConfigError::Peer { problem, .. } => problem,
other => panic!("{other}"),
};
assert_eq!(
problem(peer("r1", "192.0.2.2") + &peer("r1", "192.0.2.3")),
PeerProblem::DuplicateName
);
assert_eq!(
problem(peer("r1", "192.0.2.2") + &peer("r2", "192.0.2.2")),
PeerProblem::DuplicateAddress
);
// A peer without a name goes by its address, so two of them differ.
let config = parse(&format!(
"{BASE}{}{}",
peer("", "192.0.2.2"),
peer("", "192.0.2.3")
))
.unwrap();
assert_eq!(config.peers[0].name, "192.0.2.2");
assert_eq!(config.peers[1].name, "192.0.2.3");
assert_eq!(
config
.peer_at("192.0.2.3".parse().unwrap())
.map(|p| &*p.name),
Some("192.0.2.3")
);
assert!(config.peer_at("192.0.2.4".parse().unwrap()).is_none());
}
/// A peer would refuse our OPEN.
#[test]
fn a_router_id_of_zero_is_rejected() {
assert_eq!(
rejected("router_id: \"0.0.0.0\" local_asn: 1"),
ConfigError::RouterId("0.0.0.0".into())
);
}
}
File diff suppressed because it is too large. Load diff
+116
View File
@@ -0,0 +1,116 @@
//! Active side of the FSM: dial each configured peer and keep the session up.
//!
//! One task per configured peer loops forever: connect, run the session,
//! back off, repeat. While a session with the same remote IP and ASN is
//! already established (for example because the peer dialled us first) the
//! dialer stays idle rather than opening a second one. When both sides dial
//! at once the FSM resolves the collision (see [`crate::collision`]); if our
//! connection is the one closed, the next pass finds the surviving session
//! and goes idle.
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tokio::net::{TcpSocket, TcpStream};
use tokio::task::JoinHandle;
use tracing::{info, warn};
use crate::collision::Direction;
use crate::config::PeerConfig;
use crate::connection::{PeerSettings, ServerContext, run_session};
use crate::session::DialState;
/// Upper bound for exponential backoff between connection attempts.
const MAX_BACKOFF: Duration = Duration::from_secs(120);
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
pub fn spawn_dialers(
peers: &[PeerConfig],
server: &Arc<ServerContext>,
connect_retry: Duration,
) -> Vec<JoinHandle<()>> {
peers
.iter()
.map(|peer| tokio::spawn(dial_loop(peer.clone(), Arc::clone(server), connect_retry)))
.collect()
}
/// Opens the TCP connection, from the configured source address if any.
async fn connect(peer: &PeerConfig) -> std::io::Result<TcpStream> {
let Some(local) = peer.local_address else {
return TcpStream::connect(peer.endpoint).await;
};
let socket = if peer.endpoint.is_ipv4() {
TcpSocket::new_v4()?
} else {
TcpSocket::new_v6()?
};
socket
.bind(std::net::SocketAddr::new(local, 0))
.map_err(|e| {
std::io::Error::new(e.kind(), format!("cannot use local_address {local}: {e}"))
})?;
socket.connect(peer.endpoint).await
}
async fn dial_loop(peer: PeerConfig, server: Arc<ServerContext>, connect_retry: Duration) {
let registry = &server.registry;
let mut backoff = connect_retry;
loop {
if registry.is_shutting_down() {
return;
}
if registry.find(peer.endpoint.ip(), peer.remote_asn).is_some() {
// Peer dialled us; nothing to do until that session goes away.
registry.set_dial_state(&peer.name, DialState::Idle);
tokio::time::sleep(connect_retry).await;
continue;
}
registry.set_dial_state(&peer.name, DialState::Connecting);
info!(peer = %peer.name, endpoint = %peer.endpoint, "Connecting to peer");
let ended = match tokio::time::timeout(CONNECT_TIMEOUT, connect(&peer)).await {
Ok(Ok(stream)) => {
info!(peer = %peer.name, endpoint = %peer.endpoint, "TCP connected, starting session");
let end = run_session(
stream,
peer.endpoint,
Direction::Outbound,
Arc::clone(&server),
PeerSettings::from(&peer),
)
.await;
// A session that came up is a peer worth dialling again
// soon. One that connects and is then refused, for its AS
// number say, will be refused again: it is retried ever
// more slowly, like one that cannot be reached at all.
if end.established {
backoff = connect_retry;
}
end.result.map_err(|e| e.to_string())
}
Ok(Err(e)) => Err(format!("connect failed: {e}")),
Err(_) => Err(format!("connect timed out after {CONNECT_TIMEOUT:?}")),
};
let reason = match ended {
Ok(()) => "session closed at our request".to_string(),
Err(reason) => {
warn!(peer = %peer.name, endpoint = %peer.endpoint, err = %reason, "Session attempt ended");
reason
}
};
registry.set_dial_state(
&peer.name,
DialState::Backoff {
until: SystemTime::now() + backoff,
last_error: reason,
},
);
tokio::time::sleep(backoff).await;
backoff = (backoff * 2).min(MAX_BACKOFF);
}
}
+248
View File
@@ -0,0 +1,248 @@
//! Error types of the server crate.
use std::io;
use std::net::SocketAddr;
use std::time::Duration;
use bgp_packet::errors::{BgpError, CeaseSubcode, DecodeError, FsmSubcode, OpenMsgSubcode};
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;
use crate::fsm::State;
/// Why a session ended abnormally.
#[derive(Debug, Error)]
pub enum SessionError {
#[error("connection failed: {0}")]
Io(#[from] io::Error),
#[error("received a message that could not be decoded: {0}")]
Decode(#[from] DecodeError),
#[error("could not encode a message: {0}")]
Encode(#[from] EncodeError),
/// The peer sent nothing for as long as the pre-negotiation hold time.
#[error("timed out waiting for a message in the {0} state")]
Timeout(State),
#[error("hold timer expired")]
HoldTimerExpired,
#[error("received {received} in the {state} state")]
UnexpectedMessage {
state: State,
received: &'static str,
},
/// The peer ended the session. Nothing is sent in return.
#[error("peer sent NOTIFICATION: {0}")]
NotifiedByPeer(BgpError),
#[error("refused the peer's OPEN: {0}")]
OpenRefused(BgpError),
#[error("peer announced AS{actual} but AS{expected} was configured")]
WrongPeerAs { expected: u32, actual: u32 },
#[error("closed by connection collision resolution")]
Collision,
/// The routing table has no room for another peer.
#[error(transparent)]
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}")]
Panicked(String),
/// The peer offers none of the address families configured for it, so
/// the session could carry nothing.
#[error("no address family in common: we offer {ours}, the peer offers {theirs}")]
NoCommonAddressFamily { ours: FamilySet, theirs: FamilySet },
/// The peer took nothing we sent for this long (RFC 9687): it has
/// stopped reading, and there is no telling it so.
#[error("the peer accepted no data for {0:?}")]
SendHoldTimerExpired(Duration),
}
impl SessionError {
/// The NOTIFICATION that tells the peer about this error (RFC 4271 §6),
/// or `None` when there is nothing to say or no way to say it.
pub fn notification(&self) -> Option<NotificationMessage> {
let error = match self {
Self::Io(_) | Self::SendHoldTimerExpired(_) | Self::NotifiedByPeer(_) => return None,
// Something of ours could not be written out, which is a bug
// here: as for a panic, the session is reset.
Self::Encode(_) => BgpError::Cease(CeaseSubcode::AdminReset),
Self::Decode(e) => {
return Some(NotificationMessage {
error: e.notification.clone(),
data: e.data.clone(),
});
}
Self::Timeout(_) | Self::HoldTimerExpired => BgpError::HoldTimer,
Self::UnexpectedMessage { state, .. } => BgpError::FsmError(match state {
State::OpenSent => FsmSubcode::UnexpectedOpenSent,
State::OpenConfirm => FsmSubcode::UnexpectedOpenConfirm,
// A closed machine handles no messages; Established is the
// only other state one can arrive in.
State::Established | State::Closed => FsmSubcode::UnexpectedEstablished,
}),
Self::OpenRefused(error) => error.clone(),
Self::WrongPeerAs { .. } => BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs),
Self::Collision => BgpError::Cease(CeaseSubcode::ConnCollisionResolution),
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),
// RFC 5492 §5: the Data lists the capabilities we need and the
// peer did not offer.
Self::NoCommonAddressFamily { ours, .. } => {
return Some(NotificationMessage {
error: BgpError::OpenMsg(OpenMsgSubcode::UnsupportedCapability),
data: ours.capabilities_wire(),
});
}
};
Some(NotificationMessage {
error,
data: vec![],
})
}
}
/// Why the server could not start, or could not go on.
#[derive(Debug, Error)]
pub enum ServerError {
#[error("failed to bind listener on {addr}: {source}")]
Bind {
addr: SocketAddr,
#[source]
source: io::Error,
},
#[error("listener failed: {0}")]
Io(#[from] io::Error),
/// A RIB's actor died. The server cannot work without its tables.
#[error(transparent)]
RibStopped(#[from] RibStopped),
/// A task the server depends on ended, which none of them do unless
/// they panic.
#[error("the {task} task stopped unexpectedly: {reason}")]
TaskStopped { task: &'static str, reason: String },
}
/// Why a configuration was rejected.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum ConfigError {
#[error("invalid router_id {0:?}: must be a dotted-quad IPv4 address other than 0.0.0.0")]
RouterId(String),
#[error("local_asn must be non-zero")]
LocalAsn,
#[error("invalid IP address {0:?}")]
Address(String),
#[error("port {0} is out of range")]
Port(u32),
#[error("http_endpoint.port is required")]
HttpPort,
#[error("peer {peer:?}: {problem}")]
Peer { peer: String, problem: PeerProblem },
#[error("originate {prefix:?}: {problem}")]
Originate {
prefix: String,
problem: OriginateProblem,
},
}
/// What is wrong with one `peers` entry.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum PeerProblem {
#[error("endpoint is missing")]
MissingEndpoint,
#[error("endpoint is invalid: {0}")]
Endpoint(Box<ConfigError>),
#[error("remote_asn must be non-zero")]
RemoteAsn,
#[error("hold_time_seconds must be 0 or at least 3")]
HoldTime,
#[error("there is another peer of this name")]
DuplicateName,
#[error("there is another peer at this address")]
DuplicateAddress,
#[error("local_address {0:?} is not an IP address")]
LocalAddress(String),
#[error("local_address and endpoint are different address families")]
AddressFamilyMismatch,
#[error("address_families has a value that is not an address family")]
AddressFamily,
#[error("add_path has a value that is not one of its settings")]
AddPath,
}
/// What is wrong with one `originate` entry.
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum OriginateProblem {
#[error("{0}")]
Prefix(#[from] bgp_primitives::ip_prefix::PrefixError),
#[error("origin {0:?} must be IGP, EGP or INCOMPLETE")]
Origin(String),
#[error("as_path_prepend must be at most {max}")]
Prepend { max: u32 },
#[error("{0}")]
Community(#[from] bgp_packet::community::CommunityError),
#[error("listed more than once")]
Duplicate,
}
#[cfg(test)]
mod tests {
use bgp_packet::errors::MsgHeaderSubcode;
use super::*;
#[test]
fn each_session_error_names_the_notification_rfc_4271_assigns_it() {
let notification = |e: SessionError| e.notification().map(|n| (n.error, n.data));
assert_eq!(
notification(SessionError::Decode(DecodeError {
notification: BgpError::MsgHeader(MsgHeaderSubcode::BadMessageLength),
data: vec![0x13, 0x88],
detail: "too long".into(),
})),
Some((
BgpError::MsgHeader(MsgHeaderSubcode::BadMessageLength),
vec![0x13, 0x88]
))
);
assert_eq!(
notification(SessionError::Timeout(State::OpenSent)),
Some((BgpError::HoldTimer, vec![]))
);
assert_eq!(
notification(SessionError::UnexpectedMessage {
state: State::OpenConfirm,
received: "update"
}),
Some((
BgpError::FsmError(FsmSubcode::UnexpectedOpenConfirm),
vec![]
))
);
assert_eq!(
notification(SessionError::WrongPeerAs {
expected: 1,
actual: 2
}),
Some((BgpError::OpenMsg(OpenMsgSubcode::BadPeerAs), vec![]))
);
// Something of ours that cannot be written is our fault: the peer
// is told the session is being reset, not left to time it out.
assert_eq!(
notification(SessionError::Encode(EncodeError::TooLong {
what: "path attribute",
len: 70000,
max: 65535
})),
Some((BgpError::Cease(CeaseSubcode::AdminReset), vec![]))
);
// A broken connection cannot carry a NOTIFICATION.
assert_eq!(
notification(SessionError::Io(io::ErrorKind::BrokenPipe.into())),
None
);
}
}
+292
View File
@@ -0,0 +1,292 @@
//! The address families a session can carry, and which of them it does.
//!
//! Each side lists the families it is willing to exchange in its OPEN, as
//! Multiprotocol capabilities (RFC 4760 §8). A family is in use on a session
//! only if both sides listed it.
//!
//! Any family can be named, read from a peer and held in a [`FamilySet`].
//! The ones this speaker can actually exchange routes for are [`SUPPORTED`],
//! and only those are ever offered to a peer.
use std::collections::BTreeSet;
use std::fmt;
use bgp_packet::message::{PathAttribute, UpdateMessage};
use bgp_packet::open::{Capability, OpenMessage};
use bgp_primitives::constants::AfiSafi;
use bgp_primitives::ip_prefix::IpPrefix;
use bytes::BytesMut;
/// The address families this speaker can exchange routes for. Supporting a
/// new one means being able to parse, store and advertise its routes; then
/// it is added here.
pub const SUPPORTED: [AfiSafi; 2] = [AfiSafi::IPV4_UNICAST, AfiSafi::IPV6_UNICAST];
/// A set of address families.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct FamilySet(BTreeSet<AfiSafi>);
impl FamilySet {
/// Every family in [`SUPPORTED`].
pub fn supported() -> Self {
SUPPORTED.into_iter().collect()
}
pub fn insert(&mut self, family: AfiSafi) {
self.0.insert(family);
}
pub fn contains(&self, family: AfiSafi) -> bool {
self.0.contains(&family)
}
/// Whether a unicast route for `prefix` may be exchanged.
pub fn carries(&self, prefix: &IpPrefix) -> bool {
self.contains(AfiSafi::unicast(prefix.address_family()))
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn intersection(&self, other: &Self) -> Self {
Self(self.0.intersection(&other.0).copied().collect())
}
pub fn iter(&self) -> impl Iterator<Item = AfiSafi> + '_ {
self.0.iter().copied()
}
/// The families a peer offered in its OPEN, whether or not we support
/// them.
///
/// A peer that sends no Multiprotocol capability at all is a speaker of
/// plain RFC 4271 BGP, which carries IPv4 unicast and nothing else. One
/// that sends any has listed everything it wants, IPv4 unicast included.
pub fn offered_by(open: &OpenMessage) -> Self {
let listed: Self = open
.capabilities()
.filter_map(|capability| match capability {
Capability::MultiProtocol { afi, safi } => Some(AfiSafi::new(*afi, *safi)),
_ => None,
})
.collect();
if listed.is_empty() {
[AfiSafi::IPV4_UNICAST].into_iter().collect()
} else {
listed
}
}
/// These families as Multiprotocol capabilities in wire form: the Data
/// of an Unsupported Capability NOTIFICATION (RFC 5492 §5).
pub fn capabilities_wire(&self) -> Vec<u8> {
let mut out = BytesMut::new();
for family in self.iter() {
// A Multiprotocol capability always encodes; if that ever
// changed, a NOTIFICATION with less Data is still correct.
if Capability::multiprotocol(family).to_wire(&mut out).is_err() {
return vec![];
}
}
out.to_vec()
}
/// Removes from `update` every route of a family that is not in this
/// set, and returns how many prefixes that was.
///
/// The withdrawn routes and NLRI in the body of an UPDATE are IPv4
/// unicast; every other family travels in MP_REACH_NLRI and
/// MP_UNREACH_NLRI, which name theirs.
pub fn restrict(&self, update: &mut UpdateMessage) -> usize {
let mut removed = 0;
if !self.contains(AfiSafi::IPV4_UNICAST) {
removed += update.withdrawn_routes.len() + update.announced_routes.len();
update.withdrawn_routes.clear();
update.announced_routes.clear();
}
update.path_attributes.retain(|attribute| {
let (family, prefixes) = match attribute {
PathAttribute::MpReachNlri(mp) => (mp.family, mp.prefixes.len()),
PathAttribute::MpUnreachNlri(mp) => (mp.family, mp.prefixes.len()),
_ => return true,
};
let keep = self.contains(family);
if !keep {
removed += prefixes;
}
keep
});
removed
}
}
impl FromIterator<AfiSafi> for FamilySet {
fn from_iter<I: IntoIterator<Item = AfiSafi>>(families: I) -> Self {
Self(families.into_iter().collect())
}
}
impl fmt::Display for FamilySet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_empty() {
return f.write_str("none");
}
for (i, family) in self.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
write!(f, "{family}")?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::net::{Ipv4Addr, Ipv6Addr};
use bgp_packet::message::{
MpReachNlriPathAttribute, MpUnreachNlriPathAttribute, NlriNextHop, OriginPathAttribute,
};
use bgp_packet::open::OpenOption;
use bgp_primitives::constants::{Afi, Safi};
use super::*;
fn open(capabilities: Vec<Capability>) -> OpenMessage {
OpenMessage {
version: 4,
asn: 65001,
hold_time: 90,
identifier: Ipv4Addr::new(10, 0, 0, 2),
options: if capabilities.is_empty() {
vec![]
} else {
vec![OpenOption::Capabilities(capabilities)]
},
}
}
fn prefix(s: &str) -> IpPrefix {
IpPrefix::try_from(s).unwrap()
}
fn only(family: AfiSafi) -> FamilySet {
[family].into_iter().collect()
}
#[test]
fn a_peer_without_multiprotocol_capabilities_speaks_ipv4_unicast() {
assert_eq!(
FamilySet::offered_by(&open(vec![])),
only(AfiSafi::IPV4_UNICAST)
);
// Other capabilities do not change that.
assert_eq!(
FamilySet::offered_by(&open(vec![Capability::RouteRefresh])),
only(AfiSafi::IPV4_UNICAST)
);
}
#[test]
fn a_peer_that_lists_families_gets_exactly_those() {
assert_eq!(
FamilySet::offered_by(&open(vec![Capability::multiprotocol(
AfiSafi::IPV6_UNICAST
)])),
only(AfiSafi::IPV6_UNICAST),
"IPv4 unicast is not implied once a peer lists its families"
);
// Capabilities may be spread over several optional parameters.
let mut spread = open(vec![Capability::multiprotocol(AfiSafi::IPV4_UNICAST)]);
spread
.options
.push(OpenOption::Capabilities(vec![Capability::multiprotocol(
AfiSafi::IPV6_UNICAST,
)]));
assert_eq!(FamilySet::offered_by(&spread), FamilySet::supported());
// A family we do not support is still what the peer offered. It
// drops out when intersected with what we offer.
let evpn = AfiSafi::new(Afi::L2VPN, Safi::EVPN);
let offered = FamilySet::offered_by(&open(vec![
Capability::multiprotocol(evpn),
Capability::multiprotocol(AfiSafi::IPV6_UNICAST),
]));
assert_eq!(offered.to_string(), "IPv6 unicast, L2VPN EVPN");
assert_eq!(
FamilySet::supported().intersection(&offered),
only(AfiSafi::IPV6_UNICAST)
);
assert!(
FamilySet::supported()
.intersection(&FamilySet::offered_by(&open(vec![
Capability::multiprotocol(evpn)
])))
.is_empty()
);
}
#[test]
fn sets_intersect_and_describe_themselves() {
let v6 = only(AfiSafi::IPV6_UNICAST);
assert_eq!(FamilySet::supported().intersection(&v6), v6);
assert!(only(AfiSafi::IPV4_UNICAST).intersection(&v6).is_empty());
assert_eq!(
FamilySet::supported().to_string(),
"IPv4 unicast, IPv6 unicast"
);
assert_eq!(FamilySet::default().to_string(), "none");
assert!(v6.carries(&prefix("2001:db8::/32")));
assert!(!v6.carries(&prefix("192.0.2.0/24")));
// One capability each: code 1, length 4, AFI, reserved, SAFI.
assert_eq!(
FamilySet::supported().capabilities_wire(),
[1, 4, 0, 1, 0, 1, 1, 4, 0, 2, 0, 1]
);
}
fn mixed_update() -> UpdateMessage {
UpdateMessage {
withdrawn_routes: vec![prefix("198.51.100.0/24").into()],
path_attributes: vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
family: AfiSafi::IPV6_UNICAST,
next_hop: NlriNextHop::Ipv6(Ipv6Addr::LOCALHOST),
prefixes: vec![
prefix("2001:db8:1::/48").into(),
prefix("2001:db8:2::/48").into(),
],
}),
PathAttribute::MpUnreachNlri(MpUnreachNlriPathAttribute {
family: AfiSafi::IPV6_UNICAST,
prefixes: vec![prefix("2001:db8:3::/48").into()],
}),
],
announced_routes: vec![prefix("192.0.2.0/24").into()],
}
}
#[test]
fn restrict_removes_the_routes_of_families_not_in_the_set() {
let mut update = mixed_update();
assert_eq!(FamilySet::supported().restrict(&mut update), 0);
assert_eq!(update, mixed_update());
let mut update = mixed_update();
assert_eq!(only(AfiSafi::IPV4_UNICAST).restrict(&mut update), 3);
assert_eq!(update.announced_routes, vec![prefix("192.0.2.0/24")]);
assert_eq!(update.withdrawn_routes, vec![prefix("198.51.100.0/24")]);
assert_eq!(
update.path_attributes,
vec![PathAttribute::Origin(OriginPathAttribute::IGP)]
);
let mut update = mixed_update();
assert_eq!(only(AfiSafi::IPV6_UNICAST).restrict(&mut update), 2);
assert!(update.announced_routes.is_empty());
assert!(update.withdrawn_routes.is_empty());
assert_eq!(update.path_attributes.len(), 3);
}
}
+989 -344
View File
File diff suppressed because it is too large. Load diff
+10
View File
@@ -1,7 +1,17 @@
pub mod advertise;
pub mod collision;
pub mod config;
pub mod connection;
pub mod dialer;
pub mod error;
pub mod family;
pub mod fsm;
pub mod metrics;
mod panic;
mod routes;
pub mod server;
pub mod session;
mod sync;
pub mod tracing;
pub mod proto {
-29
View File
@@ -1,29 +0,0 @@
use bgp_server::config::Config;
use bgp_server::proto::config::BgpConfig;
use bgp_server::server::BgpServerBuilder;
use bgp_server::tracing::TracingArgs;
use clap::Parser;
use eyre::{Context, Result};
#[derive(Parser, Debug)]
struct ServerArgs {
/// Path to the textproto configuration file.
config: String,
#[command(flatten)]
tracing: TracingArgs,
}
#[tokio::main]
async fn main() -> Result<()> {
let args = ServerArgs::parse();
args.tracing.install()?;
let config_string =
std::fs::read_to_string(&args.config).wrap_err("Failed to open config file")?;
let proto_config = protobuf::text_format::parse_from_str::<BgpConfig>(&config_string)
.wrap_err("Failed to parse config file")?;
let config = Config::from_proto(proto_config).wrap_err("Invalid configuration")?;
BgpServerBuilder::new(config).run().await
}
+133
View File
@@ -0,0 +1,133 @@
//! Process-wide Prometheus metrics, registered on the default registry.
use std::sync::LazyLock;
use bgp_packet::message::Message;
use prometheus::{
IntCounter, IntCounterVec, IntGauge, register_int_counter, register_int_counter_vec,
register_int_gauge,
};
pub static MESSAGES_SENT: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!(
"bgp_messages_sent_total",
"BGP messages sent, by type",
&["type"]
)
.expect("metric registration")
});
pub static MESSAGES_RECEIVED: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!(
"bgp_messages_received_total",
"BGP messages received, by type",
&["type"]
)
.expect("metric registration")
});
pub static UPDATE_ERRORS: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!(
"bgp_update_errors_total",
"Errors in received UPDATE messages that did not reset the session, by how they were handled (RFC 7606)",
&["handling"]
)
.expect("metric registration")
});
pub static SESSIONS_ESTABLISHED_TOTAL: LazyLock<IntCounter> = LazyLock::new(|| {
register_int_counter!(
"bgp_sessions_established_total",
"Sessions that reached Established"
)
.expect("metric registration")
});
pub static SESSIONS_ESTABLISHED: LazyLock<IntGauge> = LazyLock::new(|| {
register_int_gauge!(
"bgp_sessions_established",
"Sessions currently in Established"
)
.expect("metric registration")
});
pub static CONNECTIONS_REFUSED: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!(
"bgp_connections_refused_total",
"Incoming connections closed without a session, by why",
&["reason"]
)
.expect("metric registration")
});
pub static COLLISIONS: LazyLock<IntCounterVec> = LazyLock::new(|| {
register_int_counter_vec!(
"bgp_connection_collisions_total",
"Connection collisions resolved, by which connection was closed",
&["closed"]
)
.expect("metric registration")
});
pub static LOCAL_ROUTES: LazyLock<IntGauge> = LazyLock::new(|| {
register_int_gauge!("bgp_local_routes", "Locally originated routes")
.expect("metric registration")
});
pub static RIB_ROUTES: LazyLock<IntGauge> = LazyLock::new(|| {
register_int_gauge!(
"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")
});
/// Forces registration of every metric so they appear in scrapes before
/// their first observation.
pub fn init() {
LazyLock::force(&MESSAGES_SENT);
LazyLock::force(&MESSAGES_RECEIVED);
LazyLock::force(&UPDATE_ERRORS);
LazyLock::force(&SESSIONS_ESTABLISHED_TOTAL);
LazyLock::force(&SESSIONS_ESTABLISHED);
LazyLock::force(&COLLISIONS);
LazyLock::force(&LOCAL_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 {
match msg {
Message::Open(_) => "open",
Message::Update(_) => "update",
Message::Notification(_) => "notification",
Message::KeepAlive => "keepalive",
Message::RouteRefresh(_) => "route_refresh",
}
}
+60
View File
@@ -0,0 +1,60 @@
//! Containing panics.
//!
//! A panic is a bug. It unwinds the task it happens in, and what that task
//! was responsible for still has to be finished properly: a session tells
//! its peer it is closing and gives up its routes; the server, if it lost
//! something it cannot do without, closes every session and exits.
use std::any::Any;
use std::panic::AssertUnwindSafe;
use futures::FutureExt;
/// Runs `future` to completion. If it panics, the panic is caught and its
/// message returned, so that the caller can clean up after it.
///
/// The caller must not go on using what the future was working on as if
/// nothing had happened: a panic can leave it half changed. Closing it down
/// is what this is for.
pub(crate) async fn contained<F: Future>(future: F) -> Result<F::Output, String> {
AssertUnwindSafe(future)
.catch_unwind()
.await
.map_err(|payload| message(payload.as_ref()))
}
/// The text of a panic, from what `catch_unwind` or a `JoinError` carries.
pub(crate) fn message(payload: &(dyn Any + Send)) -> String {
if let Some(text) = payload.downcast_ref::<&'static str>() {
(*text).to_string()
} else if let Some(text) = payload.downcast_ref::<String>() {
text.clone()
} else {
"a panic that carried no message".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn a_panic_is_caught_with_its_message() {
assert_eq!(contained(async { 7 }).await, Ok(7));
let literal: Result<(), _> = contained(async { panic!("the table is gone") }).await;
assert_eq!(literal, Err("the table is gone".to_string()));
let formatted: Result<(), _> =
contained(async { panic!("index {} out of range", 9) }).await;
assert_eq!(formatted, Err("index 9 out of range".to_string()));
// After a point where the future had been suspended, too.
let later: Result<(), _> = contained(async {
tokio::task::yield_now().await;
panic!("later")
})
.await;
assert_eq!(later, Err("later".to_string()));
}
}
+64
View File
@@ -0,0 +1,64 @@
//! 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::Nlri;
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). Where the peer sends
/// several routes for a prefix (ADD-PATH, RFC 7911), each is named by its
/// prefix and path identifier together, and is a route of its own.
///
/// `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 = |nlri: &[Nlri], next_hop| -> Result<(), EncodeError> {
if !nlri.is_empty() {
outcome += table.announce(peer, &attributes(next_hop)?, nlri);
}
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)
}
+390 -48
View File
@@ -1,70 +1,123 @@
//! The server: listeners, the tasks that run for as long as it does, and
//! what happens when one of them stops.
use std::collections::HashMap;
use std::io;
use std::net::SocketAddr;
use std::sync::Arc;
use std::net::{IpAddr, SocketAddr};
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use eyre::{Result, WrapErr};
use futures::stream::{FuturesUnordered, StreamExt};
use tokio::net::{TcpListener, TcpStream};
use tracing::{error, info};
use tokio::net::{TcpListener, TcpSocket, TcpStream};
use tokio::task::{JoinError, JoinHandle};
use tracing::{error, info, warn};
use bgp_rib::AdjRibIn;
use bgp_rib::table::RibTable;
use bgp_rib::{LocalRib, RibStopped};
use crate::advertise::spawn_advertiser;
use crate::collision::{CollisionTable, Direction};
use crate::config::Config;
use crate::connection::handle_connection;
use crate::fsm::SessionConfig;
use crate::connection::{PeerSettings, ServerContext, run_session};
use crate::dialer::spawn_dialers;
use crate::error::ServerError;
use crate::metrics::CONNECTIONS_REFUSED;
use crate::panic;
use crate::session::{ConfiguredPeer, SessionRegistry};
use crate::sync::IgnorePoison;
pub struct BgpServerBuilder {
config: Config,
}
/// How long sessions get to send their Cease when the server stops.
const SHUTDOWN_GRACE: Duration = Duration::from_secs(2);
/// A `BgpServerBuilder` that has bound all its listeners and is ready to accept.
/// Call [`BgpServer::run`] to start the accept loop.
/// How long to wait before accepting again after an accept failed. What
/// makes it fail (no file descriptors left, usually) does not pass at once,
/// and trying again in a loop would only fill the log.
const ACCEPT_RETRY: Duration = Duration::from_millis(200);
/// How often attribute sets that no route uses any more are dropped. They
/// are not dropped as their last route goes (see `AttrStore::sweep`), and a
/// table whose routes keep changing attributes collects them.
const SWEEP_INTERVAL: Duration = Duration::from_secs(60);
/// How many connections may wait to be accepted on a listener.
const LISTEN_BACKLOG: u32 = 1024;
/// How many connections one address may have open to us at a time. A peer
/// needs one, a second while a collision is resolved, and now and then one
/// more while an old one is being closed. Each connection costs a task and
/// a socket for up to four minutes before it has to show an OPEN, so
/// without a limit a single host could use up either.
const MAX_CONNECTIONS_PER_ADDRESS: usize = 4;
/// At most one line in the log about refused connections in this long: a
/// host that keeps trying must not be able to fill the log.
const REFUSAL_LOG_INTERVAL: Duration = Duration::from_secs(10);
/// A BGP speaker that has bound its listeners and is ready to accept.
/// Call [`BgpServer::run`] to start it.
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.
pub registry: SessionRegistry,
}
impl BgpServerBuilder {
pub fn new(config: Config) -> Self {
Self { config }
}
/// Binds all configured listen endpoints and returns a [`BgpServer`].
/// Separating bind from run lets callers (e.g. tests) inspect the actual
/// bound addresses before the accept loop starts.
pub async fn bind(self) -> Result<BgpServer> {
let mut listeners = Vec::with_capacity(self.config.listen_endpoints.len());
for &addr in &self.config.listen_endpoints {
let listener = TcpListener::bind(addr)
.await
.wrap_err_with(|| format!("Failed to bind listener on {addr}"))?;
impl BgpServer {
/// Binds all configured listen endpoints. Separate from
/// [`BgpServer::run`] so that callers (tests, say) can learn the
/// addresses actually bound before any connection is accepted.
pub async fn bind(config: Config) -> Result<Self, ServerError> {
let mut listeners = Vec::with_capacity(config.listen_endpoints.len());
for &addr in &config.listen_endpoints {
let listener = listen(addr).map_err(|source| ServerError::Bind { addr, source })?;
let bound_addr = listener.local_addr()?;
info!(%bound_addr, "Listening for BGP connections");
listeners.push((bound_addr, listener));
}
// Routes from the configuration go in before any session exists;
// each session is sent the whole table when it comes up.
let local_rib = LocalRib::spawn();
for route in &config.originate {
local_rib.announce(route.clone()).await?;
}
let configured = config
.peers
.iter()
.map(|p| ConfiguredPeer {
name: p.name.clone(),
endpoint: p.endpoint,
remote_asn: p.remote_asn,
})
.collect();
Ok(BgpServer {
config: self.config,
listeners,
adj_rib_in: AdjRibIn::spawn(),
rib: Arc::new(RibTable::new(table_shards())),
local_rib,
registry: SessionRegistry::with_configured(configured),
config,
})
}
/// Convenience method: bind then run immediately.
pub async fn run(self) -> Result<()> {
self.bind().await?.run().await
pub fn config(&self) -> &Config {
&self.config
}
}
impl BgpServer {
/// The addresses actually bound (useful when the config uses port 0).
pub fn local_addrs(&self) -> Vec<SocketAddr> {
self.listeners.iter().map(|(addr, _)| *addr).collect()
}
pub async fn run(self) -> Result<()> {
/// Runs the server. Returns only if something it cannot do without has
/// failed, after closing its sessions.
pub async fn run(self) -> Result<(), ServerError> {
crate::metrics::init();
info!(
router_id = %self.config.router_id,
local_asn = self.config.local_asn,
@@ -72,36 +125,268 @@ impl BgpServer {
"BGP server started"
);
let session_config = Arc::new(SessionConfig {
let context = Arc::new(ServerContext {
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),
});
// The tasks the server cannot do without. None of them ends while
// the server runs. The advertiser comes first: it has to be
// listening before a dialer brings up the first session.
let mut tasks: FuturesUnordered<NamedTask> = FuturesUnordered::new();
tasks.push(named(
"advertiser",
spawn_advertiser(
self.local_rib.clone(),
self.registry.clone(),
self.config.local_asn,
),
));
tasks.push(named("sweeper", spawn_sweeper(Arc::clone(&self.rib))));
for dialer in spawn_dialers(&self.config.peers, &context, self.config.connect_retry) {
tasks.push(named("dialer", dialer));
}
let connections = ConnectionCounts::default();
let mut refusals = Refusals::default();
let mut accepting: FuturesUnordered<_> = self
.listeners
.into_iter()
.map(|(addr, listener)| accept_one(addr, listener))
.collect();
while let Some((listen_addr, result, listener)) = accepting.next().await {
loop {
let (listen_addr, result, listener) = tokio::select! {
Some(accepted) = accepting.next() => accepted,
failure = failed(&self.local_rib, &mut tasks) => {
error!(err = %failure, "Shutting down: the server cannot continue");
close_sessions(&self.registry).await;
return Err(failure);
}
};
match result {
Ok((stream, peer_addr)) => {
tokio::spawn(handle_connection(
stream,
peer_addr,
Arc::clone(&session_config),
));
match admit(&self.config, peer_addr.ip(), &connections) {
Ok((peer, slot)) => {
let context = Arc::clone(&context);
tokio::spawn(serve(stream, peer_addr, context, peer, slot));
}
// Closed by being dropped, with nothing read and
// nothing said: a host we do not know learns no
// more than that the port is open.
Err(refusal) => refusals.note(peer_addr, refusal),
}
}
Err(e) => {
error!(%listen_addr, err = %e, "Accept error");
tokio::time::sleep(ACCEPT_RETRY).await;
}
}
accepting.push(accept_one(listen_addr, listener));
}
}
}
Ok(())
/// Closes every session politely and waits a moment for them to go: each
/// peer is told we are going (Cease / Administrative Shutdown), so that it
/// stops using our routes now and not when its hold timer expires.
pub async fn close_sessions(registry: &SessionRegistry) {
let left = registry.shutdown(SHUTDOWN_GRACE).await;
if left > 0 {
warn!(sessions = left, "Some sessions did not close in time");
}
}
/// Opens a listening socket on `addr`.
fn listen(addr: SocketAddr) -> io::Result<TcpListener> {
let socket = match addr {
SocketAddr::V4(_) => TcpSocket::new_v4()?,
SocketAddr::V6(_) => {
let socket = TcpSocket::new_v6()?;
// IPv6 only. Where a system's default is to take IPv4
// connections on such a socket too, listening on both 0.0.0.0
// and [::] would fail on the second, and IPv4 peers would come
// in under IPv4-mapped addresses that match no configured peer.
socket2::SockRef::from(&socket).set_only_v6(true)?;
socket
}
};
socket.set_reuseaddr(true)?;
socket.bind(addr)?;
socket.listen(LISTEN_BACKLOG)
}
/// Decides whether to take a connection from `address`, and if so with what
/// settings.
fn admit(
config: &Config,
address: IpAddr,
connections: &ConnectionCounts,
) -> Result<(PeerSettings, ConnectionSlot), Refusal> {
// A connection from a configured peer is run with what is configured
// for that peer.
let peer = match config.peer_at(address) {
Some(peer) => PeerSettings::from(peer),
None if config.accept_unconfigured_peers => {
PeerSettings::unconfigured(config.hold_time_secs)
}
None => return Err(Refusal::NotAPeer),
};
let slot = connections.take(address).ok_or(Refusal::TooMany)?;
Ok((peer, slot))
}
/// Runs a session on a connection a peer opened, and logs how it ended.
/// `_slot` is the connection's place among those its address may have.
async fn serve(
stream: TcpStream,
peer_addr: SocketAddr,
context: Arc<ServerContext>,
peer: PeerSettings,
_slot: ConnectionSlot,
) {
let end = run_session(stream, peer_addr, Direction::Inbound, context, peer).await;
if let Err(e) = end.result {
warn!(%peer_addr, err = %e, "BGP session terminated with error");
}
}
/// Why an incoming connection was closed without a session.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Refusal {
/// It is not from the address of a configured peer.
NotAPeer,
/// Its address has as many connections open as one may.
TooMany,
}
impl Refusal {
fn as_str(self) -> &'static str {
match self {
Self::NotAPeer => "not a configured peer",
Self::TooMany => "too many connections from the address",
}
}
}
/// Counts refused connections, and says so in the log now and then.
#[derive(Default)]
struct Refusals {
logged: Option<Instant>,
/// How many were refused since the log last said so.
unlogged: u64,
}
impl Refusals {
fn note(&mut self, peer_addr: SocketAddr, refusal: Refusal) {
CONNECTIONS_REFUSED
.with_label_values(&[refusal.as_str()])
.inc();
if self
.logged
.is_some_and(|logged| logged.elapsed() < REFUSAL_LOG_INTERVAL)
{
self.unlogged += 1;
return;
}
warn!(
%peer_addr,
reason = refusal.as_str(),
others_since_last_report = self.unlogged,
"Refused a connection"
);
self.logged = Some(Instant::now());
self.unlogged = 0;
}
}
/// How many connections each address has open to us.
#[derive(Clone, Default)]
struct ConnectionCounts(Arc<Mutex<HashMap<IpAddr, usize>>>);
impl ConnectionCounts {
/// A place for one more connection from `address`, unless it has as
/// many as it may. The place is given back when the slot is dropped.
fn take(&self, address: IpAddr) -> Option<ConnectionSlot> {
let mut counts = self.0.locked();
let count = counts.entry(address).or_insert(0);
if *count >= MAX_CONNECTIONS_PER_ADDRESS {
return None;
}
*count += 1;
Some(ConnectionSlot {
counts: self.clone(),
address,
})
}
}
struct ConnectionSlot {
counts: ConnectionCounts,
address: IpAddr,
}
impl Drop for ConnectionSlot {
fn drop(&mut self) {
let mut counts = self.counts.0.locked();
if let Some(count) = counts.get_mut(&self.address) {
*count -= 1;
if *count == 0 {
counts.remove(&self.address);
}
}
}
}
/// Drops, every so often, the attribute sets no route uses any more.
fn spawn_sweeper(rib: Arc<RibTable>) -> JoinHandle<()> {
tokio::spawn(async move {
let mut tick = tokio::time::interval(SWEEP_INTERVAL);
loop {
tick.tick().await;
let rib = Arc::clone(&rib);
// Takes each of the store's locks in turn and looks at every
// set: not for a thread that sessions run on.
let _ = tokio::task::spawn_blocking(move || rib.attrs().sweep()).await;
}
})
}
/// 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())
}
type NamedTask = Pin<Box<dyn Future<Output = (&'static str, Result<(), JoinError>)> + Send>>;
fn named(name: &'static str, task: JoinHandle<()>) -> NamedTask {
Box::pin(async move { (name, task.await) })
}
/// 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(local_rib: &LocalRib, tasks: &mut FuturesUnordered<NamedTask>) -> ServerError {
tokio::select! {
() = local_rib.stopped() => RibStopped { rib: "local RIB" }.into(),
Some((task, result)) = tasks.next() => ServerError::TaskStopped {
task,
reason: match result {
Ok(()) => "it returned".into(),
Err(e) if e.is_panic() => {
format!("it panicked: {}", panic::message(e.into_panic().as_ref()))
}
Err(e) => e.to_string(),
},
},
}
}
@@ -112,3 +397,60 @@ async fn accept_one(
let result = listener.accept().await;
(addr, result, listener)
}
#[cfg(test)]
mod tests {
use super::*;
/// The advertiser and the dialers run for as long as the server does.
/// 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 local_rib = LocalRib::spawn();
let mut tasks: FuturesUnordered<NamedTask> = FuturesUnordered::new();
tasks.push(named("dialer", tokio::spawn(std::future::pending())));
tasks.push(named(
"advertiser",
tokio::spawn(async { panic!("no route to announce") }),
));
let failure = failed(&local_rib, &mut tasks).await;
assert_eq!(
failure.to_string(),
"the advertiser task stopped unexpectedly: it panicked: no route to announce"
);
}
#[tokio::test]
async fn a_stopped_rib_is_a_failure_of_the_server() {
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(&local_rib, &mut tasks).await;
assert!(
matches!(failure, ServerError::RibStopped(RibStopped { rib }) if rib == "local RIB")
);
}
/// One address may hold only so many connections, and a connection
/// that ends makes room for another.
#[test]
fn an_address_may_have_only_so_many_connections() {
let counts = ConnectionCounts::default();
let address: IpAddr = "192.0.2.1".parse().unwrap();
let other: IpAddr = "192.0.2.2".parse().unwrap();
let mut slots: Vec<_> = (0..MAX_CONNECTIONS_PER_ADDRESS)
.map(|_| counts.take(address).expect("room"))
.collect();
assert!(counts.take(address).is_none());
assert!(counts.take(other).is_some(), "each address has its own");
slots.pop();
assert!(counts.take(address).is_some());
drop(slots);
assert!(counts.0.locked().is_empty(), "nothing is kept for nobody");
}
}
+294
View File
@@ -0,0 +1,294 @@
//! Registry of established BGP sessions.
//!
//! A session registers itself when it reaches Established and is removed when
//! its [`SessionGuard`] drops, so a session that errors out is always
//! deregistered. Other parts of the server (the advertiser, the HTTP API) use
//! the registry to enumerate live sessions and queue outbound messages.
use std::collections::HashMap;
use std::net::{IpAddr, SocketAddr};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, RwLock};
use std::time::{Duration, SystemTime};
use bgp_packet::errors::{BgpError, CeaseSubcode};
use bgp_packet::message::NotificationMessage;
use bgp_packet::message::Message;
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;
const EVENT_CHANNEL_CAPACITY: usize = 256;
/// Identifies a session by the peer's TCP address and its AS number.
#[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)
}
}
/// Where the dialer is in bringing up a configured peer.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DialState {
/// Not dialling: the peer connected to us, or the dialer has not started.
Idle,
Connecting,
/// The last attempt failed or the session ended; waiting to retry.
Backoff {
until: SystemTime,
/// How the last attempt or session ended.
last_error: String,
},
}
/// A peer from the configuration file, whether or not it is currently up.
#[derive(Debug, Clone)]
pub struct ConfiguredPeer {
pub name: String,
pub endpoint: SocketAddr,
pub remote_asn: u32,
}
/// Handle to one established session. Cloning is cheap.
#[derive(Debug, Clone)]
pub struct SessionHandle {
pub peer: PeerId,
/// Name from the configuration, if this peer is configured.
pub name: Option<String>,
/// Which side opened the TCP connection.
pub direction: Direction,
/// Our side of the TCP connection; used as the next hop for routes we
/// originate to this peer.
pub local_addr: SocketAddr,
pub established_at: SystemTime,
/// Whether the peer is in our own AS.
pub ibgp: bool,
/// Whether the peer negotiated RFC 6793 four-octet ASNs. Determines how
/// AS_PATH is encoded on the wire for this session.
pub four_octet_asn: bool,
/// 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 families for which the peer sends us several routes for a
/// prefix (ADD-PATH, RFC 7911), and those for which we send it path
/// identifiers.
pub add_path_receive: FamilySet,
pub add_path_send: FamilySet,
/// The peer's number in the routing table: what its routes there are
/// marked with.
pub rib_peer: PeerIndex,
outbound: mpsc::Sender<Message>,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("session with {0} has closed")]
pub struct SessionClosed(pub PeerId);
impl SessionHandle {
pub(crate) fn new(
peer: PeerId,
name: Option<String>,
direction: Direction,
local_addr: SocketAddr,
negotiated: &Negotiated,
rib_peer: PeerIndex,
outbound: mpsc::Sender<Message>,
) -> Self {
Self {
peer,
name,
direction,
local_addr,
established_at: SystemTime::now(),
ibgp: negotiated.ibgp,
four_octet_asn: negotiated.four_octet_asn,
families: negotiated.families.clone(),
add_path_receive: negotiated.add_path_receive.clone(),
add_path_send: negotiated.add_path_send.clone(),
rib_peer,
outbound,
}
}
/// Whether `other` is a handle to this same session, and not to another
/// session with the same peer.
pub fn is_same_session(&self, other: &SessionHandle) -> bool {
self.outbound.same_channel(&other.outbound)
}
/// Queues a message for transmission on this session. Waits for queue
/// space if the session's writer is behind.
pub async fn send(&self, msg: Message) -> Result<(), SessionClosed> {
self.outbound
.send(msg)
.await
.map_err(|_| SessionClosed(self.peer))
}
}
#[derive(Debug, Clone)]
pub enum SessionEvent {
Up(SessionHandle),
Down(PeerId),
/// The peer sent ROUTE-REFRESH (RFC 2918): it wants everything we
/// advertise to it sent again, typically because its import policy
/// changed.
RefreshRequested(SessionHandle),
}
#[derive(Clone)]
pub struct SessionRegistry {
inner: Arc<RwLock<HashMap<PeerId, SessionHandle>>>,
configured: Arc<Vec<ConfiguredPeer>>,
dial_states: Arc<RwLock<HashMap<String, DialState>>>,
shutting_down: Arc<AtomicBool>,
events: broadcast::Sender<SessionEvent>,
}
impl Default for SessionRegistry {
fn default() -> Self {
Self::new()
}
}
impl SessionRegistry {
pub fn new() -> Self {
Self::with_configured(vec![])
}
pub fn with_configured(configured: Vec<ConfiguredPeer>) -> Self {
let (events, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
let dial_states = configured
.iter()
.map(|p| (p.name.clone(), DialState::Idle))
.collect();
Self {
inner: Arc::new(RwLock::new(HashMap::new())),
configured: Arc::new(configured),
dial_states: Arc::new(RwLock::new(dial_states)),
shutting_down: Arc::new(AtomicBool::new(false)),
events,
}
}
/// Peers from the configuration file, each with the dialer's current
/// state for it.
pub fn configured_peers(&self) -> Vec<(ConfiguredPeer, DialState)> {
let states = self.dial_states.reading();
self.configured
.iter()
.map(|p| {
(
p.clone(),
states.get(&p.name).cloned().unwrap_or(DialState::Idle),
)
})
.collect()
}
pub(crate) fn set_dial_state(&self, name: &str, state: DialState) {
self.dial_states.writing().insert(name.to_string(), state);
}
/// An established session with this remote IP and ASN, regardless of
/// which side opened the TCP connection.
pub fn find(&self, ip: IpAddr, asn: u32) -> Option<SessionHandle> {
self.inner
.reading()
.values()
.find(|h| h.peer.addr.ip() == ip && h.peer.asn == asn)
.cloned()
}
/// Adds a session and emits [`SessionEvent::Up`]. The returned guard
/// removes the session again when dropped.
pub(crate) fn register(&self, handle: SessionHandle) -> SessionGuard {
let peer = handle.peer;
self.inner.writing().insert(peer, handle.clone());
SESSIONS_ESTABLISHED.inc();
let _ = self.events.send(SessionEvent::Up(handle));
SessionGuard {
registry: self.clone(),
peer,
}
}
/// All currently established sessions, in no particular order.
pub fn list(&self) -> Vec<SessionHandle> {
self.inner.reading().values().cloned().collect()
}
/// Called by a session when its peer asks for a route refresh.
pub(crate) fn request_refresh(&self, peer: &PeerId) {
let handle = self.inner.reading().get(peer).cloned();
if let Some(handle) = handle {
let _ = self.events.send(SessionEvent::RefreshRequested(handle));
}
}
/// True once [`shutdown`](Self::shutdown) has begun; dialers stop
/// reconnecting.
pub fn is_shutting_down(&self) -> bool {
self.shutting_down.load(Ordering::Relaxed)
}
/// Closes every session politely: each peer is sent Cease /
/// Administrative Shutdown (RFC 4486) so it can drop the session at once
/// instead of discovering a dead TCP connection. Waits up to `grace` for
/// the sessions to finish; returns how many were still open.
pub async fn shutdown(&self, grace: Duration) -> usize {
self.shutting_down.store(true, Ordering::Relaxed);
for session in self.list() {
// Not waited for: a session whose queue is full is one whose
// peer is not reading, and it would hold up the goodbye to all
// the others. It ends when the process does.
let _ = session
.outbound
.try_send(Message::Notification(NotificationMessage {
error: BgpError::Cease(CeaseSubcode::AdminShutdown),
data: vec![],
}));
}
let deadline = tokio::time::Instant::now() + grace;
while !self.list().is_empty() && tokio::time::Instant::now() < deadline {
tokio::time::sleep(Duration::from_millis(20)).await;
}
self.list().len()
}
pub fn subscribe(&self) -> broadcast::Receiver<SessionEvent> {
self.events.subscribe()
}
fn remove(&self, peer: &PeerId) {
let removed = self.inner.writing().remove(peer);
if removed.is_some() {
SESSIONS_ESTABLISHED.dec();
let _ = self.events.send(SessionEvent::Down(*peer));
}
}
}
/// Removes the session from its registry on drop.
pub struct SessionGuard {
registry: SessionRegistry,
peer: PeerId,
}
impl Drop for SessionGuard {
fn drop(&mut self) {
self.registry.remove(&self.peer);
}
}
+66
View File
@@ -0,0 +1,66 @@
//! Locks that survive a panic.
//!
//! A standard lock is poisoned when a thread panics while holding it, and
//! every later `lock().unwrap()` then panics too. For the tables shared by
//! all sessions that would turn a bug in one session into the end of every
//! session, one panic at a time.
//!
//! The tables here are maps that are changed by single inserts and removes,
//! so a panic part-way through an update cannot leave one in a state that
//! is unsafe to read. The poison is therefore ignored: a session that
//! panics is cleaned up by its guards like one that ended any other way.
use std::sync::{Mutex, MutexGuard, PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard};
pub(crate) trait IgnorePoison<T> {
/// Locks, whether or not an earlier holder panicked.
fn locked(&self) -> MutexGuard<'_, T>;
}
impl<T> IgnorePoison<T> for Mutex<T> {
fn locked(&self) -> MutexGuard<'_, T> {
self.lock().unwrap_or_else(PoisonError::into_inner)
}
}
pub(crate) trait IgnorePoisonRw<T> {
/// Locks for reading, whether or not an earlier holder panicked.
fn reading(&self) -> RwLockReadGuard<'_, T>;
/// Locks for writing, whether or not an earlier holder panicked.
fn writing(&self) -> RwLockWriteGuard<'_, T>;
}
impl<T> IgnorePoisonRw<T> for RwLock<T> {
fn reading(&self) -> RwLockReadGuard<'_, T> {
self.read().unwrap_or_else(PoisonError::into_inner)
}
fn writing(&self) -> RwLockWriteGuard<'_, T> {
self.write().unwrap_or_else(PoisonError::into_inner)
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
#[test]
fn a_lock_whose_holder_panicked_can_still_be_taken() {
let mutex = Arc::new(Mutex::new(1));
let rwlock = Arc::new(RwLock::new(1));
let (m, r) = (mutex.clone(), rwlock.clone());
let panicked = std::thread::spawn(move || {
let _m = m.lock().unwrap();
let _r = r.write().unwrap();
panic!("while holding both");
})
.join();
assert!(panicked.is_err());
assert!(mutex.is_poisoned() && rwlock.is_poisoned());
*mutex.locked() += 1;
*rwlock.writing() += 1;
assert_eq!((*mutex.locked(), *rwlock.reading()), (2, 2));
}
}
+7 -9
View File
@@ -1,26 +1,24 @@
use clap::Args;
use eyre::Result;
use tracing::Level;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::filter::ParseError;
/// Default targets that we enable logging for.
const DEFAULT_TARGETS: [&str; 1] = ["bgp_server"];
const DEFAULT_TARGETS: [&str; 3] = ["bgp", "bgp_server", "bgp_web"];
#[derive(Debug, Args)]
pub struct TracingArgs {
/// Set the minimum log level.
#[arg(short, long, global = true, default_value_t = Level::TRACE, help_heading = "Tracing")]
/// Set the minimum log level. At `debug` and below there is a line for
/// every UPDATE and every route announced.
#[arg(short, long, global = true, default_value_t = Level::INFO, help_heading = "Tracing")]
level: Level,
/// Disable logging to stdout.
#[arg(long, global = true, default_value_t = false, help_heading = "Tracing")]
quiet: bool,
}
impl TracingArgs {
pub fn install(&self) -> Result<()> {
pub fn install(&self) -> Result<(), ParseError> {
let filter = DEFAULT_TARGETS.into_iter().try_fold(
EnvFilter::builder().from_env_lossy(),
|filter, target| -> Result<_> {
|filter, target| -> Result<_, ParseError> {
Ok(filter.add_directive(format!("{}={}", target, self.level).parse()?))
},
)?;
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "bgp-web"
description = "HTTP API, event stream and metrics for the BGP daemon"
authors.workspace = true
edition.workspace = true
homepage.workspace = true
license.workspace = true
repository.workspace = true
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
prometheus.workspace = true
serde.workspace = true
tokio.workspace = true
tokio-stream.workspace = true
tracing.workspace = true
[lints]
workspace = true
+406
View File
@@ -0,0 +1,406 @@
//! JSON handlers and their view types. Everything here is read-only: the
//! daemon is configured through its configuration file.
use std::time::{SystemTime, UNIX_EPOCH};
use axum::Json;
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::{NlriNextHop, OriginPathAttribute, PathAttribute};
use bgp_rib::table::{PeerIndex, Route};
use bgp_rib::{LocalRoute, RibStopped};
use bgp_server::session::{ConfiguredPeer, DialState, PeerId, SessionHandle, SessionRegistry};
use serde::{Deserialize, Serialize};
use crate::AppState;
// ---------------------------------------------------------------------------
// Views
// ---------------------------------------------------------------------------
fn unix_secs(t: SystemTime) -> u64 {
t.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
#[derive(Debug, Serialize)]
pub struct StatusView {
pub router_id: String,
pub local_asn: u32,
pub version: String,
pub hostname: String,
pub started_at_unix: u64,
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)]
pub struct PeerView {
/// Name from the configuration file; absent for unconfigured inbound peers.
pub name: Option<String>,
pub peer_addr: String,
pub peer_asn: u32,
/// `Established`, `Connecting`, `Backoff` or `Idle`.
pub state: String,
/// `inbound` or `outbound`: which side opened the live TCP connection.
pub direction: Option<String>,
pub local_addr: Option<String>,
pub established_at_unix: Option<u64>,
pub uptime_secs: Option<u64>,
pub four_octet_asn: Option<bool>,
/// The address families negotiated with the peer, e.g. `IPv4 unicast`.
pub address_families: Option<Vec<String>>,
/// The families for which the peer sends us several routes for a
/// prefix (ADD-PATH, RFC 7911), and those for which we send it path
/// identifiers.
pub add_path_receive: Option<Vec<String>>,
pub add_path_send: Option<Vec<String>>,
/// Why the last connection attempt or session ended, while in Backoff.
pub last_error: Option<String>,
pub retry_at_unix: Option<u64>,
}
impl From<&SessionHandle> for PeerView {
fn from(s: &SessionHandle) -> Self {
Self {
name: s.name.clone(),
peer_addr: s.peer.addr.to_string(),
peer_asn: s.peer.asn,
state: "Established".into(),
direction: Some(s.direction.as_str().into()),
local_addr: Some(s.local_addr.to_string()),
established_at_unix: Some(unix_secs(s.established_at)),
uptime_secs: Some(s.established_at.elapsed().map(|d| d.as_secs()).unwrap_or(0)),
four_octet_asn: Some(s.four_octet_asn),
address_families: Some(s.families.iter().map(|f| f.to_string()).collect()),
add_path_receive: Some(s.add_path_receive.iter().map(|f| f.to_string()).collect()),
add_path_send: Some(s.add_path_send.iter().map(|f| f.to_string()).collect()),
last_error: None,
retry_at_unix: None,
}
}
}
impl PeerView {
fn from_configured(peer: &ConfiguredPeer, state: &DialState) -> Self {
let (state_name, last_error, retry_at_unix) = match state {
DialState::Idle => ("Idle", None, None),
DialState::Connecting => ("Connecting", None, None),
DialState::Backoff { until, last_error } => {
("Backoff", Some(last_error.clone()), Some(unix_secs(*until)))
}
};
Self {
name: Some(peer.name.clone()),
peer_addr: peer.endpoint.to_string(),
peer_asn: peer.remote_asn,
state: state_name.into(),
direction: None,
local_addr: None,
established_at_unix: None,
uptime_secs: None,
four_octet_asn: None,
address_families: None,
add_path_receive: None,
add_path_send: None,
last_error,
retry_at_unix,
}
}
}
/// Configured peers first (with their live session when established), then
/// any unconfigured inbound sessions.
pub fn peer_views(registry: &SessionRegistry) -> Vec<PeerView> {
let sessions = registry.list();
let mut claimed = std::collections::HashSet::new();
let mut views: Vec<PeerView> = registry
.configured_peers()
.iter()
.map(|(peer, state)| {
match sessions
.iter()
.find(|s| s.peer.addr.ip() == peer.endpoint.ip() && s.peer.asn == peer.remote_asn)
{
Some(session) => {
claimed.insert(session.peer);
PeerView {
name: Some(peer.name.clone()),
..PeerView::from(session)
}
}
None => PeerView::from_configured(peer, state),
}
})
.collect();
let mut rest: Vec<PeerView> = sessions
.iter()
.filter(|s| !claimed.contains(&s.peer))
.map(PeerView::from)
.collect();
rest.sort_by(|a, b| a.peer_addr.cmp(&b.peer_addr));
views.extend(rest);
views
}
#[derive(Debug, Clone, Serialize)]
pub struct LocalRouteView {
pub prefix: String,
pub origin: String,
pub prepend: u8,
pub med: Option<u32>,
pub communities: Vec<String>,
pub large_communities: Vec<String>,
pub announced_at_unix: u64,
}
impl From<&LocalRoute> for LocalRouteView {
fn from(r: &LocalRoute) -> Self {
Self {
prefix: r.prefix.to_string(),
origin: origin_name(r.attrs.origin).to_string(),
prepend: r.attrs.prepend,
med: r.attrs.med,
communities: r
.attrs
.communities
.iter()
.copied()
.map(community::format_standard)
.collect(),
large_communities: r
.attrs
.large_communities
.iter()
.map(community::format_large)
.collect(),
announced_at_unix: unix_secs(r.announced_at),
}
}
}
/// Human-oriented digest of a path attribute list.
#[derive(Debug, Clone, Default, Serialize)]
pub struct AttrSummary {
pub origin: Option<String>,
/// AS_PATH flattened in order; AS_SETs are included in place.
pub as_path: Vec<u32>,
pub next_hop: Option<String>,
pub med: Option<u32>,
pub local_pref: Option<u32>,
pub communities: Vec<String>,
pub large_communities: Vec<String>,
}
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: next_hop.map(ToString::to_string),
..Default::default()
};
for attr in attributes {
match attr {
PathAttribute::Origin(o) => s.origin = Some(origin_name(*o).to_string()),
PathAttribute::AsPath(p) => {
s.as_path = p
.segments
.iter()
.flat_map(|seg| seg.path.iter().copied())
.collect()
}
PathAttribute::NextHop(nh) => s.next_hop = Some(nh.0.to_string()),
PathAttribute::MultiExitDisc(m) => s.med = Some(m.0),
PathAttribute::LocalPref(l) => s.local_pref = Some(l.0),
PathAttribute::Communities(c) => {
s.communities =
c.0.iter()
.copied()
.map(community::format_standard)
.collect()
}
PathAttribute::LargeCommunities(l) => {
s.large_communities = l.communities.iter().map(community::format_large).collect()
}
_ => {}
}
}
s
}
#[derive(Debug, Clone, Serialize)]
pub struct ReceivedRouteView {
pub peer_addr: String,
pub peer_asn: u32,
pub prefix: String,
/// The peer's identifier for the route: zero unless the peer sends
/// several routes for a prefix (ADD-PATH, RFC 7911).
pub path_id: u32,
/// 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>,
}
/// 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, route: &Route) -> Option<ReceivedRouteView> {
let peer = self.0.get(&route.peer)?;
// What a session stored was accepted by the decoder, and parses.
let (attributes, next_hop) = route.attrs.attributes().unwrap_or_default();
Some(ReceivedRouteView {
peer_addr: peer.addr.to_string(),
peer_asn: peer.asn,
prefix: route.prefix.to_string(),
path_id: route.path_id,
best: route.best,
summary: summarize(&attributes, next_hop.as_ref()),
attributes,
})
}
}
fn origin_name(o: OriginPathAttribute) -> &'static str {
match o {
OriginPathAttribute::IGP => "IGP",
OriginPathAttribute::EGP => "EGP",
OriginPathAttribute::INCOMPLETE => "INCOMPLETE",
}
}
// ---------------------------------------------------------------------------
// Handlers
// ---------------------------------------------------------------------------
/// A table could not be read because its actor has stopped. The daemon is
/// on its way down; saying so beats answering with an empty table.
pub struct Unavailable(RibStopped);
impl From<RibStopped> for Unavailable {
fn from(e: RibStopped) -> Self {
Self(e)
}
}
impl IntoResponse for Unavailable {
fn into_response(self) -> Response {
(StatusCode::SERVICE_UNAVAILABLE, self.0.to_string()).into_response()
}
}
pub async fn status(State(state): State<AppState>) -> Result<Json<StatusView>, Unavailable> {
Ok(Json(StatusView {
router_id: state.info.router_id.to_string(),
local_asn: state.info.local_asn,
version: state.info.version.clone(),
hostname: state.info.hostname.clone(),
started_at_unix: unix_secs(state.info.started_at),
uptime_secs: state.info.started.elapsed().as_secs(),
sessions: state.registry.list().len(),
local_routes: state.local_rib.len().await?,
received_routes: state.rib.len(),
received_prefixes: state.rib.prefix_count(),
}))
}
/// The configuration file the daemon was started with.
#[derive(Debug, Clone, Serialize)]
pub struct ConfigView {
pub path: Option<String>,
/// Raw file contents as read at startup; later edits to the file are not
/// reflected until the daemon restarts.
pub text: Option<String>,
}
pub async fn config(State(state): State<AppState>) -> Json<ConfigView> {
let (path, text) = state.info.config.clone().unzip();
Json(ConfigView { path, text })
}
pub async fn peers(State(state): State<AppState>) -> Json<Vec<PeerView>> {
Json(peer_views(&state.registry))
}
pub async fn local_routes(
State(state): State<AppState>,
) -> Result<Json<Vec<LocalRouteView>>, Unavailable> {
let mut routes: Vec<LocalRouteView> = state
.local_rib
.snapshot()
.await?
.iter()
.map(LocalRouteView::from)
.collect();
routes.sort_by(|a, b| a.prefix.cmp(&b.prefix));
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;
/// The most routes [`received_routes`] returns, whatever is asked for:
/// each is copied out of the table and written as JSON.
const MAX_ROUTE_LIMIT: usize = 100_000;
#[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>,
Query(query): Query<RouteQuery>,
) -> Result<Json<Vec<ReceivedRouteView>>, StatusCode> {
let limit = query
.limit
.unwrap_or(DEFAULT_ROUTE_LIMIT)
.min(MAX_ROUTE_LIMIT);
let names = PeerNames::of(&state.registry);
// Copying, parsing and sorting many routes takes long enough to keep
// off the threads that sessions run on.
let routes = tokio::task::spawn_blocking(move || {
let mut routes: Vec<ReceivedRouteView> = state
.rib
.some_routes(limit)
.iter()
.filter_map(|route| names.view(route))
.collect();
routes.sort_by(|a, b| (&a.prefix, &a.peer_addr).cmp(&(&b.prefix, &b.peer_addr)));
routes
})
.await
// Only if making the views panicked.
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(routes))
}
+198
View File
@@ -0,0 +1,198 @@
//! 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_primitives::ip_prefix::IpPrefix;
use bgp_rib::LocalRibEvent;
use bgp_rib::table::{self, Route};
use bgp_server::session::SessionEvent;
use futures::stream::{Stream, StreamExt};
use serde::Serialize;
use tokio::sync::mpsc;
use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
use tokio_stream::wrappers::{BroadcastStream, ReceiverStream};
use crate::AppState;
use crate::api::{LocalRouteView, PeerNames, PeerView, ReceivedRouteView};
#[derive(Debug, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum WireEvent {
SessionUp {
peer: PeerView,
},
SessionDown {
peer_addr: String,
peer_asn: u32,
},
/// The peer asked us to send all our routes again (ROUTE-REFRESH).
RouteRefreshRequested {
peer: PeerView,
},
LocalAnnounced {
route: LocalRouteView,
},
LocalWithdrawn {
prefix: String,
},
/// The best route for a prefix is now this one.
BestChanged {
route: ReceivedRouteView,
},
/// A prefix has no route any more.
BestWithdrawn {
prefix: String,
},
/// 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,
},
}
impl WireEvent {
fn from_session(ev: Result<SessionEvent, BroadcastStreamRecvError>) -> Self {
match ev {
Ok(SessionEvent::Up(h)) => Self::SessionUp {
peer: PeerView::from(&h),
},
Ok(SessionEvent::Down(p)) => Self::SessionDown {
peer_addr: p.addr.to_string(),
peer_asn: p.asn,
},
Ok(SessionEvent::RefreshRequested(h)) => Self::RouteRefreshRequested {
peer: PeerView::from(&h),
},
Err(BroadcastStreamRecvError::Lagged(n)) => Self::Lagged {
stream: "sessions",
missed: n,
},
}
}
fn from_local(ev: Result<LocalRibEvent, BroadcastStreamRecvError>) -> Self {
match ev {
Ok(LocalRibEvent::Announced(r)) => Self::LocalAnnounced {
route: LocalRouteView::from(&r),
},
Ok(LocalRibEvent::Withdrawn { prefix }) => Self::LocalWithdrawn {
prefix: prefix.to_string(),
},
Err(BroadcastStreamRecvError::Lagged(n)) => Self::Lagged {
stream: "local_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 {
table::Event::Reset => {
syncing = true;
changes.push(Change::Lagged);
}
table::Event::Synced => syncing = false,
table::Event::Best {
prefix,
peer,
path_id,
attrs,
} if !syncing => {
changes.push(Change::Best(Route {
prefix,
peer,
path_id,
attrs: Arc::clone(attrs),
best: true,
}));
}
table::Event::Withdrawn { prefix } if !syncing => {
changes.push(Change::Withdrawn(prefix));
}
_ => {}
});
let names = PeerNames::of(&registry);
for change in changes {
let event = match change {
Change::Best(route) => match names.view(&route) {
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(Route),
Withdrawn(IpPrefix),
/// The client's reader fell too far behind the table.
Lagged,
}
pub async fn stream(
State(state): State<AppState>,
) -> 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 = best_routes(&state);
let merged =
futures::stream::select(sessions, futures::stream::select(local, received)).map(|ev| {
Ok(Event::default()
.event("bgp")
.json_data(&ev)
.unwrap_or_else(|e| Event::default().event("error").data(e.to_string())))
});
Sse::new(merged).keep_alive(KeepAlive::default())
}
+127
View File
@@ -0,0 +1,127 @@
//! HTTP surface of the daemon: a read-only JSON API for inspecting state, a
//! status page, a server-sent event stream, and Prometheus metrics.
//!
//! Everything here is a thin layer over handles owned by the BGP server
//! ([`SessionRegistry`], [`LocalRib`], [`RibTable`]); no routing state lives
//! in this crate.
mod api;
mod events;
mod metrics;
use std::net::{Ipv4Addr, SocketAddr};
use std::sync::Arc;
use std::time::{Instant, SystemTime};
use axum::Router;
use axum::http::header;
use axum::response::{Html, IntoResponse, Redirect};
use axum::routing::get;
use bgp_rib::LocalRib;
use bgp_rib::table::RibTable;
use bgp_server::session::SessionRegistry;
use tokio::net::TcpListener;
use tracing::info;
pub use api::{AttrSummary, ConfigView, LocalRouteView, PeerView, ReceivedRouteView, StatusView};
/// Static facts about the router shown in status output.
#[derive(Debug, Clone)]
pub struct RouterInfo {
pub router_id: Ipv4Addr,
pub local_asn: u32,
pub version: String,
pub started: Instant,
/// Wall-clock start time, for display.
pub started_at: SystemTime,
/// Name of the machine we run on, for display.
pub hostname: String,
/// The configuration file as read at startup (path and raw text), shown
/// on the status page. `None` when the server was configured in code.
pub config: Option<(String, String)>,
}
impl RouterInfo {
pub fn new(router_id: Ipv4Addr, local_asn: u32, version: impl Into<String>) -> Self {
Self {
router_id,
local_asn,
version: version.into(),
started: Instant::now(),
started_at: SystemTime::now(),
hostname: hostname(),
config: None,
}
}
/// Records the configuration file so the status page can show it.
pub fn with_config(mut self, path: impl Into<String>, text: impl Into<String>) -> Self {
self.config = Some((path.into(), text.into()));
self
}
}
/// Best-effort host name without pulling in a dependency: ask the `hostname`
/// utility, which exists on every platform we run on.
fn hostname() -> String {
std::process::Command::new("hostname")
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "unknown host".into())
}
#[derive(Clone)]
pub struct AppState {
pub info: Arc<RouterInfo>,
pub registry: SessionRegistry,
pub local_rib: LocalRib,
/// 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.
const STATUSZ_HTML: &str = include_str!("../static/statusz.html");
async fn statusz() -> impl IntoResponse {
(
[(header::CONTENT_TYPE, "text/html; charset=utf-8")],
Html(STATUSZ_HTML),
)
}
/// Builds the router. Exposed so tests can drive it without a socket.
pub fn router(state: AppState) -> Router {
Router::new()
.route("/", get(|| async { Redirect::temporary("/statusz") }))
.route("/statusz", get(statusz))
.route("/api/status", get(api::status))
.route("/api/config", get(api::config))
.route("/api/peers", get(api::peers))
.route("/api/routes/local", get(api::local_routes))
.route("/api/routes/received", get(api::received_routes))
.route("/api/events", get(events::stream))
.route("/metricz", get(metrics::metricz))
.with_state(state)
}
/// Binds `listen` and serves the API until the task is dropped.
pub async fn serve(state: AppState, listen: SocketAddr) -> std::io::Result<()> {
let listener = TcpListener::bind(listen).await.map_err(|e| {
std::io::Error::new(
e.kind(),
format!("failed to bind HTTP listener on {listen}: {e}"),
)
})?;
serve_on(state, listener).await
}
/// Serves the API on an already-bound listener (lets tests use port 0).
pub async fn serve_on(state: AppState, listener: TcpListener) -> std::io::Result<()> {
info!(addr = %listener.local_addr()?, "HTTP API listening");
axum::serve(listener, router(state)).await?;
Ok(())
}
+38
View File
@@ -0,0 +1,38 @@
//! Prometheus scrape endpoint. Counters are updated by the server crate as
//! messages flow; table-size gauges are refreshed at scrape time.
use axum::extract::State;
use axum::http::{StatusCode, header};
use axum::response::IntoResponse;
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 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 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) {
Ok(()) => (
StatusCode::OK,
[(header::CONTENT_TYPE, "text/plain; version=0.0.4")],
buf,
)
.into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
}
+260
View File
@@ -0,0 +1,260 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Status for ferrix_bgp</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' fill='%23eef'/%3E%3Ctext x='8' y='12' font-size='11' font-family='sans-serif' font-weight='bold' fill='%2300c' text-anchor='middle'%3EB%3C/text%3E%3C/svg%3E">
<!--
A read-only status page in the manner of the classic /statusz pages: plain
sans-serif HTML, default links, dense bordered tables, and one full-width
light blue (#eef) heading bar per section. Deliberately no dark theme, no
framework, no external resources. Everything shown comes from /api/*.
-->
<style>
body { font-family: sans-serif; font-size: 13px; color: #000; background: #fff; margin: 8px; }
h1 { clear: both; width: 100%; text-align: center; font-size: 120%; background: #eef; margin: 14px 0 6px; padding: 2px 0; }
h1.top { font-size: 150%; margin-top: 0; }
.lefthand { float: left; width: 70%; }
.righthand { text-align: right; }
a { color: #00c; } a:visited { color: #551a8b; }
table.data { border-collapse: collapse; margin: 4px 0; }
table.data th, table.data td { border: 1px solid #99a; padding: 2px 6px; text-align: left; vertical-align: top; }
table.data th { background: #dde; font-weight: bold; }
.mono, table.data td.mono { font-family: monospace; }
.ok { color: #080; font-weight: bold; } .bad { color: #c00; font-weight: bold; } .warn { color: #a60; font-weight: bold; }
.dim { color: #666; }
#event-log { font-family: monospace; font-size: 12px; max-height: 22em; overflow-y: auto; }
#event-log div { white-space: pre-wrap; }
details { clear: both; margin: 8px 0 0; }
summary { cursor: pointer; color: #00c; }
summary:hover { text-decoration: underline; }
#config-text { border: 1px solid #99a; padding: 4px 6px; margin: 4px 0; font-size: 12px; overflow-x: auto; }
address { font-size: smaller; margin-top: 2em; color: #444; }
</style>
</head>
<body>
<h1 class="top">Status for ferrix_bgp</h1>
<div>
<div class="lefthand">
Started: <span id="s-started">...</span> (up <span id="s-uptime">...</span>)<br>
Version <span id="s-version">...</span><br>
Router ID <b id="s-router-id">...</b>, local AS <b id="s-asn">...</b><br>
</div>
<div class="righthand">
Running on <span id="s-hostname">...</span><br>
View: <a href="/metricz">variables</a>, <a href="/api/status">status json</a>, <a href="/api/peers">peers json</a>, <a href="/api/routes/local">originated routes json</a>, <a href="/api/routes/received">received routes json</a><br>
<span id="refresh-ctl"></span><br>
</div>
</div>
<details id="config-zippy" hidden>
<summary>Configuration <span class="dim" id="config-path"></span></summary>
<pre id="config-text"></pre>
</details>
<h1 id="peers">Peers (<span id="peers-count">...</span>)</h1>
<table class="data">
<thead><tr><th>name</th><th>peer</th><th>AS</th><th>state</th><th>opened by</th><th>local address (next hop)</th><th>established</th><th>uptime</th><th>4-byte AS</th><th>address families</th><th>detail</th></tr></thead>
<tbody id="peers-body"></tbody>
</table>
<h1 id="local">Originated routes (<span id="local-count">...</span>)</h1>
<table class="data">
<thead><tr><th>prefix</th><th>origin</th><th>AS path</th><th>MED</th><th>communities</th><th>large communities</th><th>since</th></tr></thead>
<tbody id="local-body"></tbody>
</table>
<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>best</th></tr></thead>
<tbody id="received-body"></tbody>
</table>
<h1 id="events">Event log</h1>
<div id="log-state" class="dim"></div>
<div id="event-log"></div>
<address>ferrix_bgp &middot; <a href="/statusz">/statusz</a> <a href="/metricz">/metricz</a> <a href="/api/events">/api/events</a></address>
<script>
(() => {
// Well-known standard communities (RFC 1997, RFC 7999, RFC 8326), shown by
// name with the number as a tooltip.
const WELL_KNOWN = {
'65535:0': 'GRACEFUL_SHUTDOWN', '65535:666': 'BLACKHOLE', '65535:65281': 'NO_EXPORT',
'65535:65282': 'NO_ADVERTISE', '65535:65283': 'NO_EXPORT_SUBCONFED', '65535:65284': 'NOPEER',
};
const $ = id => document.getElementById(id);
const esc = s => String(s).replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
const state = { status: null, peers: [], local: [], received: [] };
const DASH = '<span class="dim">-</span>';
// ---- helpers -----------------------------------------------------------
const fmtDur = s => {
s = Math.max(0, Math.floor(s));
const d = Math.floor(s / 86400), h = Math.floor(s % 86400 / 3600), m = Math.floor(s % 3600 / 60), sec = s % 60;
return d ? `${d}d ${h}h` : h ? `${h}h ${m}m` : m ? `${m}m ${sec}s` : `${sec}s`;
};
const now = () => Math.floor(Date.now() / 1000);
// Tables are only rewritten when their markup actually changes, so a
// refresh with unchanged data never touches the DOM (no flicker, and
// selections survive). Anything that changes with the clock is a
// placeholder that tick() fills in as plain text.
function setHTML(el, html) { if (el._html !== html) { el.innerHTML = html; el._html = html; tick(); } }
const age = unix => `<span data-since="${unix}"></span>`;
const until = unix => `<span data-until="${unix}"></span>`;
function tick() {
const t = now();
document.querySelectorAll('[data-since]').forEach(e => { const v = fmtDur(t - e.dataset.since); if (e.textContent !== v) e.textContent = v; });
document.querySelectorAll('[data-until]').forEach(e => { const v = fmtDur(e.dataset.until - t); if (e.textContent !== v) e.textContent = v; });
if (state.status) $('s-uptime').textContent = fmtDur(t - state.status.started_at_unix);
}
const fmtTime = unix => new Date(unix * 1000).toLocaleTimeString();
const commList = (list, named) => list.length ? list.map(c =>
named && WELL_KNOWN[c] ? `<span title="${esc(c)}">${WELL_KNOWN[c]}</span>` : esc(c)).join(' ') : DASH;
const asPath = p => p.length ? p.join(' ') : '<span class="dim">(empty)</span>';
async function getJSON(url) {
const r = await fetch(url);
if (!r.ok) throw new Error(`${url}: ${r.status} ${await r.text()}`);
return r.json();
}
// ---- renderers ---------------------------------------------------------
function renderStatus() {
const s = state.status; if (!s) return;
$('s-router-id').textContent = s.router_id; $('s-asn').textContent = s.local_asn;
$('s-version').textContent = s.version; $('s-hostname').textContent = s.hostname;
$('s-started').textContent = new Date(s.started_at_unix * 1000).toString().replace(/ \(.*\)$/, '');
tick();
}
const STATE_CLASS = { Established: 'ok', Connecting: 'warn', Backoff: 'bad', Idle: 'dim' };
// Where several routes for a prefix are exchanged (ADD-PATH), and which way.
function addPath(p) {
const rx = p.add_path_receive || [], tx = p.add_path_send || [];
if (!rx.length && !tx.length) return '';
const parts = [];
if (rx.length) parts.push(`receive ${esc(rx.join(', '))}`);
if (tx.length) parts.push(`send ${esc(tx.join(', '))}`);
return ` <span class="dim">· add-path: ${parts.join('; ')}</span>`;
}
function renderPeers() {
const b = $('peers-body');
$('peers-count').textContent = `${state.peers.filter(p => p.state === 'Established').length} of ${state.peers.length} established`;
if (!state.peers.length) { setHTML(b, '<tr><td colspan="11" class="dim">No peers configured and none connected.</td></tr>'); return; }
setHTML(b, state.peers.map(p => {
const up = p.state === 'Established';
const detail = p.state === 'Backoff' ? `${esc(p.last_error || '')}; retry in ${until(p.retry_at_unix)}` : '';
return `<tr>
<td>${p.name ? esc(p.name) : '<span class="dim">(inbound)</span>'}</td><td class="mono">${esc(p.peer_addr)}</td><td>${p.peer_asn}</td>
<td><span class="${STATE_CLASS[p.state] || ''}">${esc(p.state)}</span></td><td>${up ? (p.direction === 'outbound' ? 'us' : 'peer') : DASH}</td>
<td class="mono">${up ? esc(p.local_addr) : DASH}</td><td>${up ? fmtTime(p.established_at_unix) : DASH}</td>
<td>${up ? age(p.established_at_unix) : DASH}</td><td>${up ? (p.four_octet_asn ? 'yes' : 'no') : DASH}</td>
<td>${up ? esc((p.address_families || []).join(', ')) + addPath(p) : DASH}</td><td>${detail}</td></tr>`;
}).join(''));
}
function renderLocal() {
const b = $('local-body'); $('local-count').textContent = state.local.length;
if (!state.local.length) { setHTML(b, '<tr><td colspan="7" class="dim">This router originates no routes.</td></tr>'); return; }
const asn = state.status ? state.status.local_asn : '?';
setHTML(b, state.local.map(r => `<tr>
<td class="mono">${esc(r.prefix)}</td><td>${esc(r.origin)}</td>
<td class="mono">${Array(1 + r.prepend).fill(asn).join(' ')}</td><td>${r.med ?? DASH}</td>
<td class="mono">${commList(r.communities, true)}</td><td class="mono">${commList(r.large_communities)}</td>
<td>${age(r.announced_at_unix)}</td></tr>`).join(''));
}
function renderReceived() {
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}${r.path_id ? ` path ${r.path_id}` : ''}</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>${r.best ? '<span class="ok">best</span>' : ''}</td></tr>`; }).join(''));
}
// ---- data loading ------------------------------------------------------
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(); }
async function loadAll() { await Promise.all([loadStatus(), loadPeers(), loadLocal(), loadReceived()]); }
// ---- events ------------------------------------------------------------
function logEvent(html) {
const box = $('event-log'), d = document.createElement('div');
d.innerHTML = `${new Date().toLocaleTimeString()} ${html}`;
box.prepend(d); while (box.children.length > 200) box.lastChild.remove();
}
const names = list => list.map(c => WELL_KNOWN[c] || c).map(esc).join(' ');
function describe(ev) {
switch (ev.type) {
case 'session_up': return `<span class="ok">session UP</span> with ${esc(ev.peer.name || ev.peer.peer_addr)} AS${ev.peer.peer_asn} (opened by ${ev.peer.direction === 'outbound' ? 'us' : 'peer'})`;
case 'route_refresh_requested': return `${esc(ev.peer.name || ev.peer.peer_addr)} AS${ev.peer.peer_asn} asked for a route refresh; re-sent all originated routes`;
case 'session_down': return `<span class="bad">session DOWN</span> with ${esc(ev.peer_addr)} AS${ev.peer_asn}`;
case 'local_announced': { const r = ev.route; return `originating ${esc(r.prefix)}` +
(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 '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));
}
}
function connect() {
const st = $('log-state');
const es = new EventSource('/api/events');
es.onopen = () => { st.textContent = 'Live: connected to the event stream.'; };
es.onerror = () => { st.textContent = 'Reconnecting to the event stream...'; };
// Events only feed the log. How often the tables reload is decided by
// ?refresh=, so the page never changes faster than the reader asked for.
es.addEventListener('bgp', e => logEvent(describe(JSON.parse(e.data))));
}
// ---- auto-refresh, opt-in through ?refresh=<seconds> --------------------
const refreshParam = parseInt(new URLSearchParams(location.search).get('refresh'), 10);
const REFRESH = Number.isFinite(refreshParam) && refreshParam >= 1 ? Math.min(refreshParam, 3600) : 0;
function refreshLink(seconds, text) {
const url = new URL(location.href);
if (seconds) url.searchParams.set('refresh', seconds); else url.searchParams.delete('refresh');
return `<a href="${esc(url.pathname + url.search + url.hash)}">${text}</a>`;
}
function renderRefreshControl() {
$('refresh-ctl').innerHTML = (REFRESH
? `Auto-refresh: <b>every ${fmtDur(REFRESH)}</b> (${refreshLink(0, 'turn off')})`
: `Auto-refresh: off (turn on: ${[2, 5, 30].map(n => refreshLink(n, n + 's')).join(', ')})`) +
` &middot; <a href="#" id="reload-now">reload now</a>`;
$('reload-now').addEventListener('click', e => { e.preventDefault(); loadAll().catch(console.error); });
}
// ---- boot --------------------------------------------------------------
getJSON('/api/config').then(c => {
if (!c.text) return;
$('config-path').textContent = c.path ? `(${c.path}, as read at startup)` : '';
$('config-text').textContent = c.text;
$('config-zippy').hidden = false;
}).catch(console.error);
renderRefreshControl();
loadAll().catch(e => logEvent(`<span class="bad">${esc(e.message)}</span>`));
if (REFRESH) {
connect();
setInterval(() => loadAll().catch(console.error), REFRESH * 1000);
setInterval(tick, 1000);
} else {
// A snapshot: nothing moves until the reader reloads or turns refresh on.
$('log-state').textContent = 'The event log is live only while auto-refresh is on.';
}
})();
</script>
</body>
</html>