Files
ferrix/crates/rpki/src/rtr.rs
T
Rayhaan JaufeerallyandClaude Fable 5.1 8086aa994f Verify AS paths with ASPAs, over RTR version 2
Origin validation says who may originate a prefix and nothing about the
rest of the path: it does not catch a route leak, nor a hijack that keeps
the right origin at the end of a made-up path. ASPAs, in which a network
lists its providers, do.

bgp-rpki:
- aspa: the ASPAs and the upstream and downstream verification procedures
  of draft-ietf-sidrops-aspa-verification-24, tested against the draft's
  wording as written.
- rtr: protocol version 2 (draft-ietf-sidrops-8210bis-23) with the ASPA
  PDU and the new error codes.
- client: asks for version 2 and falls back to whatever older version the
  cache speaks; keeps ASPAs with the VRPs, applied whole or not at all;
  publishes which customers' ASPAs changed; tells the cache why it is
  being left (Error Report); does not ask one version's serial of another.

Server:
- A peer has a role (provider, customer, peer, route server, route server
  client), which decides how its paths are verified. Without one they are
  not.
- A route whose path is invalid is refused ("ASPA invalid") and held, like
  one with an invalid origin; accept_aspa_invalid exempts a peer.
- When ASPAs change, routes in the table whose path has a changed customer
  on it are verified again, and held routes are released when they pass
  both checks.
- The revalidator finds a route's peer by index instead of searching a
  list for every route.

The status page and API show each peer's role, each route's path validity,
the number of ASPAs and the RTR version in use; bgp_rpki_aspas is on
/metricz.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 14:12:59 +02:00

675 lines
24 KiB
Rust

