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:
1 parent
bdaf226a16
commit
8086aa994f
35 files changed
+2484
-359
No files matched your search
@@ -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
|
||||
|
||||
@@ -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(¬), first_up(¬_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(¬), last_down(¬_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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user