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>
This commit is contained in:
Rayhaan JaufeerallyandClaude Fable 5.1 committed 2026-10-02 14:12:59 +02:00
1 parent bdaf226a16
commit 8086aa994f
35 files changed
+2484 -359

No files matched your search

+1
View File
@@ -10,6 +10,7 @@ version.workspace = true
[dependencies]
bgp-primitives.workspace = true
bytes.workspace = true
foldhash.workspace = true
thiserror.workspace = true
tokio = { workspace = true, features = ["full"] }
tracing.workspace = true
+426
View File
@@ -0,0 +1,426 @@
//! Autonomous System Provider Authorisations, and what they say about a
//! route's AS path (draft-ietf-sidrops-aspa-verification).
//!
//! An ASPA is a signed statement by an AS of who its providers are: the
//! networks it buys transit from. With those statements a router can tell
//! a path that goes the way traffic is paid to go (up from the origin
//! through providers, across at most one peering, and down through
//! customers) from one that does not. A path that climbs again after it
//! has come down, or crosses a second peering, is a route leak or a forged
//! path, and this is the check that finds them.
use std::collections::HashMap;
use std::sync::Arc;
/// What the ASPAs say about a path.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PathValidity {
/// Some AS on the path has no ASPA, and nothing on the path is known
/// to be wrong. Most paths are in this state while few networks have
/// an ASPA, and are used.
Unknown,
/// Every hop that has to be from a customer to its provider is
/// attested to be one.
Valid,
/// Some AS on the path has said who its providers are, and the path
/// went from it to an AS that is not one of them where only a provider
/// could come next.
Invalid,
}
impl PathValidity {
pub fn as_str(self) -> &'static str {
match self {
Self::Unknown => "unknown",
Self::Valid => "valid",
Self::Invalid => "invalid",
}
}
}
/// Which of the two procedures verifies a path. That depends on what the
/// neighbor it came from is to us.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Algorithm {
/// For a route from a customer, a peer, or across a route server: all
/// of its path must lead upwards, from each AS to a provider of it
/// (§6.1 of the draft).
Upstream,
/// For a route from a provider: its path may lead up from the origin
/// and then down to us, and nothing else (§6.2).
Downstream,
}
/// Whether one AS is a provider of another, as far as the ASPAs say.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Hop {
/// The customer has no ASPA.
NoAttestation,
/// The customer's ASPA lists the other AS.
Provider,
/// The customer has an ASPA, and the other AS is not on it.
NotProvider,
}
/// The ASPAs there are: for each customer AS that has one, its providers.
///
/// Cloning copies the list of customers and shares the lists of providers.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AspaTable {
providers: HashMap<u32, Arc<[u32]>, foldhash::fast::FixedState>,
}
impl AspaTable {
/// How many customer ASes have an ASPA.
pub fn len(&self) -> usize {
self.providers.len()
}
pub fn is_empty(&self) -> bool {
self.providers.is_empty()
}
/// Records that `customer`'s providers are `providers`, in place of
/// what was recorded for it before, which is returned.
pub fn set(
&mut self,
customer: u32,
providers: impl IntoIterator<Item = u32>,
) -> Option<Arc<[u32]>> {
let mut providers: Vec<u32> = providers.into_iter().collect();
providers.sort_unstable();
providers.dedup();
self.providers.insert(customer, providers.into())
}
/// Forgets `customer`'s ASPA, and returns the providers it listed.
pub fn remove(&mut self, customer: u32) -> Option<Arc<[u32]>> {
self.providers.remove(&customer)
}
/// Puts back what [`AspaTable::set`] or [`AspaTable::remove`] returned.
pub(crate) fn restore(&mut self, customer: u32, providers: Option<Arc<[u32]>>) {
match providers {
Some(providers) => self.providers.insert(customer, providers),
None => self.providers.remove(&customer),
};
}
/// The providers `customer` lists, in increasing order, if it has an
/// ASPA.
pub fn providers(&self, customer: u32) -> Option<&[u32]> {
self.providers.get(&customer).map(|providers| &**providers)
}
/// The customers and their providers, in no particular order.
pub fn iter(&self) -> impl Iterator<Item = (u32, &[u32])> {
self.providers
.iter()
.map(|(customer, providers)| (*customer, &**providers))
}
fn hop(&self, customer: u32, provider: u32) -> Hop {
match self.providers.get(&customer) {
None => Hop::NoAttestation,
Some(providers) if providers.binary_search(&provider).is_ok() => Hop::Provider,
Some(_) => Hop::NotProvider,
}
}
/// Verifies `path`: the AS numbers of an AS_PATH in the order they are
/// in it, the neighbor's first and the origin's last, with whatever
/// repeats it has.
///
/// This is the part of the draft's procedures that the ASPAs decide.
/// What comes before it there is the caller's to check: that the path
/// has no AS_SET and begins with the neighbor's AS. An empty path is
/// invalid.
pub fn verify(&self, path: &[u32], algorithm: Algorithm) -> PathValidity {
// The draft numbers the path's distinct ASes from the origin,
// A(1), to the neighbor, A(N). Here they are `hops[0]` to
// `hops[n - 1]`.
let mut hops: Vec<u32> = Vec::with_capacity(path.len());
for asn in path.iter().rev() {
if hops.last() != Some(asn) {
hops.push(*asn);
}
}
let n = hops.len();
if n == 0 {
return PathValidity::Invalid;
}
// How far up from the origin the path can be a chain of customers
// and their providers at most (as far as nothing says otherwise),
// and at least (as far as it is attested).
let (mut max_up, mut min_up) = (n, n);
for (at, pair) in hops.windows(2).enumerate() {
match self.hop(pair[0], pair[1]) {
Hop::Provider => continue,
Hop::NoAttestation => min_up = min_up.min(at + 1),
Hop::NotProvider => {
min_up = min_up.min(at + 1);
max_up = at + 1;
break;
}
}
}
if algorithm == Algorithm::Upstream {
return match (max_up < n, min_up < n) {
(true, _) => PathValidity::Invalid,
(false, true) => PathValidity::Unknown,
(false, false) => PathValidity::Valid,
};
}
// The same from the neighbor's end, looking down: each AS is to be
// the customer of the one after it.
let (mut max_down, mut min_down) = (n, n);
for (at, pair) in hops.windows(2).rev().enumerate() {
match self.hop(pair[1], pair[0]) {
Hop::Provider => continue,
Hop::NoAttestation => min_down = min_down.min(at + 1),
Hop::NotProvider => {
min_down = min_down.min(at + 1);
max_down = at + 1;
break;
}
}
}
// The way up and the way down have to meet, or overlap.
if max_up + max_down < n {
PathValidity::Invalid
} else if min_up + min_down < n {
PathValidity::Unknown
} else {
PathValidity::Valid
}
}
}
impl FromIterator<(u32, Vec<u32>)> for AspaTable {
fn from_iter<T: IntoIterator<Item = (u32, Vec<u32>)>>(aspas: T) -> Self {
let mut table = Self::default();
for (customer, providers) in aspas {
table.set(customer, providers);
}
table
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::Algorithm::{Downstream, Upstream};
use super::PathValidity::{Invalid, Unknown, Valid};
use super::*;
fn table(aspas: &[(u32, &[u32])]) -> AspaTable {
aspas
.iter()
.map(|(customer, providers)| (*customer, providers.to_vec()))
.collect()
}
/// A small Internet. 10 and 20 are the large networks, which peer and
/// have no providers (an ASPA that lists AS 0). 100 buys from 10, 200
/// from 20, 300 from both. 1000 buys from 100; 2000 from 200 and has
/// no ASPA.
fn internet() -> AspaTable {
table(&[
(10, &[0]),
(20, &[0]),
(100, &[10]),
(200, &[20]),
(300, &[10, 20]),
(1000, &[100]),
])
}
#[test]
fn a_path_that_only_climbs_is_valid_from_a_customer_or_peer() {
let aspas = internet();
// 1000 -> 100 -> 10, as 10 hears it from its customer 100.
assert_eq!(aspas.verify(&[100, 1000], Upstream), Valid);
// As 20 hears it from its peer 10.
assert_eq!(aspas.verify(&[10, 100, 1000], Upstream), Valid);
// A route the neighbor originates is as far up as it has got.
assert_eq!(aspas.verify(&[1000], Upstream), Valid);
// Prepending changes nothing.
assert_eq!(
aspas.verify(&[10, 10, 100, 100, 100, 1000, 1000], Upstream),
Valid
);
}
#[test]
fn a_leak_is_invalid_from_a_customer_or_peer() {
let aspas = internet();
// 300 buys from 10 and 20, and passes 10's routes on to 20: the
// classic leak. 20 hears the path 300 10 100 1000 from its
// customer; 10 says it has no providers, so 300 is not one.
assert_eq!(aspas.verify(&[300, 10, 100, 1000], Upstream), Invalid);
// 20 passes a route of its peer 10's on to another peer: 20 is not
// a provider of 10.
assert_eq!(aspas.verify(&[20, 10, 100, 1000], Upstream), Invalid);
// An origin that is not where the path says: 1000 lists only 100.
assert_eq!(aspas.verify(&[200, 1000], Upstream), Invalid);
}
#[test]
fn a_hop_nobody_has_attested_leaves_the_path_unknown() {
let aspas = internet();
// 2000 has no ASPA.
assert_eq!(aspas.verify(&[200, 2000], Upstream), Unknown);
assert_eq!(aspas.verify(&[20, 200, 2000], Upstream), Unknown);
// But what is known to be wrong is invalid whatever else is not
// known.
assert_eq!(aspas.verify(&[300, 20, 200, 2000], Upstream), Invalid);
// With no ASPAs at all, nothing is known.
assert_eq!(AspaTable::default().verify(&[1, 2, 3], Upstream), Unknown);
assert_eq!(AspaTable::default().verify(&[1, 2, 3], Downstream), Unknown);
// Except of the path that is no path.
assert_eq!(AspaTable::default().verify(&[], Upstream), Invalid);
assert_eq!(AspaTable::default().verify(&[], Downstream), Invalid);
}
#[test]
fn from_a_provider_a_path_may_climb_and_then_descend() {
let aspas = internet();
// 1000's route, as 200 hears it from its provider 20: up through
// 100 to 10, across to 20.
assert_eq!(aspas.verify(&[20, 10, 100, 1000], Downstream), Valid);
// As 300 hears it from 10: only up.
assert_eq!(aspas.verify(&[10, 100, 1000], Downstream), Valid);
// As 1000 would hear 300's route from 100: up to 10, down to 100.
assert_eq!(aspas.verify(&[100, 10, 300], Downstream), Valid);
// Short paths have no room to be wrong in.
assert_eq!(aspas.verify(&[20, 1000], Downstream), Valid);
assert_eq!(aspas.verify(&[20], Downstream), Valid);
}
#[test]
fn from_a_provider_a_path_that_descends_and_climbs_again_is_invalid() {
let aspas = internet();
// 300 leaks 10's routes to 20, and 20 passes them down to 200,
// which hears the path 20 300 10 100 1000. The way up from 1000
// ends at 10, three ASes in: 300 is not a provider of 10. The way
// down to 200 begins no earlier than 20, one AS: 20 has no
// providers, so it did not get the route from one. Three and one
// do not make five.
assert_eq!(aspas.verify(&[20, 300, 10, 100, 1000], Downstream), Invalid);
// Two peerings in a row: 30 is another large network, which hands
// its customer a route it got from its peer 20, which got it from
// its peer 10.
let mut aspas = internet();
aspas.set(30, [0]);
assert_eq!(aspas.verify(&[30, 20, 10, 100, 1000], Downstream), Invalid);
}
#[test]
fn from_a_provider_what_is_not_attested_leaves_the_path_unknown() {
let aspas = internet();
// 2000 (no ASPA) up to 200, to 20, across to 10, down to 100: the
// first hop is not attested either way.
assert_eq!(aspas.verify(&[10, 20, 200, 2000], Downstream), Unknown);
// With 2000's ASPA it is valid.
let mut with = aspas.clone();
with.set(2000, [200]);
assert_eq!(with.verify(&[10, 20, 200, 2000], Downstream), Valid);
}
#[test]
fn an_aspa_is_replaced_and_removed_whole() {
let mut aspas = internet();
assert_eq!(aspas.providers(300), Some(&[10, 20][..]));
assert_eq!(
aspas.set(300, [20, 10, 20, 5]).as_deref(),
Some(&[10, 20][..])
);
assert_eq!(aspas.providers(300), Some(&[5, 10, 20][..]));
assert_eq!(aspas.remove(300).as_deref(), Some(&[5, 10, 20][..]));
assert_eq!(aspas.providers(300), None);
assert_eq!(aspas.remove(300), None);
assert_eq!(aspas.len(), 5);
}
/// The procedures as the draft words them, with its numbering: A(1) is
/// the origin, A(N) the neighbor.
fn by_the_draft(aspas: &AspaTable, path: &[u32], algorithm: Algorithm) -> PathValidity {
let mut a: Vec<u32> = path.iter().rev().copied().collect();
a.dedup();
let n = a.len();
if n == 0 {
return Invalid;
}
// One-based, as written.
let hop = |i: usize, j: usize| aspas.hop(a[i - 1], a[j - 1]);
let first_up = |wanted: &[Hop]| {
(1..n)
.find(|i| wanted.contains(&hop(*i, *i + 1)))
.unwrap_or(n)
};
let last_down = |wanted: &[Hop]| {
(2..=n)
.rev()
.find(|j| wanted.contains(&hop(*j, *j - 1)))
.map_or(n, |j| n - j + 1)
};
let not = [Hop::NotProvider];
let not_attested = [Hop::NoAttestation, Hop::NotProvider];
let (max_up, min_up) = (first_up(&not), first_up(&not_attested));
match algorithm {
Upstream if max_up < n => Invalid,
Upstream if min_up < n => Unknown,
Upstream => Valid,
Downstream => {
let (max_down, min_down) = (last_down(&not), last_down(&not_attested));
if max_up + max_down < n {
Invalid
} else if min_up + min_down < n {
Unknown
} else {
Valid
}
}
}
}
proptest! {
/// Over a handful of AS numbers, so that paths and ASPAs meet.
#[test]
fn verification_agrees_with_the_draft_as_written(
aspas in proptest::collection::vec(
(0u32..8, proptest::collection::vec(0u32..8, 1..4)), 0..8),
path in proptest::collection::vec(0u32..8, 0..10),
downstream: bool,
) {
let aspas: AspaTable = aspas.into_iter().collect();
let algorithm = if downstream { Downstream } else { Upstream };
prop_assert_eq!(
aspas.verify(&path, algorithm),
by_the_draft(&aspas, &path, algorithm)
);
}
/// What is invalid from a provider is invalid from anyone, and
/// what is valid from a customer is valid from anyone: the
/// procedure for providers allows more.
#[test]
fn the_procedure_for_providers_is_the_more_lenient(
aspas in proptest::collection::vec(
(0u32..8, proptest::collection::vec(0u32..8, 1..4)), 0..8),
path in proptest::collection::vec(0u32..8, 1..10),
) {
let aspas: AspaTable = aspas.into_iter().collect();
let (up, down) = (aspas.verify(&path, Upstream), aspas.verify(&path, Downstream));
if down == Invalid {
prop_assert_eq!(up, Invalid);
}
if up == Valid {
prop_assert_eq!(down, Valid);
}
}
}
}
+339 -120
View File
@@ -1,11 +1,14 @@
//! Keeps the list of VRPs in step with a validator, over the RPKI-to-Router
//! protocol (RFC 8210).
//! Keeps the VRPs and ASPAs in step with a validator, over the
//! RPKI-to-Router protocol (RFC 8210 and draft-ietf-sidrops-8210bis).
//!
//! The client connects to a cache (the validator's RTR server), asks for
//! everything once, and from then on for what has changed: when the cache
//! says there is something new, and otherwise every so often. Each time the
//! data changes a new [`VrpTable`] is published for whoever validates
//! routes.
//! data changes an [`Update`] is published for whoever validates routes.
//!
//! The newest version of the protocol is asked for first, and an older one
//! if that is what the cache speaks. Only version 2 carries ASPAs: with an
//! older cache there are none, and paths are not verified.
//!
//! A cache that cannot be reached is tried again, and the other caches in
//! turn. Meanwhile the data last received is used, but not for ever: a list
@@ -30,7 +33,8 @@ use tokio::task::JoinHandle;
use tokio::time::{Instant, sleep_until, timeout};
use tracing::{info, warn};
use crate::rtr::{Pdu, RtrError, Timers, VERSION_0, VERSION_1, error_code};
use crate::aspa::AspaTable;
use crate::rtr::{Pdu, RtrError, Timers, VERSION_0, VERSION_2, error_code};
use crate::vrp::{Vrp, VrpTable};
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
@@ -41,6 +45,9 @@ const RESPONSE_TIMEOUT: Duration = Duration::from_secs(60);
/// How long to wait before trying the next cache after one has failed.
const NEXT_CACHE_DELAY: Duration = Duration::from_secs(1);
/// How long a cache that is being left is given to take the reason.
const ERROR_REPORT_TIMEOUT: Duration = Duration::from_secs(1);
/// Where the VRPs come from, and how patiently.
#[derive(Debug, Clone, Default)]
pub struct RpkiConfig {
@@ -54,16 +61,28 @@ pub struct RpkiConfig {
pub expire: Option<Duration>,
}
/// The VRPs as they are after a change.
/// The VRPs and ASPAs as they are after a change.
#[derive(Debug, Clone)]
pub struct Update {
/// Goes up by one with every update.
pub version: u64,
pub table: Arc<VrpTable>,
/// The VRPs that were added or removed to get here from the version
/// before: only routes that these cover can have changed validity.
/// `None` if any route may have.
pub changed: Option<Arc<VrpTable>>,
pub vrps: Arc<VrpTable>,
pub aspas: Arc<AspaTable>,
/// What was added or removed to get here from the version before.
/// `None` if any route's validity may have changed.
pub changed: Option<Changed>,
}
/// What differs between one [`Update`] and the next.
#[derive(Debug, Clone)]
pub struct Changed {
/// The VRPs that came or went: only routes that these cover can have
/// another origin validity than before.
pub vrps: Arc<VrpTable>,
/// The customer ASes whose ASPA came, went or lists other providers:
/// only paths with one of these on them can verify otherwise than
/// before.
pub aspa_customers: Arc<HashSet<u32>>,
}
/// How things stand with the caches.
@@ -89,6 +108,9 @@ pub struct Status {
pub protocol_version: u8,
pub vrps_ipv4: usize,
pub vrps_ipv6: usize,
/// How many ASes have an ASPA. None do if the cache speaks a version
/// of the protocol before 2.
pub aspas: usize,
/// When the data was last confirmed by a cache.
pub updated_at: Option<SystemTime>,
/// When it will be dropped unless confirmed again.
@@ -112,12 +134,15 @@ pub struct Rpki {
impl Rpki {
/// Starts a client for `config` and returns the handle to its data,
/// with the task that runs it. Until the first cache has answered the
/// table is empty, and every route is [`crate::Validity::NotFound`].
/// with the task that runs it. Until the first cache has answered
/// there are no VRPs and no ASPAs: every route's origin is
/// [`crate::Validity::NotFound`] and every path
/// [`crate::PathValidity::Unknown`].
pub fn spawn(config: RpkiConfig) -> (Self, JoinHandle<()>) {
let (updates, _) = watch::channel(Update {
version: 0,
table: Arc::default(),
vrps: Arc::default(),
aspas: Arc::default(),
changed: None,
});
let rpki = Self {
@@ -129,9 +154,10 @@ impl Rpki {
state: CacheState::Connecting,
session_id: None,
serial: None,
protocol_version: VERSION_1,
protocol_version: VERSION_2,
vrps_ipv4: 0,
vrps_ipv6: 0,
aspas: 0,
updated_at: None,
expires_at: None,
last_error: None,
@@ -141,16 +167,16 @@ impl Rpki {
let client = Client {
shared: Arc::clone(&rpki.shared),
config,
vrps: HashSet::new(),
data: Data::default(),
place: None,
protocol_version: VERSION_1,
protocol_version: VERSION_2,
timers: Timers::default(),
expires: None,
};
(rpki, tokio::spawn(client.run()))
}
/// The VRPs as they are now.
/// The VRPs and ASPAs as they are now.
pub fn current(&self) -> Update {
self.shared.updates.borrow().clone()
}
@@ -192,33 +218,114 @@ enum SessionError {
#[error("the cache sent {0} where it has no place")]
Unexpected(&'static str),
#[error("the cache {0}")]
Inconsistent(&'static str),
/// Not a failure of the cache: it speaks the older version, and is to
Inconsistent(Inconsistency),
/// Not a failure of the cache: it speaks an older version, and is to
/// be asked again in that.
#[error("the cache speaks protocol version 0")]
Downgrade,
#[error("the cache speaks protocol version {0}")]
Downgrade(u8),
}
impl SessionError {
/// The error code that tells the cache why it is being left, if that
/// is something it did and can be told (RFC 8210 §10). An Error Report
/// is never answered with another.
fn report_code(&self) -> Option<u16> {
match self {
Self::Malformed(e) => Some(e.code()),
Self::Unexpected(_) => Some(error_code::CORRUPT_DATA),
Self::Inconsistent(problem) => Some(problem.code),
Self::Io(_) | Self::Closed | Self::Timeout | Self::Reported { .. } => None,
Self::Downgrade(_) => None,
}
}
}
/// Something a cache said that does not fit with what it said before.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Inconsistency {
what: &'static str,
/// The error code it is reported to the cache with.
code: u16,
}
impl std::fmt::Display for Inconsistency {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.what)
}
}
const fn inconsistent(what: &'static str, code: u16) -> Inconsistency {
Inconsistency { what, code }
}
const DUPLICATE_VRP: Inconsistency =
inconsistent("announced a VRP twice", error_code::DUPLICATE_ANNOUNCEMENT);
const UNKNOWN_VRP: Inconsistency = inconsistent(
"withdrew a VRP it had not announced",
error_code::WITHDRAWAL_OF_UNKNOWN_RECORD,
);
const UNKNOWN_ASPA: Inconsistency = inconsistent(
"withdrew an ASPA it had not announced",
error_code::WITHDRAWAL_OF_UNKNOWN_RECORD,
);
const OTHER_SESSION: Inconsistency = inconsistent(
"ended an answer in another session than it began it",
error_code::CORRUPT_DATA,
);
const OTHER_VERSION: Inconsistency = inconsistent(
"changed protocol version in the middle of a session",
error_code::UNEXPECTED_VERSION,
);
const INTERNAL: Inconsistency = inconsistent(
"could not be followed (a bug here)",
error_code::INTERNAL_ERROR,
);
/// Where the data is at: which of the cache's sessions, and which serial.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Place {
cache: SocketAddr,
/// The version of the protocol the data came in. What another version
/// carries is not the same data.
protocol_version: u8,
session_id: u16,
serial: u32,
}
/// What a cache has given: the data validation rests on.
#[derive(Debug, Clone, Default)]
struct Data {
vrps: HashSet<Vrp>,
aspas: AspaTable,
}
/// One thing an answer says: a VRP or an ASPA, come or gone.
#[derive(Debug, Clone)]
enum Record {
Vrp {
announce: bool,
vrp: Vrp,
},
/// With providers, the ASPA that is now `customer`'s; without, that
/// it has none any more.
Aspa {
customer: u32,
providers: Option<Vec<u32>>,
},
}
struct Client {
shared: Arc<Shared>,
config: RpkiConfig,
/// The VRPs as last received.
vrps: HashSet<Vrp>,
/// Where `vrps` is at, if it came from a cache that can be asked for
/// The data as last received.
data: Data,
/// Where `data` is at, if it came from a cache that can be asked for
/// what has changed since.
place: Option<Place>,
protocol_version: u8,
/// As the cache last gave them, or the defaults.
timers: Timers,
/// When `vrps` is to be dropped unless a cache confirms it first.
/// When `data` is to be dropped unless a cache confirms it first.
expires: Option<Instant>,
}
@@ -291,9 +398,9 @@ impl Client {
status.state = CacheState::Connecting;
});
let ended = self.session(cache).await;
if matches!(ended, SessionError::Downgrade) {
info!(%cache, "RPKI cache speaks RTR version 0; asking again in that");
self.protocol_version = VERSION_0;
if let SessionError::Downgrade(version) = ended {
info!(%cache, version, "RPKI cache speaks an older RTR version; asking again in that");
self.protocol_version = version;
continue;
}
warn!(%cache, err = %ended, "RPKI cache session ended");
@@ -304,8 +411,9 @@ impl Client {
// The next cache at once; when all have been tried, a rest.
attempt += 1;
// What is new may be at a newer version.
self.protocol_version = VERSION_1;
// The next cache, or this one when it is back, may speak a
// newer version.
self.protocol_version = VERSION_2;
let wait = if attempt.is_multiple_of(self.config.caches.len()) {
self.retry()
} else {
@@ -331,10 +439,11 @@ impl Client {
/// The data has gone unconfirmed for too long to be used.
fn expired(&mut self) {
warn!(
vrps = self.vrps.len(),
vrps = self.data.vrps.len(),
aspas = self.data.aspas.len(),
"RPKI data has expired without a cache to confirm it; dropping it"
);
self.vrps.clear();
self.data = Data::default();
self.place = None;
self.expires = None;
self.publish(Arc::default(), None);
@@ -365,7 +474,7 @@ impl Client {
loop {
if let Err(e) = self.synchronise(cache, &mut connection).await {
return e;
return self.leave(&mut connection, e).await;
}
// Until the cache says there is something new, or it is time
// to ask anyway. If the data runs out first, it is the cache
@@ -373,14 +482,20 @@ impl Client {
let ask = Instant::now() + self.refresh();
let expires = self.expires.unwrap_or(ask);
tokio::select! {
pdu = connection.next() => match pdu {
Ok((_, Pdu::SerialNotify { .. })) => {}
Ok((_, Pdu::ErrorReport { code, text })) => {
return SessionError::Reported { code, text };
}
Ok(_) => return SessionError::Unexpected("an answer nobody asked for"),
Err(e) => return e,
},
pdu = connection.next() => {
let problem = match pdu {
Ok((_, Pdu::ErrorReport { code, text })) => {
SessionError::Reported { code, text }
}
Ok((version, _)) if version != self.protocol_version => {
SessionError::Inconsistent(OTHER_VERSION)
}
Ok((_, Pdu::SerialNotify { .. })) => continue,
Ok(_) => SessionError::Unexpected("an answer nobody asked for"),
Err(e) => e,
};
return self.leave(&mut connection, problem).await;
}
() = sleep_until(ask) => {}
() = sleep_until(expires), if expires < ask => {
self.expired();
@@ -390,6 +505,24 @@ impl Client {
}
}
/// Tells the cache why the session is ending, if it is for something
/// the cache did, and gives the reason back.
async fn leave(&self, connection: &mut Connection, why: SessionError) -> SessionError {
if let Some(code) = why.report_code() {
let report = Pdu::ErrorReport {
code,
text: why.to_string(),
};
// If it cannot be told, it cannot.
let _ = timeout(
ERROR_REPORT_TIMEOUT,
connection.send(self.protocol_version, &report),
)
.await;
}
why
}
/// Asks the cache for what has changed, or for everything, and takes
/// the answer.
async fn synchronise(
@@ -397,8 +530,11 @@ impl Client {
cache: SocketAddr,
connection: &mut Connection,
) -> Result<(), SessionError> {
// Changes can be asked for only of the cache the data came from.
let mut since = self.place.filter(|place| place.cache == cache);
// Changes can be asked for only of the cache the data came from,
// in the version of the protocol it came in.
let mut since = self.place.filter(|place| {
place.cache == cache && place.protocol_version == self.protocol_version
});
loop {
let query = match since {
Some(place) => Pdu::SerialQuery {
@@ -433,20 +569,26 @@ impl Client {
let session_id = loop {
let (version, pdu) = next().await?;
if version < self.protocol_version {
// An older cache answers in its own version; the query has
// to be put again in that (RFC 8210 §7).
return Err(SessionError::Downgrade);
// An older cache answers in its own version, with an error
// or as if it had been asked in that; the query has to be
// put again in that version (RFC 8210 §7).
return Err(SessionError::Downgrade(version));
}
if version > self.protocol_version {
return Err(SessionError::Inconsistent(OTHER_VERSION));
}
match pdu {
Pdu::CacheResponse { session_id } => break session_id,
Pdu::CacheReset if since.is_some() => return Ok(Answer::Reset),
// News of what is being asked for anyway.
Pdu::SerialNotify { .. } => {}
// A cache that refuses the version in the version it
// refuses: the next one down, then.
Pdu::ErrorReport { code, .. }
if code == error_code::UNSUPPORTED_VERSION
&& self.protocol_version != VERSION_0 =>
{
return Err(SessionError::Downgrade);
return Err(SessionError::Downgrade(self.protocol_version - 1));
}
Pdu::ErrorReport { code, text } => {
return Err(SessionError::Reported { code, text });
@@ -460,13 +602,25 @@ impl Client {
return Ok(Answer::Reset);
}
// Then the VRPs, and End of Data. Nothing takes effect until the
// answer is complete (RFC 8210 §8.1), so what it says is kept
// aside in the order it came.
let mut records: Vec<(Vrp, bool)> = Vec::new();
// Then the VRPs and ASPAs, and End of Data. Nothing takes effect
// until the answer is complete (RFC 8210 §8.1), so what it says is
// kept aside in the order it came.
let mut records: Vec<Record> = Vec::new();
let (serial, timers) = loop {
match next().await?.1 {
Pdu::Prefix { announce, vrp } => records.push((vrp, announce)),
let (version, pdu) = next().await?;
if version != self.protocol_version && !matches!(pdu, Pdu::ErrorReport { .. }) {
return Err(SessionError::Inconsistent(OTHER_VERSION));
}
match pdu {
Pdu::Prefix { announce, vrp } => records.push(Record::Vrp { announce, vrp }),
Pdu::Aspa {
announce,
customer,
providers,
} => records.push(Record::Aspa {
customer,
providers: announce.then_some(providers),
}),
Pdu::RouterKey => {}
Pdu::EndOfData {
session_id: ended,
@@ -474,9 +628,7 @@ impl Client {
timers,
} => {
if ended != session_id {
return Err(SessionError::Inconsistent(
"ended an answer in another session than it began it",
));
return Err(SessionError::Inconsistent(OTHER_SESSION));
}
break (serial, timers);
}
@@ -492,47 +644,47 @@ impl Client {
};
// The first data there is can change any route's validity; after
// that, only where a VRP came or went.
// that, only where something came or went.
let first = self.place.is_none() && self.expires.is_none();
let before = match since {
let (before, replaced) = match since {
// Changes, to what there is.
Some(_) => std::mem::take(&mut self.vrps),
Some(_) => (std::mem::take(&mut self.data), None),
// Everything anew.
None => HashSet::new(),
};
let replaced = match since {
Some(_) => None,
None => Some(std::mem::take(&mut self.vrps)),
None => (Data::default(), Some(std::mem::take(&mut self.data))),
};
let vrps = Arc::clone(&self.shared.updates.borrow().vrps);
// Building the tables is work for a thread that may take its time.
let built = tokio::task::spawn_blocking(move || apply(before, replaced, &records, first))
let built = tokio::task::spawn_blocking(move || apply(before, replaced, &records, vrps))
.await
.map_err(|_| SessionError::Inconsistent("could not be followed (a bug here)"))?;
.map_err(|_| SessionError::Inconsistent(INTERNAL))?;
let built = match built {
Ok(built) => built,
Err((vrps, problem)) => {
Err((data, problem)) => {
// What there was stays as it was.
self.vrps = vrps;
self.data = data;
return Err(SessionError::Inconsistent(problem));
}
};
self.vrps = built.vrps;
self.data = built.data;
self.place = Some(Place {
cache,
protocol_version: self.protocol_version,
session_id,
serial,
});
self.timers = timers.unwrap_or_default();
self.expires = Some(Instant::now() + self.expire());
if first || built.changes > 0 {
self.publish(built.table, built.changed);
let changes = built.changed.vrps.len() + built.changed.aspa_customers.len();
if first || changes > 0 {
self.publish(built.vrps, (!first).then_some(built.changed));
}
info!(
%cache,
serial,
vrps = self.vrps.len(),
changed = built.changes,
vrps = self.data.vrps.len(),
aspas = self.data.aspas.len(),
changes,
"RPKI data is up to date"
);
let expires_at = SystemTime::now() + self.expire();
@@ -547,11 +699,11 @@ impl Client {
Ok(Answer::Complete)
}
/// Makes `table`, which holds the VRPs as they now are, the one in
/// use.
fn publish(&self, table: Arc<VrpTable>, changed: Option<Arc<VrpTable>>) {
/// Publishes the data as it now is. `vrps` holds its VRPs.
fn publish(&self, vrps: Arc<VrpTable>, changed: Option<Changed>) {
let count = |version| {
self.vrps
self.data
.vrps
.iter()
.filter(|vrp| vrp.prefix.address_family() == version)
.count()
@@ -560,84 +712,151 @@ impl Client {
self.status(|status| {
status.vrps_ipv4 = ipv4;
status.vrps_ipv6 = ipv6;
status.aspas = self.data.aspas.len();
});
let aspas = Arc::new(self.data.aspas.clone());
self.shared.updates.send_modify(|update| {
update.version += 1;
update.table = table;
update.vrps = vrps;
update.aspas = aspas;
update.changed = changed;
self.shared.version.store(update.version, Ordering::Release);
});
}
}
/// The VRPs after an answer, ready to be used.
/// The data after an answer, ready to be used.
struct Built {
vrps: HashSet<Vrp>,
table: Arc<VrpTable>,
/// See [`Update::changed`].
changed: Option<Arc<VrpTable>>,
/// How many VRPs came or went.
changes: usize,
data: Data,
/// The VRPs of `data`, arranged for validating.
vrps: Arc<VrpTable>,
changed: Changed,
}
/// Applies the `records` of a complete answer, in order, to `vrps`: each
/// a VRP and whether it is announced or withdrawn. `replaced` is the set
/// that an answer to a Reset Query takes the place of.
/// Applies the `records` of a complete answer, in order, to `data`.
/// `replaced` is the data that an answer to a Reset Query takes the place
/// of, and `vrps` the table of the VRPs as they were before the answer,
/// which is kept if the answer changes none.
///
/// If the answer contradicts itself or what there was, what there was is
/// given back with the reason.
fn apply(
mut vrps: HashSet<Vrp>,
replaced: Option<HashSet<Vrp>>,
records: &[(Vrp, bool)],
first: bool,
) -> Result<Built, (HashSet<Vrp>, &'static str)> {
mut data: Data,
replaced: Option<Data>,
records: &[Record],
vrps: Arc<VrpTable>,
) -> Result<Built, (Data, Inconsistency)> {
// What each record displaced, to put back if a later one cannot be
// applied.
let mut displaced: Vec<Option<Arc<[u32]>>> = Vec::new();
let mut problem = None;
let mut applied = 0;
for (vrp, announce) in records {
let ok = match announce {
true => vrps.insert(*vrp),
false => vrps.remove(vrp),
for record in records {
let ok = match record {
Record::Vrp {
announce: true,
vrp,
} => data.vrps.insert(*vrp).then_some(None).ok_or(DUPLICATE_VRP),
Record::Vrp {
announce: false,
vrp,
} => data.vrps.remove(vrp).then_some(None).ok_or(UNKNOWN_VRP),
// An ASPA that is announced takes the place of the one there
// was (draft-ietf-sidrops-8210bis §5.12).
Record::Aspa {
customer,
providers: Some(providers),
} => Ok(data.aspas.set(*customer, providers.iter().copied())),
Record::Aspa {
customer,
providers: None,
} => data.aspas.remove(*customer).map(Some).ok_or(UNKNOWN_ASPA),
};
if !ok {
problem = Some(match announce {
true => "announced a VRP twice",
false => "withdrew a VRP it had not announced",
});
break;
match ok {
Ok(was) => displaced.push(was),
Err(e) => {
problem = Some(e);
break;
}
}
applied += 1;
}
if let Some(problem) = problem {
return Err(match replaced {
Some(replaced) => (replaced, problem),
None => {
// Undo, last first.
for (vrp, announce) in records[..applied].iter().rev() {
match announce {
true => vrps.remove(vrp),
false => vrps.insert(*vrp),
};
for (record, was) in records.iter().zip(displaced).rev() {
match record {
Record::Vrp {
announce: true,
vrp,
} => {
data.vrps.remove(vrp);
}
Record::Vrp {
announce: false,
vrp,
} => {
data.vrps.insert(*vrp);
}
Record::Aspa { customer, .. } => data.aspas.restore(*customer, was),
}
}
(vrps, problem)
(data, problem)
}
});
}
// What came or went: against the set replaced, or else whatever the
// What came or went: against the data replaced, or else whatever the
// answer touched.
let changed: Vec<Vrp> = match &replaced {
Some(replaced) => vrps.symmetric_difference(replaced).copied().collect(),
None => records.iter().map(|(vrp, _)| *vrp).collect(),
let (changed_vrps, aspa_customers): (Vec<Vrp>, HashSet<u32>) = match &replaced {
Some(replaced) => (
data.vrps
.symmetric_difference(&replaced.vrps)
.copied()
.collect(),
differing_customers(&data.aspas, &replaced.aspas),
),
None => (
records
.iter()
.filter_map(|record| match record {
Record::Vrp { vrp, .. } => Some(*vrp),
Record::Aspa { .. } => None,
})
.collect(),
records
.iter()
.filter_map(|record| match record {
Record::Aspa { customer, .. } => Some(*customer),
Record::Vrp { .. } => None,
})
.collect(),
),
};
Ok(Built {
table: Arc::new(VrpTable::new(vrps.iter().copied())),
changed: (!first).then(|| Arc::new(VrpTable::new(changed.iter().copied()))),
changes: changed.len(),
vrps,
vrps: match changed_vrps.is_empty() {
true => vrps,
false => Arc::new(VrpTable::new(data.vrps.iter().copied())),
},
changed: Changed {
vrps: Arc::new(VrpTable::new(changed_vrps)),
aspa_customers: Arc::new(aspa_customers),
},
data,
})
}
/// The customers whose ASPA is not the same in `a` as in `b`.
fn differing_customers(a: &AspaTable, b: &AspaTable) -> HashSet<u32> {
let differs = |(customer, providers): (u32, &[u32]), other: &AspaTable| {
(other.providers(customer) != Some(providers)).then_some(customer)
};
a.iter()
.filter_map(|aspa| differs(aspa, b))
.chain(b.iter().filter_map(|aspa| differs(aspa, a)))
.collect()
}
/// How a query was answered.
enum Answer {
/// With data, which is now in use.
+15 -4
View File
@@ -1,4 +1,5 @@
//! Route origin validation with the RPKI (RFC 6811).
//! Route origin validation (RFC 6811) and AS path verification
//! (draft-ietf-sidrops-aspa-verification) with the RPKI.
//!
//! The holder of an address block can sign a statement of which AS may
//! originate routes for it. Validators collect and check those statements
@@ -7,13 +8,23 @@
//! tell a route whose origin is authorised from one whose origin is not:
//! the commonest kind of hijack and of mistake.
//!
//! - [`vrp`] holds the list and answers whether a route is valid.
//!
//! The same way, a network can sign a statement of who its providers are
//! (an ASPA). With those a router can tell a path that could have come
//! about by networks passing routes on as they are paid to from one that
//! could not: a route leak, or a path made up to get past origin
//! validation.
//!
//! - [`vrp`] holds the VRPs and answers whether a route's origin is valid.
//! - [`aspa`] holds the ASPAs and answers whether a path is.
//! - [`rtr`] is the protocol's messages.
//! - [`client`] keeps the list in step with a validator.
//! - [`client`] keeps both in step with a validator.
pub mod aspa;
pub mod client;
pub mod rtr;
pub mod vrp;
pub use client::{CacheState, Rpki, RpkiConfig, Status, Update};
pub use aspa::{Algorithm, AspaTable, PathValidity};
pub use client::{CacheState, Changed, Rpki, RpkiConfig, Status, Update};
pub use vrp::{Validity, Vrp, VrpTable};
+138 -17
View File
@@ -1,5 +1,6 @@
//! The messages of the RPKI-to-Router protocol (RFC 8210, and RFC 6810 for
//! version 0), as bytes and back. No I/O here.
//! 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
@@ -11,17 +12,20 @@ use bytes::{BufMut, BytesMut};
use crate::vrp::Vrp;
/// The protocol version this client asks for. Version 1 added the timers in
/// End of Data and router keys.
/// 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 an older validator speaks.
/// 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 and Router
/// Key has a fixed, small length, and those two have no need to be large.
pub const MAX_PDU_LEN: usize = 16 * 1024;
/// 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;
@@ -34,9 +38,10 @@ mod pdu_type {
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).
/// 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;
@@ -47,6 +52,9 @@ pub mod error_code {
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
@@ -97,6 +105,14 @@ pub enum Pdu {
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 },
}
@@ -112,6 +128,8 @@ pub enum RtrError {
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 {
@@ -121,6 +139,7 @@ impl RtrError {
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,
}
}
}
@@ -141,12 +160,12 @@ impl Pdu {
let Some(header) = buf.first_chunk::<HEADER_LEN>() else {
return Ok(None);
};
let (version, pdu_type) = (header[0], header[1]);
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_1 {
if version > VERSION_2 {
return Err(RtrError::Version(version));
}
let total = usize::try_from(length).map_err(|_| bad_length())?;
@@ -162,6 +181,15 @@ impl Pdu {
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) {
@@ -215,6 +243,24 @@ impl Pdu {
},
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.
@@ -234,7 +280,8 @@ impl Pdu {
}
/// Writes the PDU, for a session that speaks protocol version
/// `version`, to the end of `out`.
/// `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| {
@@ -288,6 +335,17 @@ impl Pdu {
}
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.
@@ -387,6 +445,35 @@ mod tests {
}
)
);
// 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,
@@ -423,8 +510,8 @@ mod tests {
fn what_is_not_a_pdu_is_refused_from_its_header() {
// A version from the future.
assert_eq!(
Pdu::decode(&[2, 2, 0, 0, 0, 0, 0, 8]),
Err(RtrError::Version(2))
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]),
@@ -456,6 +543,29 @@ mod tests {
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];
@@ -508,7 +618,7 @@ mod tests {
proptest! {
#[test]
fn a_pdu_that_is_written_reads_back_equal(pdu in any_pdu(), version in 0u8..=1) {
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.
@@ -523,6 +633,17 @@ mod tests {
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]
@@ -535,8 +656,8 @@ mod tests {
/// 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..=1,
pdu_type in 0u8..12,
version in 0u8..=2,
pdu_type in 0u8..13,
field: u16,
length in 8u32..64,
body in proptest::collection::vec(any::<u8>(), 0..64),
+265 -39
View File
@@ -7,7 +7,7 @@ use std::time::Duration;
use bgp_primitives::ip_prefix::IpPrefix;
use bgp_rpki::rtr::{Pdu, Timers, error_code};
use bgp_rpki::{CacheState, Rpki, RpkiConfig, Update, Validity, Vrp};
use bgp_rpki::{Algorithm, CacheState, PathValidity, Rpki, RpkiConfig, Update, Validity, Vrp};
use bytes::BytesMut;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
@@ -15,6 +15,8 @@ use tokio::sync::watch;
const TIMEOUT: Duration = Duration::from_secs(5);
const SESSION: u16 = 0x1234;
/// The version of the protocol the client asks for first.
const V: u8 = 2;
fn prefix(s: &str) -> IpPrefix {
IpPrefix::try_from(s).unwrap()
@@ -120,7 +122,7 @@ impl Connection {
}];
pdus.extend(vrps.iter().copied().map(announce));
pdus.push(end_of_data(serial));
self.send(1, &pdus).await;
self.send(V, &pdus).await;
}
}
@@ -149,10 +151,10 @@ async fn the_client_takes_everything_once_and_then_the_changes() {
let (rpki, mut updates) = client_of(&[&cache]);
// Nothing is known until a cache has answered.
assert_eq!(rpki.version(), 0);
assert!(rpki.current().table.is_empty());
assert!(rpki.current().vrps.is_empty());
let mut connection = cache.accept().await;
assert_eq!(connection.received().await, (1, Pdu::ResetQuery));
assert_eq!(connection.received().await, (V, Pdu::ResetQuery));
let ripe = vrp("193.0.0.0/21", 24, 3333);
let other = vrp("2001:67c:2e8::/48", 48, 3333);
connection.send_all(&[ripe, other], 7).await;
@@ -164,13 +166,13 @@ async fn the_client_takes_everything_once_and_then_the_changes() {
update.changed.is_none(),
"the first data can change anything"
);
assert_eq!(update.table.len(), 2);
assert_eq!(update.vrps.len(), 2);
assert_eq!(
update.table.validate(&prefix("193.0.4.0/24"), Some(3333)),
update.vrps.validate(&prefix("193.0.4.0/24"), Some(3333)),
Validity::Valid
);
assert_eq!(
update.table.validate(&prefix("193.0.4.0/24"), Some(15169)),
update.vrps.validate(&prefix("193.0.4.0/24"), Some(15169)),
Validity::Invalid
);
let status = rpki.status();
@@ -182,7 +184,7 @@ async fn the_client_takes_everything_once_and_then_the_changes() {
// The cache has news: the client asks for what changed since serial 7.
connection
.send(
1,
V,
&[Pdu::SerialNotify {
session_id: SESSION,
serial: 8,
@@ -192,7 +194,7 @@ async fn the_client_takes_everything_once_and_then_the_changes() {
assert_eq!(
connection.received().await,
(
1,
V,
Pdu::SerialQuery {
session_id: SESSION,
serial: 7
@@ -202,7 +204,7 @@ async fn the_client_takes_everything_once_and_then_the_changes() {
let new = vrp("8.8.8.0/24", 24, 15169);
connection
.send(
1,
V,
&[
Pdu::CacheResponse {
session_id: SESSION,
@@ -216,23 +218,23 @@ async fn the_client_takes_everything_once_and_then_the_changes() {
let update = next_update(&mut updates).await;
assert_eq!(update.version, 2);
assert_eq!(update.table.len(), 2);
assert_eq!(update.vrps.len(), 2);
assert_eq!(
update.table.validate(&prefix("8.8.8.0/24"), Some(15169)),
update.vrps.validate(&prefix("8.8.8.0/24"), Some(15169)),
Validity::Valid
);
assert_eq!(
update
.table
.vrps
.validate(&prefix("2001:67c:2e8::/48"), Some(3333)),
Validity::NotFound,
"withdrawn"
);
// Only routes that what came and went covers can have changed.
let changed = update.changed.expect("a change to what there was");
assert!(changed.covers(&prefix("8.8.8.0/24")));
assert!(changed.covers(&prefix("2001:67c:2e8::/48")));
assert!(!changed.covers(&prefix("193.0.0.0/21")));
assert!(changed.vrps.covers(&prefix("8.8.8.0/24")));
assert!(changed.vrps.covers(&prefix("2001:67c:2e8::/48")));
assert!(!changed.vrps.covers(&prefix("193.0.0.0/21")));
assert_eq!(rpki.status().serial, Some(8));
}
@@ -251,7 +253,7 @@ async fn a_cache_reset_makes_the_client_ask_for_everything() {
connection
.send(
1,
V,
&[Pdu::SerialNotify {
session_id: SESSION,
serial: 20,
@@ -262,17 +264,17 @@ async fn a_cache_reset_makes_the_client_ask_for_everything() {
connection.received().await.1,
Pdu::SerialQuery { serial: 7, .. }
));
connection.send(1, &[Pdu::CacheReset]).await;
connection.send(V, &[Pdu::CacheReset]).await;
assert_eq!(connection.received().await.1, Pdu::ResetQuery);
let added = vrp("1.1.1.0/24", 24, 13335);
connection.send_all(&[kept, added], 20).await;
let update = next_update(&mut updates).await;
assert_eq!(update.table.len(), 2);
assert_eq!(update.vrps.len(), 2);
let changed = update.changed.expect("known what changed");
assert!(changed.covers(&prefix("8.8.8.0/24")));
assert!(changed.covers(&prefix("1.1.1.0/24")));
assert!(!changed.covers(&prefix("193.0.0.0/21")), "it stayed");
assert!(changed.vrps.covers(&prefix("8.8.8.0/24")));
assert!(changed.vrps.covers(&prefix("1.1.1.0/24")));
assert!(!changed.vrps.covers(&prefix("193.0.0.0/21")), "it stayed");
assert_eq!(rpki.status().serial, Some(20));
}
@@ -290,7 +292,7 @@ async fn an_answer_that_contradicts_itself_changes_nothing() {
connection
.send(
1,
V,
&[Pdu::SerialNotify {
session_id: SESSION,
serial: 8,
@@ -301,7 +303,7 @@ async fn an_answer_that_contradicts_itself_changes_nothing() {
// A good change, then the withdrawal of something never announced.
connection
.send(
1,
V,
&[
Pdu::CacheResponse {
session_id: SESSION,
@@ -313,8 +315,19 @@ async fn an_answer_that_contradicts_itself_changes_nothing() {
)
.await;
// The client gives the session up and comes back; what it has is
// what it had, and it asks from where it was.
// The client says what was wrong,
assert!(matches!(
connection.received().await,
(
V,
Pdu::ErrorReport {
code: error_code::WITHDRAWAL_OF_UNKNOWN_RECORD,
..
}
)
));
// gives the session up and comes back; what it has is what it had,
// and it asks from where it was.
let mut again = cache.accept().await;
assert_eq!(
again.received().await.1,
@@ -325,7 +338,7 @@ async fn an_answer_that_contradicts_itself_changes_nothing() {
);
let current = rpki.current();
assert_eq!(current.version, 1);
assert_eq!(current.table.len(), 1);
assert_eq!(current.vrps.len(), 1);
assert!(
rpki.status()
.last_error
@@ -333,6 +346,219 @@ async fn an_answer_that_contradicts_itself_changes_nothing() {
);
}
fn aspa(customer: u32, providers: &[u32]) -> Pdu {
Pdu::Aspa {
announce: true,
customer,
providers: providers.to_vec(),
}
}
/// ASPAs come with the VRPs, in version 2. One that is announced again
/// takes the place of the one before.
#[tokio::test]
async fn aspas_are_taken_replaced_and_withdrawn() {
let cache = Cache::start().await;
let (rpki, mut updates) = client_of(&[&cache]);
let mut connection = cache.accept().await;
assert_eq!(connection.received().await, (V, Pdu::ResetQuery));
connection
.send(
V,
&[
Pdu::CacheResponse {
session_id: SESSION,
},
announce(vrp("193.0.0.0/21", 24, 3333)),
aspa(3333, &[1101, 3257]),
aspa(1101, &[0]),
end_of_data(7),
],
)
.await;
let update = next_update(&mut updates).await;
assert_eq!(update.vrps.len(), 1);
assert_eq!(update.aspas.len(), 2);
assert_eq!(update.aspas.providers(3333), Some(&[1101, 3257][..]));
assert_eq!(rpki.status().aspas, 2);
assert_eq!(rpki.status().protocol_version, 2);
assert_eq!(
update.aspas.verify(&[174, 3333], Algorithm::Upstream),
PathValidity::Invalid
);
connection
.send(
V,
&[Pdu::SerialNotify {
session_id: SESSION,
serial: 8,
}],
)
.await;
connection.received().await;
connection
.send(
V,
&[
Pdu::CacheResponse {
session_id: SESSION,
},
// Another list for 3333, a new ASPA, and one gone.
aspa(3333, &[174, 1101]),
aspa(2121, &[3333]),
Pdu::Aspa {
announce: false,
customer: 1101,
providers: vec![],
},
end_of_data(8),
],
)
.await;
let update = next_update(&mut updates).await;
assert_eq!(update.aspas.len(), 2);
assert_eq!(update.aspas.providers(3333), Some(&[174, 1101][..]));
assert_eq!(update.aspas.providers(1101), None);
assert_eq!(
update.aspas.verify(&[174, 3333], Algorithm::Upstream),
PathValidity::Valid
);
let changed = update.changed.expect("a change to what there was");
assert_eq!(*changed.aspa_customers, [3333, 2121, 1101].into());
assert!(changed.vrps.is_empty(), "no VRP came or went");
assert_eq!(update.vrps.len(), 1);
// The withdrawal of an ASPA there is not is a contradiction: nothing
// of the answer is used.
connection
.send(
V,
&[Pdu::SerialNotify {
session_id: SESSION,
serial: 9,
}],
)
.await;
connection.received().await;
connection
.send(
V,
&[
Pdu::CacheResponse {
session_id: SESSION,
},
aspa(3333, &[6939]),
Pdu::Aspa {
announce: false,
customer: 1101,
providers: vec![],
},
end_of_data(9),
],
)
.await;
assert!(matches!(
connection.received().await.1,
Pdu::ErrorReport {
code: error_code::WITHDRAWAL_OF_UNKNOWN_RECORD,
..
}
));
let mut again = cache.accept().await;
assert!(matches!(
again.received().await.1,
Pdu::SerialQuery { serial: 8, .. }
));
let current = rpki.current();
assert_eq!(current.version, 2);
assert_eq!(current.aspas.providers(3333), Some(&[174, 1101][..]));
}
/// A cache that speaks version 1 is spoken to in it. It has no ASPAs to
/// give: those that came from a newer cache go when its data takes their
/// place.
#[tokio::test]
async fn a_cache_that_speaks_version_one_has_no_aspas() {
let cache = Cache::start().await;
let (rpki, mut updates) = client_of(&[&cache]);
let mut connection = cache.accept().await;
connection.received().await;
let ripe = vrp("193.0.0.0/21", 24, 3333);
connection
.send(
V,
&[
Pdu::CacheResponse {
session_id: SESSION,
},
announce(ripe),
aspa(3333, &[1101]),
end_of_data(7),
],
)
.await;
assert_eq!(next_update(&mut updates).await.aspas.len(), 1);
// The validator is replaced by an older one at the same address.
drop(connection);
let mut connection = cache.accept().await;
assert!(matches!(
connection.received().await,
(V, Pdu::SerialQuery { serial: 7, .. })
));
connection
.send(
1,
&[Pdu::ErrorReport {
code: error_code::UNSUPPORTED_VERSION,
text: "version 1 at most".into(),
}],
)
.await;
// Asked again in version 1, and for everything: the serial was of
// another version's data.
let mut connection = cache.accept().await;
assert_eq!(connection.received().await, (1, Pdu::ResetQuery));
connection
.send(
1,
&[
Pdu::CacheResponse {
session_id: SESSION,
},
announce(ripe),
end_of_data(3),
],
)
.await;
let update = next_update(&mut updates).await;
assert!(update.aspas.is_empty());
assert_eq!(update.vrps.len(), 1);
let changed = update.changed.expect("the difference to what there was");
assert_eq!(*changed.aspa_customers, [3333].into());
assert!(changed.vrps.is_empty());
let status = rpki.status();
assert_eq!((status.protocol_version, status.aspas), (1, 0));
// An ASPA in a version that has none is not RTR.
connection
.stream
.write_all(&[1, 11, 1, 0, 0, 0, 0, 16, 0, 0, 0, 1, 0, 0, 0, 2])
.await
.unwrap();
assert!(matches!(
connection.received().await,
(
1,
Pdu::ErrorReport {
code: error_code::UNSUPPORTED_PDU_TYPE,
..
}
)
));
}
/// A validator that only speaks version 0 says so, by an error or by
/// answering in version 0. The client asks again in version 0.
#[tokio::test]
@@ -341,7 +567,7 @@ async fn a_cache_that_speaks_version_zero_is_spoken_to_in_it() {
let cache = Cache::start().await;
let (rpki, mut updates) = client_of(&[&cache]);
let mut connection = cache.accept().await;
assert_eq!(connection.received().await, (1, Pdu::ResetQuery));
assert_eq!(connection.received().await, (V, Pdu::ResetQuery));
if refuses_with_an_error {
connection
.send(
@@ -381,7 +607,7 @@ async fn a_cache_that_speaks_version_zero_is_spoken_to_in_it() {
],
)
.await;
assert_eq!(next_update(&mut updates).await.table.len(), 1);
assert_eq!(next_update(&mut updates).await.vrps.len(), 1);
assert_eq!(rpki.status().protocol_version, 0);
}
}
@@ -407,7 +633,7 @@ async fn a_lost_cache_is_replaced_by_the_next_and_the_data_is_kept() {
let mut connection = second.accept().await;
assert_eq!(connection.received().await.1, Pdu::ResetQuery);
// Still in use meanwhile.
assert_eq!(rpki.current().table.len(), 1);
assert_eq!(rpki.current().vrps.len(), 1);
connection
.send_all(
@@ -416,10 +642,10 @@ async fn a_lost_cache_is_replaced_by_the_next_and_the_data_is_kept() {
)
.await;
let update = next_update(&mut updates).await;
assert_eq!(update.table.len(), 2);
assert_eq!(update.vrps.len(), 2);
let changed = update.changed.expect("the difference to what there was");
assert!(changed.covers(&prefix("1.1.1.0/24")));
assert!(!changed.covers(&prefix("193.0.0.0/21")));
assert!(changed.vrps.covers(&prefix("1.1.1.0/24")));
assert!(!changed.vrps.covers(&prefix("193.0.0.0/21")));
assert_eq!(rpki.status().cache, Some(second.address()));
}
@@ -440,16 +666,16 @@ async fn data_that_is_not_confirmed_in_time_is_dropped() {
connection
.send_all(&[vrp("193.0.0.0/21", 24, 3333)], 7)
.await;
assert_eq!(next_update(&mut updates).await.table.len(), 1);
assert_eq!(next_update(&mut updates).await.vrps.len(), 1);
// The cache goes and does not come back.
drop(connection);
drop(cache);
let update = next_update(&mut updates).await;
assert!(update.table.is_empty());
assert!(update.vrps.is_empty());
assert!(update.changed.is_none(), "anything may have changed");
assert_eq!(
update.table.validate(&prefix("193.0.0.0/21"), Some(15169)),
update.vrps.validate(&prefix("193.0.0.0/21"), Some(15169)),
Validity::NotFound
);
let status = rpki.status();
@@ -468,7 +694,7 @@ async fn a_cache_that_misbehaves_is_given_up_on() {
connection.received().await;
connection
.send(
1,
V,
&[Pdu::ErrorReport {
code: error_code::NO_DATA_AVAILABLE,
text: "still validating".into(),
@@ -486,7 +712,7 @@ async fn a_cache_that_misbehaves_is_given_up_on() {
// Half an answer, then noise.
connection
.send(
1,
V,
&[
Pdu::CacheResponse {
session_id: SESSION,
@@ -502,6 +728,6 @@ async fn a_cache_that_misbehaves_is_given_up_on() {
.unwrap();
let mut connection = cache.accept().await;
assert_eq!(connection.received().await.1, Pdu::ResetQuery);
assert!(rpki.current().table.is_empty());
assert!(rpki.current().vrps.is_empty());
assert_eq!(rpki.version(), 0);
}