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>
186 lines
6.0 KiB
Rust
186 lines
6.0 KiB
Rust
//! 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)]
|
|
pub struct Poptrie<T> {
|
|
pub inners: Vec<Inner>,
|
|
pub leaves: Vec<Leaf<T>>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct Inner {
|
|
/// A bit vector that describes the child internal nodes.
|
|
pub internal_v: u64,
|
|
/// A bit vector that describes the child leaf nodes.
|
|
pub leaf_v: u64,
|
|
/// Starting offset where the child interior nodes of this node begins.
|
|
pub inner_offset: u64,
|
|
/// Starting offset where the child leaf nodes of this node begins.
|
|
pub leaf_offset: u64,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct Leaf<T> {
|
|
pub data: T,
|
|
}
|
|
|
|
#[inline]
|
|
fn extract(width: u8, offset: u8, v: u32, bits: u8) -> u8 {
|
|
let shift = bits.saturating_sub(width * (offset + 1));
|
|
let mask = 0b111111 << shift;
|
|
((v & mask) >> shift) as u8
|
|
}
|
|
|
|
fn extract_32(width: u8, offset: u8, v: u32) -> u8 {
|
|
extract(width, offset, v, 32)
|
|
}
|
|
|
|
fn extract_128(width: u8, offset: u8, v: u32) -> u8 {
|
|
extract(width, offset, v, 128)
|
|
}
|
|
|
|
impl<T: Clone> Poptrie<T> {
|
|
pub fn match_u32(&self, addr: u32) -> Option<&T> {
|
|
let mut i = 0_u64;
|
|
let mut v = self.inners.get(i as usize)?.internal_v;
|
|
let mut offset = 0;
|
|
let mut n = extract(6, offset, addr, 32);
|
|
|
|
let mut result = None;
|
|
|
|
while (v & (1 << n)) != 0 {
|
|
let base = self.inners.get(i as usize)?.inner_offset;
|
|
let arg = v & ((2 << n) - 1);
|
|
let bc = arg.count_ones() as u64;
|
|
i = base + bc - 1;
|
|
v = self.inners.get(i as usize)?.internal_v;
|
|
|
|
offset += 1;
|
|
n = extract(6, offset, addr, 32);
|
|
|
|
let base = self.inners.get(i as usize)?.leaf_offset;
|
|
let v = self.inners.get(i as usize)?.leaf_v;
|
|
if (v & (1 << n)) != 0 {
|
|
let i = base - 1;
|
|
result = Some(&self.leaves.get(i as usize)?.data);
|
|
}
|
|
}
|
|
|
|
let base = self.inners.get(i as usize)?.leaf_offset;
|
|
let v = self.inners.get(i as usize)?.leaf_v;
|
|
if (v & (1 << n)) != 0 {
|
|
i = base - 1;
|
|
result = Some(&self.leaves.get(i as usize)?.data);
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
/// 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)];
|
|
|
|
let mut i_offset = 1;
|
|
for _ in 0..DEPTH {
|
|
let mut subforest = Vec::<(u8, BTreeMap<(u32, u8), T>)>::new();
|
|
let mut children = 0;
|
|
let mut siblings = 0;
|
|
for (_, tree) in &forest {
|
|
let mut iv = 0_u64;
|
|
let mut lv = 0_u64;
|
|
|
|
let mut subsubforest = Vec::<(u8, BTreeMap<(u32, u8), T>)>::new();
|
|
for (r, e) in tree {
|
|
let k = extract(6, DEPTH, r.0, 32);
|
|
let consumed = core::cmp::min((DEPTH + 1) * 6, 32);
|
|
if r.1 < consumed {
|
|
if ((1 << k) & iv) == 0 {
|
|
lv |= 1 << k;
|
|
self.leaves.push(Leaf { data: e.clone() });
|
|
}
|
|
|
|
// If the prefix of the route entry is less than but not equal
|
|
// to the consumed number of bits, we need to add those bits
|
|
// to the bitvec.
|
|
if r.1 != consumed {
|
|
// Shift the extra bits and add to the bitvec for this
|
|
// internal node.
|
|
let extra = 1_u32 << (consumed - r.1);
|
|
for i in 1..(extra).min(64 - k as u32) {
|
|
lv |= 1_u64 << (k as u32 + i);
|
|
self.leaves.push(Leaf { data: e.clone() });
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
iv |= 1 << k;
|
|
match subsubforest.iter_mut().find(|x| x.0 == k) {
|
|
Some(ref mut entry) => {
|
|
entry.1.insert(*r, e.clone());
|
|
}
|
|
None => {
|
|
let mut tbl = BTreeMap::new();
|
|
tbl.insert(*r, e.clone());
|
|
subsubforest.push((k, tbl));
|
|
if iv > 0 {
|
|
children += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if iv > 0 || lv > 0 {
|
|
self.inners.push(Inner {
|
|
internal_v: iv,
|
|
leaf_v: lv,
|
|
inner_offset: if iv > 0 { i_offset + siblings } else { 0 },
|
|
leaf_offset: self.leaves.len() as u64,
|
|
});
|
|
if iv > 0 {
|
|
siblings += 1;
|
|
}
|
|
}
|
|
|
|
subforest.extend_from_slice(subsubforest.as_slice());
|
|
}
|
|
i_offset += children;
|
|
forest = subforest;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::{collections::BTreeMap, net::Ipv4Addr};
|
|
|
|
use crate::poptrie::Poptrie;
|
|
|
|
#[test]
|
|
fn test_poptrie() {
|
|
let mut map: BTreeMap<(u32, u8), Ipv4Addr> = Default::default();
|
|
map.insert(
|
|
(Ipv4Addr::new(127, 0, 0, 0).into(), 8),
|
|
Ipv4Addr::new(10, 0, 0, 1),
|
|
);
|
|
|
|
let mut poptrie: Poptrie<Ipv4Addr> = Poptrie {
|
|
inners: vec![],
|
|
leaves: vec![],
|
|
};
|
|
|
|
poptrie.build(map);
|
|
assert!(
|
|
!poptrie.inners.is_empty(),
|
|
"building from a non-empty table yields nodes"
|
|
);
|
|
}
|
|
}
|