Files
ferrix/crates/rpki/src/vrp.rs
T
Rayhaan JaufeerallyandClaude Fable 5.1 1f89fb4b13 Add the bgp-rpki crate: route origin validation and an RTR client
The first part of RPKI origin validation (RFC 6811): the data and how it
gets here. Nothing uses it yet.

VrpTable holds the validated ROA payloads and says whether a route is
valid, invalid or not found. It is a binary trie in one allocation with
a short cut past the first sixteen levels; a lookup against 600,000
entries takes about 40 ns for random addresses. A table is built once
and not changed: when the data changes another takes its place.

rtr is the RPKI-to-Router protocol's PDUs (RFC 8210, and version 0 of
RFC 6810), as bytes and back, without I/O. Lengths are checked from the
header, before the rest of a PDU is waited for.

The client keeps the table in step with a validator's cache. It asks
for everything once and then for changes, when the cache notifies and
at the refresh interval. An answer takes effect only when it is complete
and consistent. Each update says which VRPs came or went, so that only
the routes they cover need looking at again. A cache that fails is
retried and the next one tried; the data is used meanwhile, and dropped
if no cache confirms it within the expire interval. A cache that speaks
only version 0 is spoken to in version 0.

Tests: the cases of RFC 6811, a property test of the trie against the
plain definition, round-trip and hostile-input properties for the
codec, a fuzz target, and the client against a scripted cache.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 13:28:32 +02:00

430 lines
14 KiB
Rust

