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