Files
ferrix/crates/primitives/src/poptrie/mod.rs
T
Rayhaan JaufeerallyandClaude Fable 5.1 630c175918 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>
2026-10-02 12:35:54 +02:00

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"
);
}
}