//! The validated ROA payloads, and what they say about a route.
use bgp_primitives::constants::IpVersion;
use bgp_primitives::ip_prefix::IpPrefix;
/// A validated ROA payload: `asn` may originate `prefix`, and any prefix
/// inside it of at most `max_length` bits.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Vrp {
pub prefix: IpPrefix,
pub max_length: u8,
pub asn: u32,
}
/// What the VRPs say about a route (RFC 6811 §2).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Validity {
/// No VRP covers the route's prefix: nothing is known about it. Most
/// of the Internet's routes are in this state, and are used.
NotFound,
/// A VRP covers the prefix and allows this origin at this length.
Valid,
/// VRPs cover the prefix and none of them allows this origin at this
/// length: the holder of the addresses has said who may announce
/// them, and this is not it.
Invalid,
}
impl Validity {
pub fn as_str(self) -> &'static str {
match self {
Self::NotFound => "not found",
Self::Valid => "valid",
Self::Invalid => "invalid",
}
}
}
/// A set of VRPs, arranged for asking about routes: this is asked for
/// every route received.
///
/// It is built once and not changed. When the VRPs change, another is built
/// and takes its place.
#[derive(Debug, Default)]
pub struct VrpTable {
v4: Trie,
v6: Trie,
len: usize,
}
/// A binary trie over prefixes: a node for each prefix that is a VRP's or
/// lies on the way to one.
#[derive(Debug)]
struct Trie {
nodes: Vec<Node>,
/// What the VRPs of each node allow, in runs by node: (maximum length,
/// origin AS).
allowed: Vec<(u8, u32)>,
/// A short cut past the first sixteen levels, which every lookup of a
/// longer prefix would otherwise walk: for each value of an address's
/// first sixteen bits, the node those bits lead to, or zero if there
/// is none. [`WALK`] is set where a VRP lies on the way there, which
/// is rare; such lookups walk from the root.
jump: Box<[u32]>,
}
/// How many leading bits [`Trie::jump`] is indexed by.
const JUMP_BITS: u8 = 16;
/// Set in a [`Trie::jump`] entry: walk from the root.
const WALK: u32 = 1 << 31;
#[derive(Debug, Clone, Copy, Default)]
struct Node {
/// The nodes for this prefix extended by a zero bit and by a one bit;
/// zero where there is none (the root is nobody's child).
children: [u32; 2],
/// Where in `allowed` this prefix's VRPs are, and how many.
first: u32,
count: u32,
}
impl Default for Trie {
fn default() -> Self {
Self {
nodes: vec![Node::default()],
allowed: Vec::new(),
jump: vec![0; 1 << JUMP_BITS].into(),
}
}
}
/// The bit of `bytes` at `index`, counting from the most significant bit
/// of the first byte.
fn bit(bytes: &[u8], index: u8) -> usize {
usize::from(bytes[usize::from(index / 8)] >> (7 - index % 8) & 1)
}
impl Trie {
/// `vrps` must be sorted, so that those of one prefix are together.
fn build(vrps: &[Vrp]) -> Self {
let mut trie = Self::default();
for group in vrps.chunk_by(|a, b| a.prefix == b.prefix) {
let prefix = &group[0].prefix;
let mut at = 0usize;
for index in 0..prefix.length() {
let side = bit(prefix.bytes(), index);
at = match trie.nodes[at].children[side] {
0 => {
trie.nodes.push(Node::default());
let new = trie.nodes.len() - 1;
trie.nodes[at].children[side] = new as u32;
new
}
child => child as usize,
};
}
trie.nodes[at].first = trie.allowed.len() as u32;
trie.nodes[at].count = group.len() as u32;
trie.allowed
.extend(group.iter().map(|vrp| (vrp.max_length, vrp.asn)));
}
assert!(
trie.nodes.len() < WALK as usize,
"more VRPs than a table can hold"
);
for slot in 0..=u16::MAX {
trie.jump[usize::from(slot)] = trie.jump_for(slot);
}
trie
}
/// The [`Trie::jump`] entry for addresses that begin with `bits`.
fn jump_for(&self, bits: u16) -> u32 {
let bytes = bits.to_be_bytes();
let mut at = 0usize;
for index in 0..JUMP_BITS {
if self.nodes[at].count > 0 {
return WALK;
}
match self.nodes[at].children[bit(&bytes, index)] {
0 => return 0,
child => at = child as usize,
}
}
at as u32
}
/// Calls `visit` with what the VRPs of each prefix that covers `prefix`
/// allow, from the shortest; stops when `visit` returns true, and
/// returns whether it did.
fn covering(&self, prefix: &IpPrefix, mut visit: impl FnMut(&[(u8, u32)]) -> bool) -> bool {
let bytes = prefix.bytes();
let mut at = 0usize;
let mut index = 0;
if let [first, second, ..] = bytes
&& prefix.length() >= JUMP_BITS
{
match self.jump[usize::from(u16::from_be_bytes([*first, *second]))] {
WALK => {}
// Nothing covers an address that begins like this.
0 => return false,
node => (at, index) = (node as usize, JUMP_BITS),
}
}
loop {
let node = &self.nodes[at];
if node.count > 0 {
let first = node.first as usize;
if visit(&self.allowed[first..first + node.count as usize]) {
return true;
}
}
if index == prefix.length() {
return false;
}
match node.children[bit(bytes, index)] {
0 => return false,
child => at = child as usize,
}
index += 1;
}
}
}
impl VrpTable {
/// A table of `vrps`. Listing one more than once is the same as listing
/// it once.
pub fn new(vrps: impl IntoIterator<Item = Vrp>) -> Self {
let mut vrps: Vec<Vrp> = vrps.into_iter().collect();
vrps.sort_unstable();
vrps.dedup();
let v6_from = vrps.partition_point(|vrp| vrp.prefix.address_family() == IpVersion::Ipv4);
debug_assert!(
vrps[..v6_from]
.iter()
.all(|vrp| vrp.prefix.address_family() == IpVersion::Ipv4)
);
Self {
v4: Trie::build(&vrps[..v6_from]),
v6: Trie::build(&vrps[v6_from..]),
len: vrps.len(),
}
}
/// How many VRPs the table holds.
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
fn trie(&self, prefix: &IpPrefix) -> &Trie {
match prefix.address_family() {
IpVersion::Ipv4 => &self.v4,
IpVersion::Ipv6 => &self.v6,
}
}
/// The validity of a route for `prefix` that originates from `origin`
/// (RFC 6811 §2). `None` is a route with no one origin, as when its AS
/// path ends in a set: no VRP can allow it.
pub fn validate(&self, prefix: &IpPrefix, origin: Option<u32>) -> Validity {
let mut covered = false;
let valid = self.trie(prefix).covering(prefix, |allowed| {
covered = true;
// AS 0 in a VRP says that nobody may originate the prefix
// (RFC 6483 §4); it is the origin of no route.
origin.is_some_and(|origin| {
origin != 0
&& allowed
.iter()
.any(|(max_length, asn)| *asn == origin && prefix.length() <= *max_length)
})
});
match (valid, covered) {
(true, _) => Validity::Valid,
(false, true) => Validity::Invalid,
(false, false) => Validity::NotFound,
}
}
/// Whether some VRP covers `prefix`: whether the VRPs say anything
/// about routes for it.
pub fn covers(&self, prefix: &IpPrefix) -> bool {
self.trie(prefix).covering(prefix, |_| true)
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::*;
fn prefix(s: &str) -> IpPrefix {
IpPrefix::try_from(s).unwrap()
}
fn vrp(p: &str, max_length: u8, asn: u32) -> Vrp {
Vrp {
prefix: prefix(p),
max_length,
asn,
}
}
/// The cases of RFC 6811 §2.
#[test]
fn a_route_is_valid_invalid_or_not_found() {
let table = VrpTable::new([
vrp("193.0.0.0/21", 24, 3333),
vrp("193.0.0.0/21", 21, 2121),
vrp("2001:67c:2e8::/48", 48, 3333),
]);
let validate = |p: &str, origin| table.validate(&prefix(p), Some(origin));
// Covered, by the right AS, no longer than allowed.
assert_eq!(validate("193.0.0.0/21", 3333), Validity::Valid);
assert_eq!(validate("193.0.4.0/24", 3333), Validity::Valid);
assert_eq!(validate("193.0.0.0/21", 2121), Validity::Valid);
assert_eq!(validate("2001:67c:2e8::/48", 3333), Validity::Valid);
// The right AS, but more specific than it may announce.
assert_eq!(validate("193.0.4.0/24", 2121), Validity::Invalid);
assert_eq!(validate("193.0.4.0/25", 3333), Validity::Invalid);
// Someone else's AS: a hijack, or a mistake.
assert_eq!(validate("193.0.0.0/21", 15169), Validity::Invalid);
assert_eq!(validate("2001:67c:2e8::/48", 15169), Validity::Invalid);
// Nothing covers these: a shorter prefix, another block, another
// family.
assert_eq!(validate("193.0.0.0/20", 3333), Validity::NotFound);
assert_eq!(validate("194.0.0.0/21", 3333), Validity::NotFound);
assert_eq!(validate("2001:67c:2e9::/48", 3333), Validity::NotFound);
// A route with no one origin matches no VRP.
assert_eq!(
table.validate(&prefix("193.0.0.0/21"), None),
Validity::Invalid
);
assert_eq!(
table.validate(&prefix("194.0.0.0/21"), None),
Validity::NotFound
);
assert_eq!(table.len(), 3);
assert!(table.covers(&prefix("193.0.7.0/24")));
assert!(!table.covers(&prefix("193.0.8.0/24")));
}
/// A VRP for AS 0 says the prefix is not to be routed at all.
#[test]
fn as_zero_allows_nobody() {
let table = VrpTable::new([vrp("203.0.113.0/24", 24, 0)]);
assert_eq!(
table.validate(&prefix("203.0.113.0/24"), Some(0)),
Validity::Invalid
);
assert_eq!(
table.validate(&prefix("203.0.113.0/24"), Some(64500)),
Validity::Invalid
);
}
/// A VRP covers a route however many other VRPs cover it too: one that
/// allows it is enough.
#[test]
fn any_covering_vrp_can_make_a_route_valid() {
let table = VrpTable::new([
vrp("10.0.0.0/8", 8, 1),
vrp("10.1.0.0/16", 24, 2),
vrp("0.0.0.0/0", 0, 3),
]);
assert_eq!(
table.validate(&prefix("10.1.2.0/24"), Some(2)),
Validity::Valid
);
assert_eq!(
table.validate(&prefix("10.1.2.0/24"), Some(1)),
Validity::Invalid
);
assert_eq!(
table.validate(&prefix("10.0.0.0/8"), Some(1)),
Validity::Valid
);
assert_eq!(
table.validate(&prefix("0.0.0.0/0"), Some(3)),
Validity::Valid
);
// The VRP for the default route covers everything.
assert_eq!(
table.validate(&prefix("192.0.2.0/24"), Some(9)),
Validity::Invalid
);
}
#[test]
fn an_empty_table_knows_nothing() {
let table = VrpTable::default();
assert!(table.is_empty());
assert_eq!(
table.validate(&prefix("193.0.0.0/21"), Some(3333)),
Validity::NotFound
);
assert_eq!(table.validate(&prefix("::/0"), None), Validity::NotFound);
}
fn any_vrp() -> impl Strategy<Value = Vrp> {
// Few distinct addresses and AS numbers, so that routes and VRPs
// meet.
(any::<bool>(), 0u8..4, 0u8..=24, 0u8..=8, 0u32..4).prop_map(
|(v6, address, length, extra, asn)| {
let version = if v6 { IpVersion::Ipv6 } else { IpVersion::Ipv4 };
let bytes = [
address << 6 | address,
address,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
];
Vrp {
prefix: IpPrefix::new(version, bytes, length).unwrap(),
max_length: length + extra,
asn,
}
},
)
}
proptest! {
/// The table agrees with reading RFC 6811 out over the list.
#[test]
fn the_table_agrees_with_looking_through_the_list(
vrps in proptest::collection::vec(any_vrp(), 0..30),
routes in proptest::collection::vec((any_vrp(), any::<bool>()), 1..40),
) {
let table = VrpTable::new(vrps.iter().copied());
for (route, has_origin) in routes {
let origin = has_origin.then_some(route.asn);
let covering: Vec<&Vrp> = vrps
.iter()
.filter(|vrp| vrp.prefix.contains(&route.prefix))
.collect();
let expected = if covering.is_empty() {
Validity::NotFound
} else if covering.iter().any(|vrp| {
origin.is_some_and(|origin| origin != 0 && origin == vrp.asn)
&& route.prefix.length() <= vrp.max_length
}) {
Validity::Valid
} else {
Validity::Invalid
};
prop_assert_eq!(table.validate(&route.prefix, origin), expected, "{:?}", route);
prop_assert_eq!(table.covers(&route.prefix), !covering.is_empty());
}
}
}
}