//! The messages of the RPKI-to-Router protocol, as bytes and back: version
//! 0 (RFC 6810), version 1 (RFC 8210) and version 2
//! (draft-ietf-sidrops-8210bis). No I/O here.
//!
//! Every message (a PDU) begins with the same eight octets: the protocol
//! version, the type, a field whose meaning depends on the type, and the
//! length of the whole PDU.
use bgp_primitives::constants::IpVersion;
use bgp_primitives::ip_prefix::IpPrefix;
use bytes::{BufMut, BytesMut};
use crate::vrp::Vrp;
/// The newest protocol version, which this client asks for first. Version
/// 2 added ASPAs.
pub const VERSION_2: u8 = 2;
/// Version 1 added the timers in End of Data and router keys.
pub const VERSION_1: u8 = 1;
/// What the oldest validators speak.
pub const VERSION_0: u8 = 0;
const HEADER_LEN: usize = 8;
/// The longest PDU this client reads. Every PDU but Error Report, Router
/// Key and ASPA has a fixed, small length. An ASPA of this length lists
/// sixteen thousand providers, more than validators let an AS have.
pub const MAX_PDU_LEN: usize = 64 * 1024;
mod pdu_type {
pub const SERIAL_NOTIFY: u8 = 0;
pub const SERIAL_QUERY: u8 = 1;
pub const RESET_QUERY: u8 = 2;
pub const CACHE_RESPONSE: u8 = 3;
pub const IPV4_PREFIX: u8 = 4;
pub const IPV6_PREFIX: u8 = 6;
pub const END_OF_DATA: u8 = 7;
pub const CACHE_RESET: u8 = 8;
pub const ROUTER_KEY: u8 = 9;
pub const ERROR_REPORT: u8 = 10;
pub const ASPA: u8 = 11;
}
/// Error codes of an Error Report (RFC 8210 §12; from 9 on, version 2).
pub mod error_code {
pub const CORRUPT_DATA: u16 = 0;
pub const INTERNAL_ERROR: u16 = 1;
pub const NO_DATA_AVAILABLE: u16 = 2;
pub const INVALID_REQUEST: u16 = 3;
pub const UNSUPPORTED_VERSION: u16 = 4;
pub const UNSUPPORTED_PDU_TYPE: u16 = 5;
pub const WITHDRAWAL_OF_UNKNOWN_RECORD: u16 = 6;
pub const DUPLICATE_ANNOUNCEMENT: u16 = 7;
pub const UNEXPECTED_VERSION: u16 = 8;
pub const ASPA_PROVIDER_LIST: u16 = 9;
pub const TRANSPORT_FAILURE: u16 = 10;
pub const ORDERING: u16 = 11;
}
/// The timers a validator tells its routers to use (RFC 8210 §6), in
/// seconds.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Timers {
/// How long to wait before asking for changes unprompted.
pub refresh: u32,
/// How long to wait before trying again after a failure.
pub retry: u32,
/// How long the data may be used without a successful update.
pub expire: u32,
}
impl Default for Timers {
/// What RFC 8210 §6 recommends, and what a version 0 session uses.
fn default() -> Self {
Self {
refresh: 3600,
retry: 600,
expire: 7200,
}
}
}
/// One PDU.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Pdu {
/// The validator has new data, up to `serial`.
SerialNotify { session_id: u16, serial: u32 },
/// Send what has changed since `serial`.
SerialQuery { session_id: u16, serial: u32 },
/// Send everything.
ResetQuery,
/// The answer to a query begins: prefixes follow, then End of Data.
CacheResponse { session_id: u16 },
/// A VRP, announced or withdrawn.
Prefix { announce: bool, vrp: Vrp },
/// The answer is complete, and the data is now at `serial`. `timers`
/// are there from version 1 on.
EndOfData {
session_id: u16,
serial: u32,
timers: Option<Timers>,
},
/// The validator cannot say what changed since the serial asked about;
/// ask for everything.
CacheReset,
/// A BGPsec router key. Not used here; read so as to be skipped.
RouterKey,
/// The ASPA of `customer`, whole (version 2). Announced, it takes the
/// place of any the cache gave for that customer before, and lists at
/// least one provider; withdrawn, it lists none.
Aspa {
announce: bool,
customer: u32,
providers: Vec<u32>,
},
/// Something went wrong at the sender's end, or with what we sent.
ErrorReport { code: u16, text: String },
}
/// Bytes that are not a PDU this client can read.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum RtrError {
#[error("PDU of type {pdu_type} has length {length}, which is not valid for it")]
Length { pdu_type: u8, length: u32 },
#[error("unknown PDU type {0}")]
UnknownType(u8),
#[error("protocol version {0} is not supported")]
Version(u8),
#[error("a prefix PDU with prefix length {length} and maximum length {max_length}")]
Prefix { length: u8, max_length: u8 },
#[error("an ASPA of AS{customer} is announced with no providers")]
AspaWithoutProviders { customer: u32 },
}
impl RtrError {
/// The error code that reports this to the sender (RFC 8210 §12).
pub fn code(&self) -> u16 {
match self {
Self::Length { .. } | Self::Prefix { .. } => error_code::CORRUPT_DATA,
Self::UnknownType(_) => error_code::UNSUPPORTED_PDU_TYPE,
Self::Version(_) => error_code::UNSUPPORTED_VERSION,
Self::AspaWithoutProviders { .. } => error_code::ASPA_PROVIDER_LIST,
}
}
}
fn be_u16(bytes: &[u8]) -> u16 {
u16::from_be_bytes([bytes[0], bytes[1]])
}
fn be_u32(bytes: &[u8]) -> u32 {
u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
}
impl Pdu {
/// Reads the PDU at the front of `buf`, if all of it has arrived:
/// the protocol version it was sent with, the PDU, and how many octets
/// it took. `Ok(None)` means more octets are needed.
pub fn decode(buf: &[u8]) -> Result<Option<(u8, Self, usize)>, RtrError> {
let Some(header) = buf.first_chunk::<HEADER_LEN>() else {
return Ok(None);
};
let (version, pdu_type, flags) = (header[0], header[1], header[2]);
let field = be_u16(&header[2..]);
let length = be_u32(&header[4..]);
let bad_length = || RtrError::Length { pdu_type, length };
if version > VERSION_2 {
return Err(RtrError::Version(version));
}
let total = usize::try_from(length).map_err(|_| bad_length())?;
if !(HEADER_LEN..=MAX_PDU_LEN).contains(&total) {
return Err(bad_length());
}
// The fixed lengths are checked before waiting for the rest, so
// that a wrong one is not waited for.
let fixed = match pdu_type {
pdu_type::SERIAL_NOTIFY | pdu_type::SERIAL_QUERY => Some(12),
pdu_type::RESET_QUERY | pdu_type::CACHE_RESPONSE | pdu_type::CACHE_RESET => Some(8),
pdu_type::IPV4_PREFIX => Some(20),
pdu_type::IPV6_PREFIX => Some(32),
pdu_type::END_OF_DATA => Some(if version == VERSION_0 { 12 } else { 24 }),
pdu_type::ROUTER_KEY | pdu_type::ERROR_REPORT => None,
pdu_type::ASPA if version >= VERSION_2 => {
// The customer, and whole AS numbers after it; none of
// them in a withdrawal.
let announce = flags & 1 == 1;
if total < 12 || !total.is_multiple_of(4) || (!announce && total != 12) {
return Err(bad_length());
}
None
}
other => return Err(RtrError::UnknownType(other)),
};
if fixed.is_some_and(|fixed| fixed != total) {
return Err(bad_length());
}
let Some(body) = buf.get(HEADER_LEN..total) else {
return Ok(None);
};
let pdu = match pdu_type {
pdu_type::SERIAL_NOTIFY => Self::SerialNotify {
session_id: field,
serial: be_u32(body),
},
pdu_type::SERIAL_QUERY => Self::SerialQuery {
session_id: field,
serial: be_u32(body),
},
pdu_type::RESET_QUERY => Self::ResetQuery,
pdu_type::CACHE_RESPONSE => Self::CacheResponse { session_id: field },
pdu_type::IPV4_PREFIX | pdu_type::IPV6_PREFIX => {
let version = if pdu_type == pdu_type::IPV4_PREFIX {
IpVersion::Ipv4
} else {
IpVersion::Ipv6
};
let (flags, length, max_length) = (body[0], body[1], body[2]);
let address = &body[4..body.len() - 4];
let bad_prefix = || RtrError::Prefix { length, max_length };
if max_length < length || max_length > version.bits() {
return Err(bad_prefix());
}
Self::Prefix {
announce: flags & 1 == 1,
vrp: Vrp {
prefix: IpPrefix::new(version, address, length)
.map_err(|_| bad_prefix())?,
max_length,
asn: be_u32(&body[body.len() - 4..]),
},
}
}
pdu_type::END_OF_DATA => Self::EndOfData {
session_id: field,
serial: be_u32(body),
timers: (version != VERSION_0).then(|| Timers {
refresh: be_u32(&body[4..]),
retry: be_u32(&body[8..]),
expire: be_u32(&body[12..]),
}),
},
pdu_type::CACHE_RESET => Self::CacheReset,
pdu_type::ROUTER_KEY => Self::RouterKey,
pdu_type::ASPA => {
let announce = flags & 1 == 1;
let (customer, providers) = body.split_at(4);
let customer = be_u32(customer);
if announce && providers.is_empty() {
return Err(RtrError::AspaWithoutProviders { customer });
}
Self::Aspa {
announce,
customer,
providers: providers
.as_chunks::<4>()
.0
.iter()
.map(|asn| u32::from_be_bytes(*asn))
.collect(),
}
}
pdu_type::ERROR_REPORT => {
// The length of the PDU that was at fault, that PDU, the
// length of the text, the text.
let text = (|| {
let pdu_len = usize::try_from(be_u32(body.get(..4)?)).ok()?;
let rest = body.get(4usize.checked_add(pdu_len)?..)?;
let text_len = usize::try_from(be_u32(rest.get(..4)?)).ok()?;
let text = rest.get(4..)?;
(text.len() == text_len).then(|| String::from_utf8_lossy(text).into_owned())
})()
.ok_or_else(bad_length)?;
Self::ErrorReport { code: field, text }
}
_ => unreachable!("unknown types were refused above"),
};
Ok(Some((version, pdu, total)))
}
/// Writes the PDU, for a session that speaks protocol version
/// `version`, to the end of `out`. Whether that version has such a PDU
/// is the caller's to know.
pub fn encode(&self, version: u8, out: &mut BytesMut) {
let start = out.len();
let header = |out: &mut BytesMut, pdu_type: u8, field: u16| {
out.put_u8(version);
out.put_u8(pdu_type);
out.put_u16(field);
// The length, filled in below.
out.put_u32(0);
};
match self {
Self::SerialNotify { session_id, serial } => {
header(out, pdu_type::SERIAL_NOTIFY, *session_id);
out.put_u32(*serial);
}
Self::SerialQuery { session_id, serial } => {
header(out, pdu_type::SERIAL_QUERY, *session_id);
out.put_u32(*serial);
}
Self::ResetQuery => header(out, pdu_type::RESET_QUERY, 0),
Self::CacheResponse { session_id } => {
header(out, pdu_type::CACHE_RESPONSE, *session_id);
}
Self::Prefix { announce, vrp } => {
let (pdu_type, address_len) = match vrp.prefix.address_family() {
IpVersion::Ipv4 => (pdu_type::IPV4_PREFIX, 4),
IpVersion::Ipv6 => (pdu_type::IPV6_PREFIX, 16),
};
header(out, pdu_type, 0);
out.put_u8(u8::from(*announce));
out.put_u8(vrp.prefix.length());
out.put_u8(vrp.max_length);
out.put_u8(0);
let mut address = [0u8; 16];
address[..vrp.prefix.bytes().len()].copy_from_slice(vrp.prefix.bytes());
out.put_slice(&address[..address_len]);
out.put_u32(vrp.asn);
}
Self::EndOfData {
session_id,
serial,
timers,
} => {
header(out, pdu_type::END_OF_DATA, *session_id);
out.put_u32(*serial);
if version != VERSION_0 {
let timers = timers.unwrap_or_default();
out.put_u32(timers.refresh);
out.put_u32(timers.retry);
out.put_u32(timers.expire);
}
}
Self::CacheReset => header(out, pdu_type::CACHE_RESET, 0),
Self::RouterKey => header(out, pdu_type::ROUTER_KEY, 0),
Self::Aspa {
announce,
customer,
providers,
} => {
header(out, pdu_type::ASPA, u16::from(*announce) << 8);
out.put_u32(*customer);
for provider in providers {
out.put_u32(*provider);
}
}
Self::ErrorReport { code, text } => {
header(out, pdu_type::ERROR_REPORT, *code);
// No copy of the PDU at fault.
out.put_u32(0);
out.put_u32(text.len() as u32);
out.put_slice(text.as_bytes());
}
}
let length = (out.len() - start) as u32;
out[start + 4..start + 8].copy_from_slice(&length.to_be_bytes());
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::*;
fn decode_one(bytes: &[u8]) -> (u8, Pdu) {
let (version, pdu, used) = Pdu::decode(bytes).unwrap().expect("a whole PDU");
assert_eq!(used, bytes.len());
(version, pdu)
}
/// The PDUs as RFC 8210 §5 draws them.
#[test]
fn pdus_are_read_as_the_rfc_lays_them_out() {
#[rustfmt::skip]
let ipv4 = [
1, 4, 0, 0, 0, 0, 0, 20,
1, 21, 24, 0,
193, 0, 0, 0,
0, 0, 0x0d, 0x05,
];
assert_eq!(
decode_one(&ipv4),
(
1,
Pdu::Prefix {
announce: true,
vrp: Vrp {
prefix: IpPrefix::try_from("193.0.0.0/21").unwrap(),
max_length: 24,
asn: 3333,
},
}
)
);
#[rustfmt::skip]
let ipv6 = [
1, 6, 0, 0, 0, 0, 0, 32,
0, 48, 48, 0,
0x20, 0x01, 0x06, 0x7c, 0x02, 0xe8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0x0d, 0x05,
];
assert_eq!(
decode_one(&ipv6),
(
1,
Pdu::Prefix {
announce: false,
vrp: Vrp {
prefix: IpPrefix::try_from("2001:67c:2e8::/48").unwrap(),
max_length: 48,
asn: 3333,
},
}
)
);
#[rustfmt::skip]
let end_of_data = [
1, 7, 0x12, 0x34, 0, 0, 0, 24,
0, 0, 0, 99,
0, 0, 0x0e, 0x10, 0, 0, 0x02, 0x58, 0, 0, 0x1c, 0x20,
];
assert_eq!(
decode_one(&end_of_data),
(
1,
Pdu::EndOfData {
session_id: 0x1234,
serial: 99,
timers: Some(Timers::default()),
}
)
);
// Version 0 has no timers.
assert_eq!(
decode_one(&[0, 7, 0x12, 0x34, 0, 0, 0, 12, 0, 0, 0, 99]),
(
0,
Pdu::EndOfData {
session_id: 0x1234,
serial: 99,
timers: None,
}
)
);
// Version 2's ASPA, as draft-ietf-sidrops-8210bis §5.12 draws it.
#[rustfmt::skip]
let aspa = [
2, 11, 1, 0, 0, 0, 0, 20,
0, 0, 0x0d, 0x05,
0, 0, 0x04, 0x4d, 0, 0, 0x0c, 0xb9,
];
assert_eq!(
decode_one(&aspa),
(
2,
Pdu::Aspa {
announce: true,
customer: 3333,
providers: vec![1101, 3257],
}
)
);
assert_eq!(
decode_one(&[2, 11, 0, 0, 0, 0, 0, 12, 0, 0, 0x0d, 0x05]),
(
2,
Pdu::Aspa {
announce: false,
customer: 3333,
providers: vec![],
}
)
);
#[rustfmt::skip]
let error = [
1, 10, 0, 2, 0, 0, 0, 30,
0, 0, 0, 8, 1, 1, 0, 0, 0, 0, 0, 12,
0, 0, 0, 6, b'n', b'o', b' ', b'd', b'a', b't',
];
assert_eq!(
decode_one(&error),
(
1,
Pdu::ErrorReport {
code: error_code::NO_DATA_AVAILABLE,
text: "no dat".into(),
}
)
);
}
#[test]
fn a_pdu_that_has_not_all_arrived_is_waited_for() {
let mut wire = BytesMut::new();
Pdu::SerialNotify {
session_id: 7,
serial: 42,
}
.encode(VERSION_1, &mut wire);
for cut in 0..wire.len() {
assert_eq!(Pdu::decode(&wire[..cut]), Ok(None), "{cut} octets");
}
assert!(Pdu::decode(&wire).unwrap().is_some());
}
#[test]
fn what_is_not_a_pdu_is_refused_from_its_header() {
// A version from the future.
assert_eq!(
Pdu::decode(&[3, 2, 0, 0, 0, 0, 0, 8]),
Err(RtrError::Version(3))
);
assert_eq!(
Pdu::decode(&[1, 99, 0, 0, 0, 0, 0, 8]),
Err(RtrError::UnknownType(99))
);
// A Reset Query that claims to be long, and a length no PDU has:
// refused without waiting for the octets.
for length in [9u32, 7, 0, 1 << 30] {
let mut header = vec![1, 2, 0, 0];
header.extend_from_slice(&length.to_be_bytes());
assert_eq!(
Pdu::decode(&header),
Err(RtrError::Length {
pdu_type: 2,
length
}),
);
}
// A maximum length shorter than the prefix, and longer than an
// address.
for (length, max_length) in [(24, 21), (24, 33)] {
#[rustfmt::skip]
let prefix = [
1, 4, 0, 0, 0, 0, 0, 20,
1, length, max_length, 0, 193, 0, 0, 0, 0, 0, 0, 1,
];
assert_eq!(
Pdu::decode(&prefix),
Err(RtrError::Prefix { length, max_length })
);
}
// An ASPA before version 2 had them.
assert_eq!(
Pdu::decode(&[1, 11, 0, 0, 0, 0, 0, 12, 0, 0, 0, 1]),
Err(RtrError::UnknownType(11))
);
// ASPAs with half an AS number, without a customer, withdrawn
// with providers: refused from the header.
for (flags, length) in [(1u8, 18u32), (1, 8), (0, 16)] {
let mut header = vec![2, 11, flags, 0];
header.extend_from_slice(&length.to_be_bytes());
assert_eq!(
Pdu::decode(&header),
Err(RtrError::Length {
pdu_type: 11,
length
}),
);
}
// Announced with no providers.
assert_eq!(
Pdu::decode(&[2, 11, 1, 0, 0, 0, 0, 12, 0, 0, 0xfd, 0xe8]),
Err(RtrError::AspaWithoutProviders { customer: 65000 })
);
// An Error Report whose inner lengths do not add up.
#[rustfmt::skip]
let error = [1, 10, 0, 2, 0, 0, 0, 16, 0, 0, 0, 9, 0, 0, 0, 0];
assert!(matches!(Pdu::decode(&error), Err(RtrError::Length { .. })));
}
fn any_prefix() -> impl Strategy<Value = Vrp> {
(
any::<bool>(),
any::<[u8; 16]>(),
0u8..=128,
0u8..=128,
any::<u32>(),
)
.prop_map(|(v6, bytes, length, extra, asn)| {
let version = if v6 { IpVersion::Ipv6 } else { IpVersion::Ipv4 };
let length = length.min(version.bits());
Vrp {
prefix: IpPrefix::new(version, bytes, length).unwrap(),
max_length: length.saturating_add(extra).min(version.bits()),
asn,
}
})
}
fn any_pdu() -> impl Strategy<Value = Pdu> {
prop_oneof![
(any::<u16>(), any::<u32>())
.prop_map(|(session_id, serial)| Pdu::SerialNotify { session_id, serial }),
(any::<u16>(), any::<u32>())
.prop_map(|(session_id, serial)| Pdu::SerialQuery { session_id, serial }),
Just(Pdu::ResetQuery),
any::<u16>().prop_map(|session_id| Pdu::CacheResponse { session_id }),
(any::<bool>(), any_prefix()).prop_map(|(announce, vrp)| Pdu::Prefix { announce, vrp }),
(any::<u16>(), any::<u32>(), any::<[u32; 3]>()).prop_map(
|(session_id, serial, [refresh, retry, expire])| Pdu::EndOfData {
session_id,
serial,
timers: Some(Timers {
refresh,
retry,
expire
}),
}
),
Just(Pdu::CacheReset),
(any::<u16>(), "[ -~]{0,60}").prop_map(|(code, text)| Pdu::ErrorReport { code, text }),
]
}
proptest! {
#[test]
fn a_pdu_that_is_written_reads_back_equal(pdu in any_pdu(), version in 0u8..=2) {
let mut wire = BytesMut::new();
pdu.encode(version, &mut wire);
// Version 0 does not carry the timers.
let expected = match (&pdu, version) {
(Pdu::EndOfData { session_id, serial, .. }, VERSION_0) => Pdu::EndOfData {
session_id: *session_id,
serial: *serial,
timers: None,
},
_ => pdu.clone(),
};
prop_assert_eq!(Pdu::decode(&wire), Ok(Some((version, expected, wire.len()))));
}
#[test]
fn an_aspa_that_is_written_reads_back_equal(
customer: u32,
providers in proptest::collection::vec(any::<u32>(), 0..40),
) {
let pdu = Pdu::Aspa { announce: !providers.is_empty(), customer, providers };
let mut wire = BytesMut::new();
pdu.encode(VERSION_2, &mut wire);
prop_assert_eq!(Pdu::decode(&wire), Ok(Some((VERSION_2, pdu, wire.len()))));
}
/// Whatever arrives, decoding returns: a PDU, a wait for more, or
/// an error. What it reads is within what arrived.
#[test]
fn noise_is_never_more_than_an_error(bytes in proptest::collection::vec(any::<u8>(), 0..80)) {
if let Ok(Some((_, _, used))) = Pdu::decode(&bytes) {
prop_assert!(used <= bytes.len());
}
}
/// Noise behind a header that could be a PDU's, to get past it.
#[test]
fn noise_behind_a_plausible_header_is_never_more_than_an_error(
version in 0u8..=2,
pdu_type in 0u8..13,
field: u16,
length in 8u32..64,
body in proptest::collection::vec(any::<u8>(), 0..64),
) {
let mut bytes = vec![version, pdu_type];
bytes.extend_from_slice(&field.to_be_bytes());
bytes.extend_from_slice(&length.to_be_bytes());
bytes.extend_from_slice(&body);
if let Ok(Some((_, _, used))) = Pdu::decode(&bytes) {
prop_assert!(used <= bytes.len());
}
}
}
}