Rework the speaker: sans-I/O FSM, sharded table, RFC 7606, ADD-PATH
The work of the speaker-cleanup branch as one commit; the branch keeps the step-by-step history. Sessions - The state machine is a pure function from events to actions, driven by one task per connection. Idle, Connect and Active belong to a dialer that keeps configured peers up with backoff. - Capabilities are negotiated: address families, four-octet AS numbers with AS_TRANS and AS4_PATH, route refresh, ADD-PATH per family and direction. - Collision detection per RFC 4271 section 6.8, by address and identifier. - Connections are taken only from configured peers, a few per address. - A panic in a session is contained and answered with a Cease; a core task that stops shuts the server down with a Cease to every peer. - Writes that make no progress are given up (RFC 9687). Codec - Malformed UPDATEs are handled as RFC 7606 says: treat-as-withdraw, attribute discard, or session reset, with the NOTIFICATION to send. - Address families are open types; any can be named and ignored. - Prefixes are fixed-size values, normalised, with path identifiers. - Lengths are filled in after writing; one definition of each type code. - Property tests and fuzz targets for every decoder. Routing table - Adj-RIBs-In and Loc-RIB in one sharded table that sessions write to directly. Routes are sixteen bytes; attribute sets are interned in wire form. Best-path selection per RFC 4271 section 9.1.2.2 with deterministic MED. - A journal per shard lets followers read changes at their own pace; one that falls behind is resynchronised. - On a RIPE RIS dump of 148 peers and 39.3M routes: loaded in 2.0 s on 12 cores, about 52 bytes a route. Announcing - Originated routes come from the configuration. Each session has its own exporter with an Adj-RIB-Out, so a slow peer delays only itself. - Routes to a peer in our own AS have an empty path and a LOCAL_PREF. Operations - HTTP status page, JSON API, event stream and Prometheus metrics. - Benchmarks on synthetic and real tables. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
1ec60255ea
commit
630c175918
138 files changed
+21650
-2925
No files matched your search
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "bgp-primitives"
|
||||
|
||||
description = "Primitive types for working with routing information"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
@@ -11,9 +11,14 @@ version.workspace = true
|
||||
[dependencies]
|
||||
bitfield.workspace = true
|
||||
bytes.workspace = true
|
||||
eyre.workspace = true
|
||||
nom.workspace = true
|
||||
serde.workspace = true
|
||||
serde_repr.workspace = true
|
||||
thiserror.workspace = true
|
||||
strum.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
proptest.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,80 +1,174 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use eyre::bail;
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use serde::Serialize;
|
||||
use serde_repr::Serialize_repr;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
|
||||
/// An address family number this implementation has no name for.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
||||
#[error("unknown address family identifier {0}")]
|
||||
pub struct UnknownAfi(pub u16);
|
||||
|
||||
/// The IP version of a prefix. For the address family field of a capability
|
||||
/// or an MP_REACH_NLRI, which can name families that are not IP, see [`Afi`].
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Serialize_repr)]
|
||||
#[repr(u16)]
|
||||
pub enum AddressFamilyId {
|
||||
pub enum IpVersion {
|
||||
Ipv4 = 1,
|
||||
Ipv6 = 2,
|
||||
}
|
||||
|
||||
impl TryFrom<u16> for AddressFamilyId {
|
||||
type Error = eyre::Error;
|
||||
|
||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
x if x == Self::Ipv4 as u16 => Self::Ipv4,
|
||||
x if x == Self::Ipv6 as u16 => Self::Ipv6,
|
||||
other => bail!("Unknown AddressFamily: {}", other),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for AddressFamilyId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl IpVersion {
|
||||
/// How many bits an address of this version has.
|
||||
pub fn bits(self) -> u8 {
|
||||
match self {
|
||||
AddressFamilyId::Ipv4 => write!(f, "Ipv4"),
|
||||
AddressFamilyId::Ipv6 => write!(f, "Ipv6"),
|
||||
Self::Ipv4 => 32,
|
||||
Self::Ipv6 => 128,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a Subsequent Address Family Identifier.
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Serialize_repr, Deserialize_repr)]
|
||||
#[repr(u8)]
|
||||
pub enum SubsequentAfi {
|
||||
Unicast = 1,
|
||||
Multicast = 2,
|
||||
NlriWithMpls = 4,
|
||||
MplsLabelledVpn = 128,
|
||||
MulticastMplsVpn = 129,
|
||||
impl TryFrom<u16> for IpVersion {
|
||||
type Error = UnknownAfi;
|
||||
|
||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
x if x == Self::Ipv4 as u16 => Ok(Self::Ipv4),
|
||||
x if x == Self::Ipv6 as u16 => Ok(Self::Ipv6),
|
||||
other => Err(UnknownAfi(other)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for SubsequentAfi {
|
||||
type Error = eyre::Error;
|
||||
impl Display for IpVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
IpVersion::Ipv4 => write!(f, "Ipv4"),
|
||||
IpVersion::Ipv6 => write!(f, "Ipv6"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
Ok(match value {
|
||||
x if x == Self::Unicast as u8 => Self::Unicast,
|
||||
x if x == Self::Multicast as u8 => Self::Multicast,
|
||||
x if x == Self::NlriWithMpls as u8 => Self::NlriWithMpls,
|
||||
x if x == Self::MplsLabelledVpn as u8 => Self::MplsLabelledVpn,
|
||||
x if x == Self::MulticastMplsVpn as u8 => Self::MulticastMplsVpn,
|
||||
other => bail!("Unknown SubsequentAfi: {}", other),
|
||||
/// An Address Family Identifier as it appears on the wire (RFC 4760).
|
||||
///
|
||||
/// Any number is a valid value: a peer may name a family this implementation
|
||||
/// has never heard of, and that must be readable so it can be ignored. Which
|
||||
/// families are *supported* is a separate question, answered where routes
|
||||
/// are stored.
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Afi(pub u16);
|
||||
|
||||
impl Afi {
|
||||
pub const IPV4: Self = Self(1);
|
||||
pub const IPV6: Self = Self(2);
|
||||
pub const L2VPN: Self = Self(25);
|
||||
pub const BGP_LS: Self = Self(16388);
|
||||
}
|
||||
|
||||
impl From<IpVersion> for Afi {
|
||||
fn from(version: IpVersion) -> Self {
|
||||
Self(version as u16)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Afi> for IpVersion {
|
||||
type Error = UnknownAfi;
|
||||
|
||||
fn try_from(afi: Afi) -> Result<Self, Self::Error> {
|
||||
Self::try_from(afi.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A Subsequent Address Family Identifier as it appears on the wire. Like
|
||||
/// [`Afi`], any number is a valid value.
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct Safi(pub u8);
|
||||
|
||||
impl Safi {
|
||||
pub const UNICAST: Self = Self(1);
|
||||
pub const MULTICAST: Self = Self(2);
|
||||
pub const MPLS_LABEL: Self = Self(4);
|
||||
pub const EVPN: Self = Self(70);
|
||||
pub const BGP_LS: Self = Self(71);
|
||||
pub const MPLS_VPN: Self = Self(128);
|
||||
pub const MULTICAST_MPLS_VPN: Self = Self(129);
|
||||
pub const FLOWSPEC: Self = Self(133);
|
||||
}
|
||||
|
||||
/// An address family: the pair that says what kind of routes a capability
|
||||
/// or an MP_REACH_NLRI is about.
|
||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize)]
|
||||
pub struct AfiSafi {
|
||||
pub afi: Afi,
|
||||
pub safi: Safi,
|
||||
}
|
||||
|
||||
impl AfiSafi {
|
||||
pub const IPV4_UNICAST: Self = Self::new(Afi::IPV4, Safi::UNICAST);
|
||||
pub const IPV6_UNICAST: Self = Self::new(Afi::IPV6, Safi::UNICAST);
|
||||
|
||||
pub const fn new(afi: Afi, safi: Safi) -> Self {
|
||||
Self { afi, safi }
|
||||
}
|
||||
|
||||
/// The unicast family of an IP version.
|
||||
pub fn unicast(version: IpVersion) -> Self {
|
||||
Self::new(version.into(), Safi::UNICAST)
|
||||
}
|
||||
|
||||
/// The usual name of the family, if it has one here.
|
||||
fn name(self) -> Option<&'static str> {
|
||||
Some(match (self.afi, self.safi) {
|
||||
(Afi::IPV4, Safi::UNICAST) => "IPv4 unicast",
|
||||
(Afi::IPV6, Safi::UNICAST) => "IPv6 unicast",
|
||||
(Afi::IPV4, Safi::MULTICAST) => "IPv4 multicast",
|
||||
(Afi::IPV6, Safi::MULTICAST) => "IPv6 multicast",
|
||||
(Afi::IPV4, Safi::MPLS_LABEL) => "IPv4 labeled unicast",
|
||||
(Afi::IPV6, Safi::MPLS_LABEL) => "IPv6 labeled unicast",
|
||||
(Afi::IPV4, Safi::MPLS_VPN) => "VPNv4 unicast",
|
||||
(Afi::IPV6, Safi::MPLS_VPN) => "VPNv6 unicast",
|
||||
(Afi::IPV4, Safi::FLOWSPEC) => "IPv4 flowspec",
|
||||
(Afi::IPV6, Safi::FLOWSPEC) => "IPv6 flowspec",
|
||||
(Afi::L2VPN, Safi::EVPN) => "L2VPN EVPN",
|
||||
(Afi::BGP_LS, Safi::BGP_LS) => "BGP-LS",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The implementation try_from for u16 just reuses the u8 implementation since
|
||||
/// we don't have any cases where the SAFI is longer than u8::MAX yet.
|
||||
impl TryFrom<u16> for SubsequentAfi {
|
||||
type Error = eyre::Error;
|
||||
|
||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||
TryFrom::<u8>::try_from(value as u8)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for SubsequentAfi {
|
||||
impl Display for AfiSafi {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
SubsequentAfi::Unicast => write!(f, "Unicast"),
|
||||
SubsequentAfi::Multicast => write!(f, "Multicast"),
|
||||
SubsequentAfi::NlriWithMpls => write!(f, "NlriWithMpls"),
|
||||
SubsequentAfi::MplsLabelledVpn => write!(f, "MplsLabelledVpn"),
|
||||
SubsequentAfi::MulticastMplsVpn => write!(f, "MulticastMplsVpn"),
|
||||
match self.name() {
|
||||
Some(name) => f.write_str(name),
|
||||
None => write!(f, "AFI {} SAFI {}", self.afi.0, self.safi.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn any_family_is_a_value_and_the_common_ones_have_names() {
|
||||
assert_eq!(AfiSafi::IPV6_UNICAST.to_string(), "IPv6 unicast");
|
||||
assert_eq!(
|
||||
AfiSafi::new(Afi::L2VPN, Safi::EVPN).to_string(),
|
||||
"L2VPN EVPN"
|
||||
);
|
||||
assert_eq!(
|
||||
AfiSafi::new(Afi(999), Safi(7)).to_string(),
|
||||
"AFI 999 SAFI 7"
|
||||
);
|
||||
assert_eq!(AfiSafi::unicast(IpVersion::Ipv4), AfiSafi::IPV4_UNICAST);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_ipv4_and_ipv6_are_ip_versions() {
|
||||
assert_eq!(IpVersion::try_from(Afi::IPV6), Ok(IpVersion::Ipv6));
|
||||
assert_eq!(IpVersion::try_from(Afi::L2VPN), Err(UnknownAfi(25)));
|
||||
assert_eq!(IpVersion::try_from(3u16), Err(UnknownAfi(3)));
|
||||
}
|
||||
}
|
||||
+274
-111
@@ -2,130 +2,187 @@ use std::fmt::Display;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use std::str::FromStr;
|
||||
|
||||
use bytes::{Buf, BufMut, BytesMut};
|
||||
use eyre::{Result, bail};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::constants::AddressFamilyId;
|
||||
use crate::parser::{ParserContext, ToWireError};
|
||||
use crate::constants::IpVersion;
|
||||
|
||||
/// IpPrefix represents some IP address prefix, for a specific AddressFamilyId.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize)]
|
||||
/// Why something is not a valid prefix.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Error)]
|
||||
pub enum PrefixError {
|
||||
#[error("prefix {0:?} is not in address/length form")]
|
||||
Format(String),
|
||||
#[error("prefix {0:?} has an address that is neither IPv4 nor IPv6")]
|
||||
Address(String),
|
||||
#[error("prefix length {length} is longer than the {max} bits of an address")]
|
||||
LengthTooLong { length: u8, max: u8 },
|
||||
#[error("prefix length {length} needs {needed} address bytes, got {got}")]
|
||||
TooFewBytes {
|
||||
length: u8,
|
||||
needed: usize,
|
||||
got: usize,
|
||||
},
|
||||
}
|
||||
|
||||
/// An IPv4 or IPv6 prefix.
|
||||
///
|
||||
/// It is a small value with no allocation behind it, cheap to copy, compare
|
||||
/// and hash: a routing table holds millions of them and looks them up by
|
||||
/// value. A prefix has one representation: the bits past its length are
|
||||
/// always zero, so two prefixes are equal exactly when they are the same
|
||||
/// prefix.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Serialize)]
|
||||
pub struct IpPrefix {
|
||||
pub address_family: AddressFamilyId,
|
||||
pub prefix: Vec<u8>,
|
||||
pub length: u8,
|
||||
address_family: IpVersion,
|
||||
length: u8,
|
||||
/// The address, left-aligned, zero past `length` bits.
|
||||
address: [u8; 16],
|
||||
}
|
||||
|
||||
impl IpPrefix {
|
||||
pub fn new(address_family: AddressFamilyId, prefix: Vec<u8>, length: u8) -> Result<Self> {
|
||||
// Ensure that the prefix we are given contains the right number of bytes corresponding to the prefix length.
|
||||
if prefix.len() < ((length + 7) / 8).into() {
|
||||
bail!(
|
||||
"Mismatched prefix {:?} for given prefix length: {}",
|
||||
prefix,
|
||||
length
|
||||
);
|
||||
/// Makes a prefix from the leading bytes of its address. `prefix` must
|
||||
/// hold at least the bytes that `length` bits cover; anything past
|
||||
/// those bits is ignored (RFC 4271 §4.3: such bits are irrelevant).
|
||||
pub fn new(
|
||||
address_family: IpVersion,
|
||||
prefix: impl AsRef<[u8]>,
|
||||
length: u8,
|
||||
) -> Result<Self, PrefixError> {
|
||||
let prefix = prefix.as_ref();
|
||||
let max = address_family.bits();
|
||||
if length > max {
|
||||
return Err(PrefixError::LengthTooLong { length, max });
|
||||
}
|
||||
let needed = usize::from(length.div_ceil(8));
|
||||
if prefix.len() < needed {
|
||||
return Err(PrefixError::TooFewBytes {
|
||||
length,
|
||||
needed,
|
||||
got: prefix.len(),
|
||||
});
|
||||
}
|
||||
let mut address = [0u8; 16];
|
||||
address[..needed].copy_from_slice(&prefix[..needed]);
|
||||
if let (Some(last), rem @ 1..) = (address[..needed].last_mut(), length % 8) {
|
||||
*last &= u8::MAX << (8 - rem);
|
||||
}
|
||||
Ok(Self {
|
||||
address_family,
|
||||
prefix,
|
||||
length,
|
||||
address,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, _: &ParserContext, out: &mut BytesMut) -> Result<(), ToWireError> {
|
||||
// Verify that there is enough space to write the IpPrefix.
|
||||
if out.remaining() < (self.prefix.len() + 1) {
|
||||
Err(ToWireError::OutBufferOverflow)?;
|
||||
pub fn address_family(&self) -> IpVersion {
|
||||
self.address_family
|
||||
}
|
||||
|
||||
/// The prefix length in bits.
|
||||
pub fn length(&self) -> u8 {
|
||||
self.length
|
||||
}
|
||||
|
||||
/// The bytes the prefix length covers, as they go on the wire: none for
|
||||
/// a default route, three for a /24.
|
||||
pub fn bytes(&self) -> &[u8] {
|
||||
&self.address[..usize::from(self.length.div_ceil(8))]
|
||||
}
|
||||
|
||||
/// Returns true if `other` is equal to or more specific than `self`, i.e.
|
||||
/// `self` covers `other`. Prefixes of different address families never
|
||||
/// contain each other.
|
||||
pub fn contains(&self, other: &IpPrefix) -> bool {
|
||||
if self.address_family != other.address_family || other.length < self.length {
|
||||
return false;
|
||||
}
|
||||
let full_bytes = usize::from(self.length / 8);
|
||||
if self.address[..full_bytes] != other.address[..full_bytes] {
|
||||
return false;
|
||||
}
|
||||
let rem_bits = self.length % 8;
|
||||
if rem_bits == 0 {
|
||||
return true;
|
||||
}
|
||||
let mask = u8::MAX << (8 - rem_bits);
|
||||
(self.address[full_bytes] & mask) == (other.address[full_bytes] & mask)
|
||||
}
|
||||
}
|
||||
|
||||
// Write length and prefix.
|
||||
out.put_u8(self.length);
|
||||
out.put(self.prefix.as_slice());
|
||||
/// A prefix as an UPDATE names it: the prefix and, on a session where
|
||||
/// ADD-PATH is in use for the address family (RFC 7911), the identifier
|
||||
/// that tells apart the several routes a peer may announce for it.
|
||||
///
|
||||
/// Whether the identifier is on the wire is a matter of what the session
|
||||
/// negotiated, not of its value. Where it is not, it is zero here.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Serialize)]
|
||||
pub struct Nlri {
|
||||
pub prefix: IpPrefix,
|
||||
pub path_id: u32,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
impl Nlri {
|
||||
/// `prefixes` as they are named on a session without ADD-PATH.
|
||||
pub fn plain(prefixes: impl IntoIterator<Item = IpPrefix>) -> Vec<Self> {
|
||||
prefixes.into_iter().map(Self::from).collect()
|
||||
}
|
||||
|
||||
/// The bytes this takes on the wire, with or without its identifier.
|
||||
pub fn wire_len(&self, with_path_id: bool) -> usize {
|
||||
1 + self.prefix.bytes().len() + if with_path_id { 4 } else { 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IpPrefix> for Nlri {
|
||||
fn from(prefix: IpPrefix) -> Self {
|
||||
Self { prefix, path_id: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
/// So that a list of these compares with a list of plain prefixes.
|
||||
impl PartialEq<IpPrefix> for Nlri {
|
||||
fn eq(&self, prefix: &IpPrefix) -> bool {
|
||||
self.path_id == 0 && self.prefix == *prefix
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Nlri {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.path_id {
|
||||
0 => write!(f, "{}", self.prefix),
|
||||
id => write!(f, "{} (path {id})", self.prefix),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for IpPrefix {
|
||||
type Error = eyre::Error;
|
||||
type Error = PrefixError;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
let parts: Vec<&str> = value.split("/").collect();
|
||||
let format = || PrefixError::Format(value.to_string());
|
||||
let address = || PrefixError::Address(value.to_string());
|
||||
let (addr, length) = value.split_once('/').ok_or_else(format)?;
|
||||
let length: u8 = length.parse().map_err(|_| format())?;
|
||||
|
||||
if parts.len() != 2 {
|
||||
bail!(
|
||||
"Expected IpPrefix in format prefix/length but got: {}",
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
let length: u8 = u8::from_str_radix(parts[1], 10).map_err(eyre::Error::from)?;
|
||||
let mut octets;
|
||||
let afi: AddressFamilyId;
|
||||
|
||||
if parts[0].contains(":") {
|
||||
afi = AddressFamilyId::Ipv6;
|
||||
let addr: Ipv6Addr = Ipv6Addr::from_str(parts[0]).map_err(eyre::Error::from)?;
|
||||
octets = addr.octets().to_vec();
|
||||
} else if parts[0].contains(".") {
|
||||
afi = AddressFamilyId::Ipv4;
|
||||
let addr: Ipv4Addr = Ipv4Addr::from_str(parts[0]).map_err(eyre::Error::from)?;
|
||||
octets = addr.octets().to_vec();
|
||||
let (afi, octets) = if addr.contains(':') {
|
||||
let addr = Ipv6Addr::from_str(addr).map_err(|_| address())?;
|
||||
(IpVersion::Ipv6, addr.octets().to_vec())
|
||||
} else {
|
||||
bail!("Could not figure out address type")
|
||||
}
|
||||
|
||||
// Truncate the octets we have to the right number of bytes to match the prefix length..
|
||||
if length % 8 == 0 {
|
||||
// We can cleanly truncate the number of bytes since we are at a byte boundary.
|
||||
octets.truncate((length / 8).into());
|
||||
} else {
|
||||
// We need to keep length % 8 bits of the last byte.
|
||||
let num_bytes = (length / 8) + 1;
|
||||
let mask = u8::MAX << (8 - (length % 8));
|
||||
octets.truncate(num_bytes.into());
|
||||
// Fix up the last byte.
|
||||
let last_pos = octets.len() - 1;
|
||||
octets[last_pos] &= mask;
|
||||
}
|
||||
|
||||
Ok(IpPrefix {
|
||||
address_family: afi,
|
||||
prefix: octets,
|
||||
length,
|
||||
})
|
||||
let addr = Ipv4Addr::from_str(addr).map_err(|_| address())?;
|
||||
(IpVersion::Ipv4, addr.octets().to_vec())
|
||||
};
|
||||
IpPrefix::new(afi, octets, length)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for IpPrefix {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.address_family {
|
||||
AddressFamilyId::Ipv4 => {
|
||||
// Pad the prefix with 0 bytes if it's less than 4 bytes long.
|
||||
let bytes = &mut self.prefix.clone();
|
||||
if bytes.len() < 4 {
|
||||
bytes.extend(std::iter::repeat(0).take(4 - bytes.len()));
|
||||
}
|
||||
let four_bytes: [u8; 4] = bytes
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.map_err(|_| std::fmt::Error {})?;
|
||||
let ipv4_addr = Ipv4Addr::from(four_bytes);
|
||||
write!(f, "{}/{}", ipv4_addr, self.length)
|
||||
IpVersion::Ipv4 => {
|
||||
let [a, b, c, d, ..] = self.address;
|
||||
write!(f, "{}/{}", Ipv4Addr::new(a, b, c, d), self.length)
|
||||
}
|
||||
AddressFamilyId::Ipv6 => {
|
||||
let bytes = &mut self.prefix.clone();
|
||||
if bytes.len() < 16 {
|
||||
bytes.extend(std::iter::repeat(0).take(16 - bytes.len()));
|
||||
}
|
||||
let sixteen_bytes: [u8; 16] = bytes
|
||||
.as_slice()
|
||||
.try_into()
|
||||
.map_err(|_| std::fmt::Error {})?;
|
||||
let ipv6_addr = Ipv6Addr::from(sixteen_bytes);
|
||||
write!(f, "{}/{}", ipv6_addr, self.length)
|
||||
IpVersion::Ipv6 => {
|
||||
write!(f, "{}/{}", Ipv6Addr::from(self.address), self.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,9 +190,11 @@ impl Display for IpPrefix {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::constants::AddressFamilyId;
|
||||
use crate::constants::IpVersion;
|
||||
|
||||
use super::IpPrefix;
|
||||
use proptest::prelude::*;
|
||||
|
||||
use super::{IpPrefix, PrefixError};
|
||||
|
||||
macro_rules! verify_roundtrip {
|
||||
($name:ident, $prefix_str:expr, $afi:expr, $bytes:expr, $length:expr) => {
|
||||
@@ -143,9 +202,9 @@ mod tests {
|
||||
fn $name() {
|
||||
let ip_prefix = IpPrefix::try_from($prefix_str).unwrap();
|
||||
|
||||
assert_eq!(ip_prefix.address_family, $afi);
|
||||
assert_eq!(ip_prefix.prefix, $bytes);
|
||||
assert_eq!(ip_prefix.length, $length);
|
||||
assert_eq!(ip_prefix.address_family(), $afi);
|
||||
assert_eq!(ip_prefix.bytes(), $bytes);
|
||||
assert_eq!(ip_prefix.length(), $length);
|
||||
|
||||
let to_str: &str = &ip_prefix.to_string();
|
||||
assert_eq!(IpPrefix::try_from(to_str).unwrap(), ip_prefix);
|
||||
@@ -156,7 +215,7 @@ mod tests {
|
||||
verify_roundtrip!(
|
||||
verify_roundtrip_ipv4_24,
|
||||
"10.1.2.0/24",
|
||||
AddressFamilyId::Ipv4,
|
||||
IpVersion::Ipv4,
|
||||
vec![10, 1, 2],
|
||||
24
|
||||
);
|
||||
@@ -165,7 +224,7 @@ mod tests {
|
||||
verify_roundtrip!(
|
||||
verify_roundtrip_ipv4_19,
|
||||
"10.245.123.0/19",
|
||||
AddressFamilyId::Ipv4,
|
||||
IpVersion::Ipv4,
|
||||
vec![10, 245, 96],
|
||||
19
|
||||
);
|
||||
@@ -174,7 +233,7 @@ mod tests {
|
||||
verify_roundtrip!(
|
||||
verify_roundtrip_ipv4_3,
|
||||
"192.168.1.0/3",
|
||||
AddressFamilyId::Ipv4,
|
||||
IpVersion::Ipv4,
|
||||
vec![192],
|
||||
3
|
||||
);
|
||||
@@ -183,7 +242,7 @@ mod tests {
|
||||
verify_roundtrip!(
|
||||
verify_roundtrip_ipv4_0,
|
||||
"0.0.0.0/0",
|
||||
AddressFamilyId::Ipv4,
|
||||
IpVersion::Ipv4,
|
||||
vec![],
|
||||
0
|
||||
);
|
||||
@@ -191,7 +250,7 @@ mod tests {
|
||||
verify_roundtrip!(
|
||||
verify_roundtrip_ipv6_48,
|
||||
"2001:db8:cafe::/48",
|
||||
AddressFamilyId::Ipv6,
|
||||
IpVersion::Ipv6,
|
||||
vec![32, 1, 13, 184, 202, 254],
|
||||
48
|
||||
);
|
||||
@@ -200,16 +259,120 @@ mod tests {
|
||||
verify_roundtrip!(
|
||||
verify_roundtrip_ipv6_32,
|
||||
"2001:db8:cafe::/32",
|
||||
AddressFamilyId::Ipv6,
|
||||
IpVersion::Ipv6,
|
||||
vec![32, 1, 13, 184],
|
||||
32
|
||||
);
|
||||
|
||||
verify_roundtrip!(
|
||||
verify_roundtrip_ipv6_0,
|
||||
"::/0",
|
||||
AddressFamilyId::Ipv6,
|
||||
vec![],
|
||||
0
|
||||
);
|
||||
#[test]
|
||||
fn text_that_is_not_a_prefix_is_rejected_with_the_reason() {
|
||||
for (text, expected) in [
|
||||
("10.0.0.0", PrefixError::Format("10.0.0.0".into())),
|
||||
("10.0.0.0/x", PrefixError::Format("10.0.0.0/x".into())),
|
||||
("10.0.0/8", PrefixError::Address("10.0.0/8".into())),
|
||||
("nonsense/8", PrefixError::Address("nonsense/8".into())),
|
||||
(
|
||||
"10.0.0.0/33",
|
||||
PrefixError::LengthTooLong {
|
||||
length: 33,
|
||||
max: 32,
|
||||
},
|
||||
),
|
||||
(
|
||||
"2001:db8::/129",
|
||||
PrefixError::LengthTooLong {
|
||||
length: 129,
|
||||
max: 128,
|
||||
},
|
||||
),
|
||||
] {
|
||||
assert_eq!(IpPrefix::try_from(text), Err(expected), "{text}");
|
||||
}
|
||||
assert!(IpPrefix::try_from("0.0.0.0/0").is_ok());
|
||||
assert!(IpPrefix::try_from("2001:db8::1/128").is_ok());
|
||||
assert_eq!(
|
||||
IpPrefix::new(IpVersion::Ipv4, vec![10], 16),
|
||||
Err(PrefixError::TooFewBytes {
|
||||
length: 16,
|
||||
needed: 2,
|
||||
got: 1
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_prefix_has_one_representation() {
|
||||
// Extra bytes and stray bits past the length are dropped.
|
||||
let noisy = IpPrefix::new(IpVersion::Ipv4, vec![10, 0xff, 3, 4], 12).unwrap();
|
||||
assert_eq!(noisy, IpPrefix::try_from("10.240.0.0/12").unwrap());
|
||||
assert_eq!(noisy.bytes(), [10, 0xf0]);
|
||||
assert_eq!(
|
||||
IpPrefix::new(IpVersion::Ipv4, vec![0xff; 13], 99),
|
||||
Err(PrefixError::LengthTooLong {
|
||||
length: 99,
|
||||
max: 32
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_prefix_is_a_small_value() {
|
||||
assert!(size_of::<IpPrefix>() <= 20);
|
||||
let prefix = IpPrefix::try_from("10.1.2.0/24").unwrap();
|
||||
let copy = prefix;
|
||||
assert_eq!(copy, prefix);
|
||||
assert_eq!(prefix.bytes(), [10, 1, 2]);
|
||||
assert_eq!(IpPrefix::try_from("0.0.0.0/0").unwrap().bytes(), [0u8; 0]);
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn text_round_trips(version: bool, bytes: [u8; 16], length in 0u8..=128) {
|
||||
let version = if version { IpVersion::Ipv6 } else { IpVersion::Ipv4 };
|
||||
let length = length.min(version.bits());
|
||||
let prefix = IpPrefix::new(version, bytes, length).unwrap();
|
||||
let text = prefix.to_string();
|
||||
prop_assert_eq!(IpPrefix::try_from(text.as_str()), Ok(prefix));
|
||||
prop_assert!(prefix.contains(&prefix));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_that_is_noise_is_an_error_or_a_prefix_that_round_trips(text in "\\PC{0,40}") {
|
||||
if let Ok(prefix) = IpPrefix::try_from(text.as_str()) {
|
||||
let shown = prefix.to_string();
|
||||
prop_assert_eq!(IpPrefix::try_from(shown.as_str()), Ok(prefix));
|
||||
}
|
||||
}
|
||||
|
||||
/// `contains` agrees with comparing the leading bits by hand.
|
||||
#[test]
|
||||
fn contains_is_a_comparison_of_leading_bits(
|
||||
a: [u8; 4], a_len in 0u8..=32, b: [u8; 4], b_len in 0u8..=32,
|
||||
) {
|
||||
let outer = IpPrefix::new(IpVersion::Ipv4, a, a_len).unwrap();
|
||||
let inner = IpPrefix::new(IpVersion::Ipv4, b, b_len).unwrap();
|
||||
let mask = if a_len == 0 { 0 } else { u32::MAX << (32 - a_len) };
|
||||
let expected = b_len >= a_len
|
||||
&& (u32::from_be_bytes(a) & mask) == (u32::from_be_bytes(b) & mask);
|
||||
prop_assert_eq!(outer.contains(&inner), expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn contains_covers_more_specifics() {
|
||||
let p = |s: &str| IpPrefix::try_from(s).unwrap();
|
||||
assert!(p("10.100.0.0/16").contains(&p("10.100.7.0/24")));
|
||||
assert!(p("10.100.0.0/16").contains(&p("10.100.0.0/16")));
|
||||
assert!(!p("10.100.0.0/16").contains(&p("10.101.0.0/24")));
|
||||
assert!(!p("10.100.7.0/24").contains(&p("10.100.0.0/16")));
|
||||
assert!(p("0.0.0.0/0").contains(&p("192.0.2.0/24")));
|
||||
// Non-byte-aligned boundary.
|
||||
assert!(p("10.100.0.0/19").contains(&p("10.100.31.0/24")));
|
||||
assert!(!p("10.100.0.0/19").contains(&p("10.100.32.0/24")));
|
||||
// Cross-family never matches.
|
||||
assert!(!p("::/0").contains(&p("10.0.0.0/8")));
|
||||
assert!(p("2001:db8::/32").contains(&p("2001:db8:cafe::/48")));
|
||||
}
|
||||
|
||||
verify_roundtrip!(verify_roundtrip_ipv6_0, "::/0", IpVersion::Ipv6, vec![], 0);
|
||||
}
|
||||
@@ -1,35 +1,43 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use nom::error::{ErrorKind, ParseError};
|
||||
|
||||
use crate::constants::AddressFamilyId;
|
||||
use crate::constants::AfiSafi;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
/// What reading or writing a message has to know about the session it
|
||||
/// belongs to. The default is a session before its OPENs are exchanged;
|
||||
/// only an UPDATE depends on anything here, and there are none until then.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ParserContext {
|
||||
/// Whether thi parser is being run with a peer that is RFC6793 compliant.
|
||||
pub four_octet_asn: Option<bool>,
|
||||
/// Which address family should be parsed by default with this parser.
|
||||
pub address_family: Option<AddressFamilyId>,
|
||||
/// Whether AS numbers are four octets wide on this session (RFC 6793).
|
||||
pub four_octet_asn: bool,
|
||||
/// Whether the peer is in our own AS. Some attributes mean something
|
||||
/// only between internal peers and are dropped when an external one
|
||||
/// sends them (RFC 7606 §7).
|
||||
pub ibgp: bool,
|
||||
/// The address families whose prefixes carry a path identifier in the
|
||||
/// direction this context is for (RFC 7911). What a session sends and
|
||||
/// what it receives are negotiated separately, so a session has one
|
||||
/// context for reading and one for writing.
|
||||
pub add_path: Vec<AfiSafi>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ToWireError {
|
||||
/// There was not enough space in the output buffer to serialize the data into.
|
||||
OutBufferOverflow,
|
||||
/// Another error.
|
||||
Other(eyre::Error),
|
||||
}
|
||||
|
||||
impl Display for ToWireError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ToWireError::OutBufferOverflow => write!(f, "OutBufferOverflow"),
|
||||
ToWireError::Other(report) => report.fmt(f),
|
||||
}
|
||||
impl ParserContext {
|
||||
pub fn has_add_path(&self, family: AfiSafi) -> bool {
|
||||
self.add_path.contains(&family)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ToWireError {}
|
||||
/// Why a value could not be written to the wire.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Error)]
|
||||
pub enum EncodeError {
|
||||
/// A length does not fit the field that has to carry it.
|
||||
#[error("{what} is too long to encode: {len} exceeds {max}")]
|
||||
TooLong {
|
||||
what: &'static str,
|
||||
len: usize,
|
||||
max: usize,
|
||||
},
|
||||
}
|
||||
|
||||
// Custom error type for the parser.
|
||||
#[derive(Debug)]
|
||||
@@ -37,8 +45,8 @@ pub enum BgpParserError<I> {
|
||||
/// ProtocolError encapsulates protocol level semantics problems that require
|
||||
/// action from the caller of the parser.
|
||||
ProtocolError(ProtocolErrorValues),
|
||||
CustomText(&'static str),
|
||||
Eyre(eyre::ErrReport),
|
||||
/// The input does not follow the format; the text says how.
|
||||
Malformed(String),
|
||||
Nom(I, ErrorKind),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
//! A Poptrie (Asai and Ohara, SIGCOMM 2015): a compressed multiway trie for
|
||||
//! longest-prefix matching.
|
||||
//!
|
||||
//! Work in progress. Nothing in the daemon uses it yet, and the construction
|
||||
//! side has no public entry point, hence the lint expectation below.
|
||||
#![expect(dead_code, reason = "not yet wired into a RIB")]
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -75,7 +82,8 @@ impl<T: Clone> Poptrie<T> {
|
||||
result
|
||||
}
|
||||
|
||||
fn new(&mut self, rt: BTreeMap<(u32, u8), T>) {
|
||||
/// Fills the trie from a routing table keyed by (address, prefix length).
|
||||
fn build(&mut self, rt: BTreeMap<(u32, u8), T>) {
|
||||
const DEPTH: u8 = 6;
|
||||
let mut forest = vec![(0, rt)];
|
||||
|
||||
@@ -168,9 +176,10 @@ mod tests {
|
||||
leaves: vec![],
|
||||
};
|
||||
|
||||
poptrie.new(map);
|
||||
println!("Success");
|
||||
|
||||
dbg!(poptrie);
|
||||
poptrie.build(map);
|
||||
assert!(
|
||||
!poptrie.inners.is_empty(),
|
||||
"building from a non-empty table yields nodes"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user