Code health after review: docs, metrics, structure, one more fuzz target
What the code-health review found, and what a fuzz target written for it
found in its first seconds.
- A set of attributes with the same attribute twice is refused by the
routing table. Its readers (the selection key, the path, the next hop)
could each take a different one of the two. The decoder never lets one
through; the new fuzz target `attrs` found that the table would.
- bgp_peer_up{peer}, to alert on a peer going away, and bgp_build_info.
Every metric is there from the first scrape. Alerts for a peer down,
hijack attempts and kernel errors are in the README.
- `bgp --check config` validates a configuration without starting, and
`--version` says which build it is. CI runs the first on the example
configuration, which a test now also parses with its commented fields
put in; the example had to learn about the next hop check.
- Path attributes have a module of their own, packet::attributes,
re-exported from packet::message as before.
- policy::From, which shadowed the trait, is RouteSource. The server's
connection, dialer and fsm modules are private. Logging arguments live
in the binary; the server library no longer depends on clap.
- One is_unicast_link_local (the standard library's) instead of two of
our own; SelectionKey::origin(); one place where an Import is made.
- READMEs say what the daemon does today rather than what it did not do
a year ago. Documentation links are checked in CI. Unused dependencies
and bgp_web::serve are gone; the Docker build is --locked and does not
ship the fuzz corpus.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
9ca86ede2f
commit
3d946c32ce
41 files changed
+1323
-1102
No files matched your search
@@ -1023,6 +1023,12 @@ pub(crate) async fn run(
|
||||
sockets: Sockets,
|
||||
mut stopping: watch::Receiver<bool>,
|
||||
) -> Result<(), FibError> {
|
||||
// There from the first scrape, not from the first time they count.
|
||||
LazyLock::force(&ROUTES);
|
||||
LazyLock::force(&UNRESOLVED);
|
||||
LazyLock::force(&REFUSED);
|
||||
LazyLock::force(&CHANGES);
|
||||
LazyLock::force(&ERRORS);
|
||||
let Sockets {
|
||||
control,
|
||||
routes,
|
||||
|
||||
@@ -63,10 +63,6 @@ impl Unresolved {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_link_local(address: &Ipv6Addr) -> bool {
|
||||
address.segments()[0] & 0xffc0 == 0xfe80
|
||||
}
|
||||
|
||||
/// The peer a route was learned from, as far as its next hop is concerned.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Neighbor {
|
||||
@@ -152,14 +148,14 @@ pub fn resolve(
|
||||
};
|
||||
match *next_hop {
|
||||
NlriNextHop::Ipv4(gateway) => direct(gateway.into()),
|
||||
NlriNextHop::Ipv6(gateway) if is_link_local(&gateway) => on_peers_link(gateway),
|
||||
NlriNextHop::Ipv6(gateway) if gateway.is_unicast_link_local() => on_peers_link(gateway),
|
||||
NlriNextHop::Ipv6(gateway) => direct(gateway.into()),
|
||||
// The global address if it is a neighbor; else the link-local one,
|
||||
// which is there for exactly that case (RFC 2545 §3).
|
||||
// Some put the link-local address, or nothing, where the global
|
||||
// one goes, having no global one to give.
|
||||
NlriNextHop::Ipv6WithLl { global, link_local }
|
||||
if global.is_unspecified() || is_link_local(&global) =>
|
||||
if global.is_unspecified() || global.is_unicast_link_local() =>
|
||||
{
|
||||
on_peers_link(link_local)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,5 @@
|
||||
|
||||
This crate implements parsers and serialisers for the Border Gateway Protocol wire format.
|
||||
|
||||
## ⚠️ Current status
|
||||
|
||||
This crate is still under development and is not yet ready for use.
|
||||
It is used by the daemon in `bin/bgp`, and tested with property tests
|
||||
(`tests/properties.rs`) and fuzz targets (`fuzz/`).
|
||||
@@ -0,0 +1,935 @@
|
||||
//! Path attributes: what an UPDATE says about the routes it announces
|
||||
//! (RFC 4271 §5 and those that added to it), each with how it is read
|
||||
//! from the wire and written to it.
|
||||
//!
|
||||
//! Everything here is re-exported from [`crate::message`], which is where
|
||||
//! the rest of the tree names it from.
|
||||
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
use bitfield::bitfield;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use nom::Err::Failure;
|
||||
use nom::number::complete::{be_u8, be_u16, be_u32};
|
||||
use nom::{IResult, Parser};
|
||||
use serde::Serialize;
|
||||
use serde_repr::Serialize_repr;
|
||||
|
||||
use bgp_primitives::constants::{Afi, AfiSafi, IpVersion, Safi};
|
||||
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
|
||||
use bgp_primitives::parser::{BgpParserError, EncodeError, ParserContext};
|
||||
|
||||
use crate::message::{AS_TRANS, write_nlri};
|
||||
|
||||
bitfield! {
|
||||
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct PathAttributeFlags(u8);
|
||||
impl new;
|
||||
u8;
|
||||
pub optional, set_optional: 7;
|
||||
pub transitive, set_transitive: 6;
|
||||
pub partial, set_partial: 5;
|
||||
pub extended_length, set_extended_length: 4;
|
||||
}
|
||||
|
||||
/// Path attribute type codes.
|
||||
pub mod attr_type {
|
||||
pub const ORIGIN: u8 = 1;
|
||||
pub const AS_PATH: u8 = 2;
|
||||
pub const NEXT_HOP: u8 = 3;
|
||||
pub const MULTI_EXIT_DISC: u8 = 4;
|
||||
pub const LOCAL_PREF: u8 = 5;
|
||||
pub const ATOMIC_AGGREGATE: u8 = 6;
|
||||
pub const AGGREGATOR: u8 = 7;
|
||||
pub const COMMUNITIES: u8 = 8;
|
||||
pub const ORIGINATOR_ID: u8 = 9;
|
||||
pub const CLUSTER_LIST: u8 = 10;
|
||||
pub const MP_REACH_NLRI: u8 = 14;
|
||||
pub const MP_UNREACH_NLRI: u8 = 15;
|
||||
pub const EXTENDED_COMMUNITIES: u8 = 16;
|
||||
pub const AS4_PATH: u8 = 17;
|
||||
pub const AS4_AGGREGATOR: u8 = 18;
|
||||
pub const LARGE_COMMUNITIES: u8 = 32;
|
||||
pub const ONLY_TO_CUSTOMER: u8 = 35;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
pub enum PathAttribute {
|
||||
Origin(OriginPathAttribute),
|
||||
AsPath(AsPathAttribute),
|
||||
NextHop(NextHopPathAttribute),
|
||||
MultiExitDisc(MultiExitDiscPathAttribute),
|
||||
LocalPref(LocalPrefPathAttribute),
|
||||
AtomicAggregate(AtomicAggregatePathAttribute),
|
||||
Aggregator(AggregatorPathAttribute),
|
||||
Communities(CommunitiesPathAttribute),
|
||||
MpReachNlri(MpReachNlriPathAttribute),
|
||||
MpUnreachNlri(MpUnreachNlriPathAttribute),
|
||||
ExtendedCommunities(ExtendedCommunitiesPathAttribute),
|
||||
/// The AS path with four-octet AS numbers, for a route that crosses
|
||||
/// speakers that only handle two-octet ones (RFC 6793 §3).
|
||||
As4Path(AsPathAttribute),
|
||||
LargeCommunities(LargeCommunitiesPathAttribute),
|
||||
/// Only to Customer (RFC 9234 §5): the route has been handed to a
|
||||
/// customer, a peer or a route server client by the AS with this
|
||||
/// number, and from there on may only go down to customers.
|
||||
OnlyToCustomer(u32),
|
||||
/// An attribute this implementation does not interpret, kept as it
|
||||
/// came so that it can be passed on.
|
||||
UnknownPathAttribute {
|
||||
flags: PathAttributeFlags,
|
||||
type_code: u8,
|
||||
payload: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
/// The Optional and Transitive flags that the attribute with this type code
|
||||
/// is defined to have, for the attributes whose definition we know. A
|
||||
/// received attribute with other values is in error (RFC 7606 §3.c).
|
||||
pub(crate) fn defined_flags(type_code: u8) -> Option<(bool, bool)> {
|
||||
use attr_type::*;
|
||||
const WELL_KNOWN: (bool, bool) = (false, true);
|
||||
const OPTIONAL_TRANSITIVE: (bool, bool) = (true, true);
|
||||
const OPTIONAL_NON_TRANSITIVE: (bool, bool) = (true, false);
|
||||
Some(match type_code {
|
||||
ORIGIN | AS_PATH | NEXT_HOP | LOCAL_PREF | ATOMIC_AGGREGATE => WELL_KNOWN,
|
||||
MULTI_EXIT_DISC | ORIGINATOR_ID | CLUSTER_LIST | MP_REACH_NLRI | MP_UNREACH_NLRI => {
|
||||
OPTIONAL_NON_TRANSITIVE
|
||||
}
|
||||
AGGREGATOR | COMMUNITIES | EXTENDED_COMMUNITIES | AS4_PATH | AS4_AGGREGATOR
|
||||
| LARGE_COMMUNITIES | ONLY_TO_CUSTOMER => OPTIONAL_TRANSITIVE,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// A parsed value, provided the parser used all of it.
|
||||
fn whole<T>((rest, parsed): (&[u8], T)) -> Result<T, nom::Err<BgpParserError<&[u8]>>> {
|
||||
if rest.is_empty() {
|
||||
Ok(parsed)
|
||||
} else {
|
||||
Err(Failure(BgpParserError::Malformed(format!(
|
||||
"{} octets left over",
|
||||
rest.len()
|
||||
))))
|
||||
}
|
||||
}
|
||||
|
||||
/// One attribute as it lies on the wire, split out but not interpreted.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RawAttribute<'a> {
|
||||
pub flags: PathAttributeFlags,
|
||||
pub type_code: u8,
|
||||
pub value: &'a [u8],
|
||||
/// The whole attribute, header and value.
|
||||
pub wire: &'a [u8],
|
||||
}
|
||||
|
||||
/// Why the bytes at the front of a buffer are not an attribute.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum NotAnAttribute {
|
||||
/// Too few octets for flags, type and length.
|
||||
NoHeader,
|
||||
/// The length says more than there is.
|
||||
Truncated {
|
||||
type_code: u8,
|
||||
length: usize,
|
||||
available: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Display for NotAnAttribute {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NoHeader => f.write_str("too few octets for an attribute"),
|
||||
Self::Truncated {
|
||||
type_code,
|
||||
length,
|
||||
available,
|
||||
} => write!(
|
||||
f,
|
||||
"attribute {type_code} has length {length} but only {available} octets remain"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits the attribute at the front of `buf` from what follows it.
|
||||
pub fn split_attribute(buf: &[u8]) -> Result<(RawAttribute<'_>, &[u8]), NotAnAttribute> {
|
||||
let [flags, type_code, rest @ ..] = buf else {
|
||||
return Err(NotAnAttribute::NoHeader);
|
||||
};
|
||||
let flags = PathAttributeFlags(*flags);
|
||||
let (length, rest) = match (flags.extended_length(), rest) {
|
||||
(true, [hi, lo, rest @ ..]) => (usize::from(u16::from_be_bytes([*hi, *lo])), rest),
|
||||
(false, [length, rest @ ..]) => (usize::from(*length), rest),
|
||||
_ => return Err(NotAnAttribute::NoHeader),
|
||||
};
|
||||
let Some((value, rest)) = rest.split_at_checked(length) else {
|
||||
return Err(NotAnAttribute::Truncated {
|
||||
type_code: *type_code,
|
||||
length,
|
||||
available: rest.len(),
|
||||
});
|
||||
};
|
||||
let raw = RawAttribute {
|
||||
flags,
|
||||
type_code: *type_code,
|
||||
value,
|
||||
wire: &buf[..buf.len() - rest.len()],
|
||||
};
|
||||
Ok((raw, rest))
|
||||
}
|
||||
|
||||
impl PathAttribute {
|
||||
pub fn type_code(&self) -> u8 {
|
||||
use attr_type::*;
|
||||
match self {
|
||||
Self::Origin(_) => ORIGIN,
|
||||
Self::AsPath(_) => AS_PATH,
|
||||
Self::NextHop(_) => NEXT_HOP,
|
||||
Self::MultiExitDisc(_) => MULTI_EXIT_DISC,
|
||||
Self::LocalPref(_) => LOCAL_PREF,
|
||||
Self::AtomicAggregate(_) => ATOMIC_AGGREGATE,
|
||||
Self::Aggregator(_) => AGGREGATOR,
|
||||
Self::Communities(_) => COMMUNITIES,
|
||||
Self::MpReachNlri(_) => MP_REACH_NLRI,
|
||||
Self::MpUnreachNlri(_) => MP_UNREACH_NLRI,
|
||||
Self::ExtendedCommunities(_) => EXTENDED_COMMUNITIES,
|
||||
Self::As4Path(_) => AS4_PATH,
|
||||
Self::LargeCommunities(_) => LARGE_COMMUNITIES,
|
||||
Self::OnlyToCustomer(_) => ONLY_TO_CUSTOMER,
|
||||
Self::UnknownPathAttribute { type_code, .. } => *type_code,
|
||||
}
|
||||
}
|
||||
|
||||
/// The flags to send the attribute with, but for Extended Length, which
|
||||
/// depends on how long the value turns out to be.
|
||||
fn flags(&self) -> PathAttributeFlags {
|
||||
match self {
|
||||
// As they came: the Partial bit has to survive (RFC 4271 §5).
|
||||
Self::UnknownPathAttribute { flags, .. } => flags.clone(),
|
||||
known => {
|
||||
let (optional, transitive) = defined_flags(known.type_code())
|
||||
.expect("every attribute with a variant has defined flags");
|
||||
PathAttributeFlags::new(optional, transitive, false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses one attribute, header and value, from the front of `buf`.
|
||||
/// The flags are not checked against the attribute's definition;
|
||||
/// [`UpdateMessage::decode`] does that for attributes off a session.
|
||||
pub fn from_wire<'a>(
|
||||
ctx: &ParserContext,
|
||||
buf: &'a [u8],
|
||||
) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> {
|
||||
let (raw, rest) =
|
||||
split_attribute(buf).map_err(|e| Failure(BgpParserError::Malformed(e.to_string())))?;
|
||||
let attribute = Self::parse_value(ctx, raw.flags, raw.type_code, raw.value)?;
|
||||
Ok((rest, attribute))
|
||||
}
|
||||
|
||||
/// Makes the attribute with this type code from its value. `value` is
|
||||
/// exactly the value, and all of it has to be used. A type code without
|
||||
/// a variant of its own is kept as bytes.
|
||||
pub(crate) fn parse_value<'a>(
|
||||
ctx: &ParserContext,
|
||||
flags: PathAttributeFlags,
|
||||
type_code: u8,
|
||||
value: &'a [u8],
|
||||
) -> Result<Self, nom::Err<BgpParserError<&'a [u8]>>> {
|
||||
use attr_type::*;
|
||||
Ok(match type_code {
|
||||
ORIGIN => Self::Origin(whole(OriginPathAttribute::from_wire(value)?)?),
|
||||
AS_PATH => Self::AsPath(whole(AsPathAttribute::from_wire(ctx, value)?)?),
|
||||
NEXT_HOP => Self::NextHop(whole(NextHopPathAttribute::from_wire(value)?)?),
|
||||
MULTI_EXIT_DISC => {
|
||||
Self::MultiExitDisc(whole(MultiExitDiscPathAttribute::from_wire(value)?)?)
|
||||
}
|
||||
LOCAL_PREF => Self::LocalPref(whole(LocalPrefPathAttribute::from_wire(value)?)?),
|
||||
ATOMIC_AGGREGATE => {
|
||||
Self::AtomicAggregate(whole((value, AtomicAggregatePathAttribute {}))?)
|
||||
}
|
||||
AGGREGATOR => Self::Aggregator(whole(AggregatorPathAttribute::from_wire(ctx, value)?)?),
|
||||
COMMUNITIES => Self::Communities(whole(CommunitiesPathAttribute::from_wire(value)?)?),
|
||||
MP_REACH_NLRI => {
|
||||
Self::MpReachNlri(whole(MpReachNlriPathAttribute::from_wire(ctx, value)?)?)
|
||||
}
|
||||
MP_UNREACH_NLRI => {
|
||||
Self::MpUnreachNlri(whole(MpUnreachNlriPathAttribute::from_wire(ctx, value)?)?)
|
||||
}
|
||||
EXTENDED_COMMUNITIES => Self::ExtendedCommunities(whole(
|
||||
ExtendedCommunitiesPathAttribute::from_wire(value)?,
|
||||
)?),
|
||||
// Always four octets wide, whatever the session's AS_PATH is.
|
||||
AS4_PATH => Self::As4Path(whole(AsPathAttribute::from_wire_with_width(true, value)?)?),
|
||||
LARGE_COMMUNITIES => {
|
||||
Self::LargeCommunities(whole(LargeCommunitiesPathAttribute::from_wire(value)?)?)
|
||||
}
|
||||
ONLY_TO_CUSTOMER => {
|
||||
let asn = <[u8; 4]>::try_from(value).map_err(|_| {
|
||||
malformed(format!(
|
||||
"Only to Customer is {} octets long, not four",
|
||||
value.len()
|
||||
))
|
||||
})?;
|
||||
Self::OnlyToCustomer(u32::from_be_bytes(asn))
|
||||
}
|
||||
_ => Self::UnknownPathAttribute {
|
||||
flags,
|
||||
type_code,
|
||||
payload: value.to_vec(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Writes the attribute, header and value, to the end of `out`.
|
||||
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<(), EncodeError> {
|
||||
// A header with a one-octet length first; whether the value needs
|
||||
// the two-octet one is known once it is written.
|
||||
let start = out.len();
|
||||
let mut flags = self.flags();
|
||||
out.put_u8(0);
|
||||
out.put_u8(self.type_code());
|
||||
out.put_u8(0);
|
||||
let value_start = out.len();
|
||||
self.value_to_wire(ctx, out)?;
|
||||
let length = out.len() - value_start;
|
||||
|
||||
if let Ok(short) = u8::try_from(length)
|
||||
&& !flags.extended_length()
|
||||
{
|
||||
out[start + 2] = short;
|
||||
} else {
|
||||
let long = u16::try_from(length).map_err(|_| EncodeError::TooLong {
|
||||
what: "path attribute",
|
||||
len: length,
|
||||
max: u16::MAX.into(),
|
||||
})?;
|
||||
flags.set_extended_length(true);
|
||||
// Move the value up by one octet to make room.
|
||||
out.put_u8(0);
|
||||
out.copy_within(value_start..value_start + length, value_start + 1);
|
||||
out[start + 2..start + 4].copy_from_slice(&long.to_be_bytes());
|
||||
}
|
||||
out[start] = flags.0;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn value_to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<(), EncodeError> {
|
||||
match self {
|
||||
Self::Origin(a) => a.to_wire(out),
|
||||
Self::AsPath(a) => a.to_wire(ctx, out)?,
|
||||
Self::NextHop(a) => a.to_wire(out),
|
||||
Self::MultiExitDisc(a) => a.to_wire(out),
|
||||
Self::LocalPref(a) => a.to_wire(out),
|
||||
Self::AtomicAggregate(_) => {}
|
||||
Self::Aggregator(a) => a.to_wire(ctx, out),
|
||||
Self::Communities(a) => a.to_wire(out),
|
||||
Self::MpReachNlri(a) => a.to_wire(ctx, out),
|
||||
Self::MpUnreachNlri(a) => a.to_wire(ctx, out),
|
||||
Self::ExtendedCommunities(a) => a.to_wire(out),
|
||||
Self::As4Path(a) => a.to_wire_with_width(true, out)?,
|
||||
Self::LargeCommunities(a) => a.to_wire(out),
|
||||
Self::OnlyToCustomer(asn) => out.put_u32(*asn),
|
||||
Self::UnknownPathAttribute { payload, .. } => out.put_slice(payload),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// An Only to Customer attribute with this AS number, as it is on the
|
||||
/// wire: for adding to attributes that are kept as bytes.
|
||||
pub fn only_to_customer_wire(asn: u32) -> [u8; 7] {
|
||||
const OPTIONAL_TRANSITIVE: u8 = 0xc0;
|
||||
let [a, b, c, d] = asn.to_be_bytes();
|
||||
[
|
||||
OPTIONAL_TRANSITIVE,
|
||||
attr_type::ONLY_TO_CUSTOMER,
|
||||
4,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
d,
|
||||
]
|
||||
}
|
||||
|
||||
type Parsed<'a, T> = IResult<&'a [u8], T, BgpParserError<&'a [u8]>>;
|
||||
|
||||
fn malformed<'a>(detail: String) -> nom::Err<BgpParserError<&'a [u8]>> {
|
||||
Failure(BgpParserError::Malformed(detail))
|
||||
}
|
||||
|
||||
/// ORIGIN: how the route came into BGP (RFC 4271 §5.1.1).
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize_repr)]
|
||||
#[repr(u8)]
|
||||
pub enum OriginPathAttribute {
|
||||
IGP = 0,
|
||||
EGP = 1,
|
||||
INCOMPLETE = 2,
|
||||
}
|
||||
|
||||
impl OriginPathAttribute {
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let (buf, byte) = be_u8(buf)?;
|
||||
let origin = match byte {
|
||||
0 => Self::IGP,
|
||||
1 => Self::EGP,
|
||||
2 => Self::INCOMPLETE,
|
||||
other => return Err(malformed(format!("unknown ORIGIN {other}"))),
|
||||
};
|
||||
Ok((buf, origin))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
out.put_u8(*self as u8);
|
||||
}
|
||||
}
|
||||
|
||||
/// AS_PATH: the ASes a route has passed through, as a list of segments
|
||||
/// (RFC 4271 §4.3). Each segment is a type (1 for AS_SET, 2 for
|
||||
/// AS_SEQUENCE), a count in one octet, and that many AS numbers.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct AsPathAttribute {
|
||||
pub segments: Vec<AsPathSegment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct AsPathSegment {
|
||||
/// True for an AS_SEQUENCE, false for an AS_SET.
|
||||
pub ordered: bool,
|
||||
pub path: Vec<u32>,
|
||||
}
|
||||
|
||||
impl AsPathAttribute {
|
||||
pub fn from_asns(asns: Vec<u32>) -> PathAttribute {
|
||||
let segment = AsPathSegment {
|
||||
ordered: true,
|
||||
path: asns,
|
||||
};
|
||||
PathAttribute::AsPath(AsPathAttribute {
|
||||
segments: vec![segment],
|
||||
})
|
||||
}
|
||||
|
||||
/// How long the path is, as RFC 4271 §9.1.2.2 counts: one for each AS
|
||||
/// of a sequence, one for a whole set.
|
||||
pub fn hops(&self) -> usize {
|
||||
self.segments
|
||||
.iter()
|
||||
.map(|segment| {
|
||||
if segment.ordered {
|
||||
segment.path.len()
|
||||
} else {
|
||||
1
|
||||
}
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// The path as it really is, given what a two-octet session carried:
|
||||
/// this, with AS_TRANS wherever a number did not fit, and `wide`, the
|
||||
/// AS4_PATH that has the numbers themselves for the path's tail
|
||||
/// (RFC 6793 §4.2.3). What `wide` does not cover is kept from the
|
||||
/// front of this.
|
||||
///
|
||||
/// `None` if `wide` is longer than the path, which is not to be used:
|
||||
/// some speaker on the way that knew nothing of AS4_PATH changed the
|
||||
/// one and not the other.
|
||||
pub fn with_as4_path(&self, wide: &AsPathAttribute) -> Option<AsPathAttribute> {
|
||||
let mut keep = self.hops().checked_sub(wide.hops())?;
|
||||
let mut segments = Vec::with_capacity(self.segments.len() + wide.segments.len());
|
||||
for segment in &self.segments {
|
||||
if keep == 0 {
|
||||
break;
|
||||
}
|
||||
let (hops, taken) = if segment.ordered {
|
||||
let taken = segment.path.len().min(keep);
|
||||
(taken, &segment.path[..taken])
|
||||
} else {
|
||||
(1, &segment.path[..])
|
||||
};
|
||||
keep -= hops;
|
||||
segments.push(AsPathSegment {
|
||||
ordered: segment.ordered,
|
||||
path: taken.to_vec(),
|
||||
});
|
||||
}
|
||||
segments.extend(wide.segments.iter().cloned());
|
||||
Some(AsPathAttribute { segments })
|
||||
}
|
||||
|
||||
/// Parses an AS_PATH with AS numbers as wide as the session negotiated
|
||||
/// (RFC 6793).
|
||||
pub fn from_wire<'a>(ctx: &ParserContext, buf: &'a [u8]) -> Parsed<'a, Self> {
|
||||
Self::from_wire_with_width(ctx.four_octet_asn, buf)
|
||||
}
|
||||
|
||||
fn from_wire_with_width(four_octet: bool, buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let parse_segment = |buf| -> Parsed<'_, AsPathSegment> {
|
||||
let (buf, typ) = be_u8(buf)?;
|
||||
let (buf, len) = be_u8(buf)?;
|
||||
// AS_SET and AS_SEQUENCE. The confederation segment types of
|
||||
// RFC 5065 are an error from a peer outside a confederation,
|
||||
// and this speaker is never inside one.
|
||||
if typ != 1 && typ != 2 {
|
||||
return Err(malformed(format!(
|
||||
"unrecognised AS path segment type {typ}"
|
||||
)));
|
||||
}
|
||||
if len == 0 {
|
||||
return Err(malformed("AS path segment with no AS numbers".into()));
|
||||
}
|
||||
let count = usize::from(len);
|
||||
let (buf, path): (_, Vec<u32>) = if four_octet {
|
||||
nom::multi::many_m_n(count, count, be_u32).parse(buf)?
|
||||
} else {
|
||||
nom::multi::many_m_n(count, count, be_u16)
|
||||
.parse(buf)
|
||||
.map(|(buf, asns)| (buf, asns.into_iter().map(u32::from).collect()))?
|
||||
};
|
||||
Ok((
|
||||
buf,
|
||||
AsPathSegment {
|
||||
ordered: typ == 2,
|
||||
path,
|
||||
},
|
||||
))
|
||||
};
|
||||
|
||||
// Not many0: that stops quietly at a segment it cannot read, and a
|
||||
// truncated segment is an error.
|
||||
let mut segments = Vec::new();
|
||||
let mut buf = buf;
|
||||
while !buf.is_empty() {
|
||||
let (rest, segment) = parse_segment(buf)?;
|
||||
segments.push(segment);
|
||||
buf = rest;
|
||||
}
|
||||
Ok((buf, Self { segments }))
|
||||
}
|
||||
|
||||
/// Writes the path with AS numbers as wide as the session negotiated.
|
||||
/// On a two-octet session an AS number that does not fit is written as
|
||||
/// AS_TRANS (RFC 6793 §4.2.2); [`AsPathAttribute::as4_path`] is what
|
||||
/// carries the real numbers alongside.
|
||||
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<(), EncodeError> {
|
||||
self.to_wire_with_width(ctx.four_octet_asn, out)
|
||||
}
|
||||
|
||||
fn to_wire_with_width(&self, four_octet: bool, out: &mut BytesMut) -> Result<(), EncodeError> {
|
||||
for segment in &self.segments {
|
||||
out.put_u8(if segment.ordered { 2 } else { 1 });
|
||||
let count = u8::try_from(segment.path.len()).map_err(|_| EncodeError::TooLong {
|
||||
what: "AS path segment",
|
||||
len: segment.path.len(),
|
||||
max: u8::MAX.into(),
|
||||
})?;
|
||||
out.put_u8(count);
|
||||
for asn in &segment.path {
|
||||
if four_octet {
|
||||
out.put_u32(*asn);
|
||||
} else {
|
||||
out.put_u16(u16::try_from(*asn).unwrap_or(AS_TRANS));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The AS4_PATH to send with this path on a two-octet session, if one is
|
||||
/// needed: it is when some AS number does not fit in two octets and so
|
||||
/// goes out as AS_TRANS in the AS_PATH itself (RFC 6793 §4.2.2).
|
||||
pub fn as4_path(&self) -> Option<PathAttribute> {
|
||||
self.segments
|
||||
.iter()
|
||||
.flat_map(|segment| &segment.path)
|
||||
.any(|asn| u16::try_from(*asn).is_err())
|
||||
.then(|| PathAttribute::As4Path(self.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct NextHopPathAttribute(pub Ipv4Addr);
|
||||
|
||||
impl NextHopPathAttribute {
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let (buf, address) = be_u32(buf)?;
|
||||
Ok((buf, Self(Ipv4Addr::from(address))))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
out.put_u32(self.0.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct MultiExitDiscPathAttribute(pub u32);
|
||||
|
||||
impl MultiExitDiscPathAttribute {
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let (buf, value) = be_u32(buf)?;
|
||||
Ok((buf, Self(value)))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
out.put_u32(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct LocalPrefPathAttribute(pub u32);
|
||||
|
||||
impl LocalPrefPathAttribute {
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let (buf, value) = be_u32(buf)?;
|
||||
Ok((buf, Self(value)))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
out.put_u32(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// ATOMIC_AGGREGATE: a marker with no value (RFC 4271 §5.1.6).
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct AtomicAggregatePathAttribute {}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct AggregatorPathAttribute {
|
||||
pub asn: u32,
|
||||
pub ip: Ipv4Addr,
|
||||
}
|
||||
|
||||
impl AggregatorPathAttribute {
|
||||
/// The AS number is as wide as the session's AS numbers (RFC 6793), so
|
||||
/// the attribute is six or eight octets.
|
||||
pub fn from_wire<'a>(ctx: &ParserContext, buf: &'a [u8]) -> Parsed<'a, Self> {
|
||||
let (buf, asn) = if ctx.four_octet_asn {
|
||||
be_u32(buf)?
|
||||
} else {
|
||||
be_u16(buf).map(|(buf, asn)| (buf, u32::from(asn)))?
|
||||
};
|
||||
let (buf, ip) = be_u32(buf)?;
|
||||
Ok((
|
||||
buf,
|
||||
Self {
|
||||
asn,
|
||||
ip: Ipv4Addr::from(ip),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// On a two-octet session an AS number that does not fit is written as
|
||||
/// AS_TRANS (RFC 6793 §4.2.2).
|
||||
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) {
|
||||
if ctx.four_octet_asn {
|
||||
out.put_u32(self.asn);
|
||||
} else {
|
||||
out.put_u16(u16::try_from(self.asn).unwrap_or(AS_TRANS));
|
||||
}
|
||||
out.put_u32(self.ip.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct CommunitiesPathAttribute(pub Vec<(u16, u16)>);
|
||||
|
||||
impl CommunitiesPathAttribute {
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
// At least one: an empty COMMUNITIES is malformed (RFC 7606 §7.8).
|
||||
let (buf, values) = nom::multi::many1(|i| (be_u16, be_u16).parse(i)).parse(buf)?;
|
||||
Ok((buf, CommunitiesPathAttribute(values)))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
for value in &self.0 {
|
||||
out.put_u16(value.0);
|
||||
out.put_u16(value.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// EXTENDED COMMUNITIES (RFC 4360).
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct ExtendedCommunitiesPathAttribute {
|
||||
pub extended_communities: Vec<ExtendedCommunity>,
|
||||
}
|
||||
|
||||
impl ExtendedCommunitiesPathAttribute {
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let (communities, rest) = buf.as_chunks::<8>();
|
||||
if communities.is_empty() || !rest.is_empty() {
|
||||
return Err(malformed(format!(
|
||||
"{} octets are not a list of extended communities",
|
||||
buf.len()
|
||||
)));
|
||||
}
|
||||
Ok((
|
||||
rest,
|
||||
Self {
|
||||
extended_communities: communities.iter().copied().map(ExtendedCommunity).collect(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
for community in &self.extended_communities {
|
||||
out.put_slice(&community.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One extended community: eight octets, of which the first is the type
|
||||
/// and, for most types, the second a sub-type (RFC 4360 §2). How the rest
|
||||
/// divides into an administrator and a value depends on the type, and is
|
||||
/// left to whoever acts on a particular community.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize)]
|
||||
pub struct ExtendedCommunity(pub [u8; 8]);
|
||||
|
||||
impl ExtendedCommunity {
|
||||
pub fn typ(&self) -> u8 {
|
||||
self.0[0]
|
||||
}
|
||||
|
||||
pub fn sub_typ(&self) -> u8 {
|
||||
self.0[1]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
pub struct LargeCommunitiesPathAttribute {
|
||||
pub communities: Vec<LargeCommunity>,
|
||||
}
|
||||
|
||||
impl LargeCommunitiesPathAttribute {
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let (buf, communities) = nom::multi::many1(LargeCommunity::from_wire).parse(buf)?;
|
||||
Ok((buf, Self { communities }))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
for community in &self.communities {
|
||||
community.to_wire(out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
pub struct LargeCommunity {
|
||||
pub global_admin: u32,
|
||||
pub data_1: u32,
|
||||
pub data_2: u32,
|
||||
}
|
||||
|
||||
impl LargeCommunity {
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let (buf, global_admin) = be_u32(buf)?;
|
||||
let (buf, data_1) = be_u32(buf)?;
|
||||
let (buf, data_2) = be_u32(buf)?;
|
||||
Ok((
|
||||
buf,
|
||||
Self {
|
||||
global_admin,
|
||||
data_1,
|
||||
data_2,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
out.put_u32(self.global_admin);
|
||||
out.put_u32(self.data_1);
|
||||
out.put_u32(self.data_2);
|
||||
}
|
||||
}
|
||||
|
||||
/// The next hop of routes in MP_REACH_NLRI.
|
||||
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Hash)]
|
||||
pub enum NlriNextHop {
|
||||
Ipv4(Ipv4Addr),
|
||||
/// A global IPv6 address.
|
||||
Ipv6(Ipv6Addr),
|
||||
/// A global IPv6 address and a link-local one (RFC 2545 §3).
|
||||
Ipv6WithLl {
|
||||
global: Ipv6Addr,
|
||||
link_local: Ipv6Addr,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Display for NlriNextHop {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
NlriNextHop::Ipv4(addr) => write!(f, "{addr}"),
|
||||
NlriNextHop::Ipv6(addr) => write!(f, "{addr}"),
|
||||
NlriNextHop::Ipv6WithLl { global, link_local } => {
|
||||
write!(f, "{global} (link-local {link_local})")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NlriNextHop {
|
||||
/// Reads a next hop that is all of `bytes`. `None` if they are not the
|
||||
/// length of one.
|
||||
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
|
||||
let v6 = |bytes: &[u8]| <[u8; 16]>::try_from(bytes).ok().map(Ipv6Addr::from);
|
||||
match bytes.len() {
|
||||
4 => <[u8; 4]>::try_from(bytes)
|
||||
.ok()
|
||||
.map(|a| Self::Ipv4(a.into())),
|
||||
16 => v6(bytes).map(Self::Ipv6),
|
||||
32 => Some(Self::Ipv6WithLl {
|
||||
global: v6(&bytes[..16])?,
|
||||
link_local: v6(&bytes[16..])?,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
match self {
|
||||
NlriNextHop::Ipv4(address) => out.put_slice(&address.octets()),
|
||||
NlriNextHop::Ipv6(address) => out.put_slice(&address.octets()),
|
||||
NlriNextHop::Ipv6WithLl { global, link_local } => {
|
||||
out.put_slice(&global.octets());
|
||||
out.put_slice(&link_local.octets());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How many octets the next hop is on the wire.
|
||||
pub fn wire_len(&self) -> u8 {
|
||||
match self {
|
||||
NlriNextHop::Ipv4(_) => 4,
|
||||
NlriNextHop::Ipv6(_) => 16,
|
||||
NlriNextHop::Ipv6WithLl { .. } => 32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses one prefix as an UPDATE names it: with a path identifier in
|
||||
/// front if the session uses them for the family (RFC 7911 §3).
|
||||
fn parse_nlri(version: IpVersion, with_path_id: bool, buf: &[u8]) -> Parsed<'_, Nlri> {
|
||||
let (buf, path_id) = if with_path_id { be_u32(buf)? } else { (buf, 0) };
|
||||
let (buf, prefix_len) = be_u8(buf)?;
|
||||
let byte_len = usize::from(prefix_len.div_ceil(8));
|
||||
let (buf, prefix_bytes) = nom::bytes::take(byte_len).parse(buf)?;
|
||||
let prefix =
|
||||
IpPrefix::new(version, prefix_bytes, prefix_len).map_err(|e| malformed(e.to_string()))?;
|
||||
Ok((buf, Nlri { prefix, path_id }))
|
||||
}
|
||||
|
||||
/// Parses a field that is nothing but prefixes, to its end.
|
||||
pub(crate) fn parse_nlri_list(
|
||||
version: IpVersion,
|
||||
with_path_id: bool,
|
||||
mut buf: &[u8],
|
||||
) -> Parsed<'_, Vec<Nlri>> {
|
||||
// Not many0: that stops quietly at a prefix it cannot read.
|
||||
let mut nlri = Vec::new();
|
||||
while !buf.is_empty() {
|
||||
let (rest, parsed) = parse_nlri(version, with_path_id, buf)?;
|
||||
nlri.push(parsed);
|
||||
buf = rest;
|
||||
}
|
||||
Ok((buf, nlri))
|
||||
}
|
||||
|
||||
/// The IP version of the prefixes that make up the NLRI of `family`.
|
||||
///
|
||||
/// Any family can be named in MP_REACH_NLRI and MP_UNREACH_NLRI, but only
|
||||
/// unicast and multicast routes of IPv4 and IPv6 are plain prefixes, and
|
||||
/// those are the only NLRI this parser reads. A peer does not send routes
|
||||
/// of a family that was not negotiated, so the others do not come up.
|
||||
fn ip_version_of_nlri<'a>(
|
||||
family: AfiSafi,
|
||||
) -> Result<IpVersion, nom::Err<BgpParserError<&'a [u8]>>> {
|
||||
let unsupported = || malformed(format!("NLRI of {family} is not supported"));
|
||||
if family.safi != Safi::UNICAST && family.safi != Safi::MULTICAST {
|
||||
return Err(unsupported());
|
||||
}
|
||||
IpVersion::try_from(family.afi).map_err(|_| unsupported())
|
||||
}
|
||||
|
||||
fn parse_family(buf: &[u8]) -> Parsed<'_, AfiSafi> {
|
||||
let (buf, afi) = be_u16(buf)?;
|
||||
let (buf, safi) = be_u8(buf)?;
|
||||
Ok((buf, AfiSafi::new(Afi(afi), Safi(safi))))
|
||||
}
|
||||
|
||||
/// MP_REACH_NLRI: routes of a family other than the one in the message
|
||||
/// body, with their next hop (RFC 4760 §3).
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
pub struct MpReachNlriPathAttribute {
|
||||
pub family: AfiSafi,
|
||||
pub next_hop: NlriNextHop,
|
||||
pub prefixes: Vec<Nlri>,
|
||||
}
|
||||
|
||||
impl MpReachNlriPathAttribute {
|
||||
pub fn from_wire<'a>(ctx: &ParserContext, buf: &'a [u8]) -> Parsed<'a, Self> {
|
||||
let (buf, family) = parse_family(buf)?;
|
||||
let version = ip_version_of_nlri(family)?;
|
||||
|
||||
let (buf, next_hop_len) = be_u8(buf)?;
|
||||
let (buf, next_hop) = nom::bytes::take(usize::from(next_hop_len)).parse(buf)?;
|
||||
// For IPv6 routes, a global address, optionally followed by a
|
||||
// link-local one (RFC 2545). For IPv4 routes an IPv4 address, or
|
||||
// the same as for IPv6: routes over a link that has only IPv6
|
||||
// addresses (RFC 8950). Which of the two it is shows in its length.
|
||||
let next_hop = NlriNextHop::from_bytes(next_hop)
|
||||
.filter(|next_hop| {
|
||||
version == IpVersion::Ipv4 || !matches!(next_hop, NlriNextHop::Ipv4(_))
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
malformed(format!(
|
||||
"a next hop of {next_hop_len} octets is not valid for {family}"
|
||||
))
|
||||
})?;
|
||||
|
||||
// Reserved octet (formerly the SNPA count), for every family.
|
||||
let (buf, _) = be_u8(buf)?;
|
||||
let (buf, prefixes) = parse_nlri_list(version, ctx.has_add_path(family), buf)?;
|
||||
Ok((
|
||||
buf,
|
||||
Self {
|
||||
family,
|
||||
next_hop,
|
||||
prefixes,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) {
|
||||
out.put_u16(self.family.afi.0);
|
||||
out.put_u8(self.family.safi.0);
|
||||
out.put_u8(self.next_hop.wire_len());
|
||||
self.next_hop.to_wire(out);
|
||||
out.put_u8(0);
|
||||
write_nlri(&self.prefixes, ctx.has_add_path(self.family), out);
|
||||
}
|
||||
}
|
||||
|
||||
/// MP_UNREACH_NLRI: withdrawals of routes of a family other than the one
|
||||
/// in the message body (RFC 4760 §4).
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
pub struct MpUnreachNlriPathAttribute {
|
||||
pub family: AfiSafi,
|
||||
pub prefixes: Vec<Nlri>,
|
||||
}
|
||||
|
||||
impl MpUnreachNlriPathAttribute {
|
||||
pub fn from_wire<'a>(ctx: &ParserContext, buf: &'a [u8]) -> Parsed<'a, Self> {
|
||||
let (buf, family) = parse_family(buf)?;
|
||||
let version = ip_version_of_nlri(family)?;
|
||||
let (buf, prefixes) = parse_nlri_list(version, ctx.has_add_path(family), buf)?;
|
||||
Ok((buf, Self { family, prefixes }))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) {
|
||||
out.put_u16(self.family.afi.0);
|
||||
out.put_u8(self.family.safi.0);
|
||||
write_nlri(&self.prefixes, ctx.has_add_path(self.family), out);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod attributes;
|
||||
pub mod community;
|
||||
pub mod errors;
|
||||
pub mod message;
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
use bitfield::bitfield;
|
||||
use bytes::BufMut;
|
||||
use bytes::BytesMut;
|
||||
use nom::Err::Failure;
|
||||
use nom::IResult;
|
||||
use nom::Parser;
|
||||
use nom::number::complete::be_u8;
|
||||
use nom::number::complete::be_u16;
|
||||
use nom::number::complete::be_u32;
|
||||
use serde::Serialize;
|
||||
use serde_repr::Serialize_repr;
|
||||
|
||||
use crate::update_decode::UpdateError;
|
||||
use bgp_primitives::constants::{Afi, AfiSafi, IpVersion, Safi};
|
||||
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
|
||||
use bgp_primitives::constants::{Afi, AfiSafi, Safi};
|
||||
use bgp_primitives::ip_prefix::Nlri;
|
||||
use bgp_primitives::parser::BgpParserError;
|
||||
use bgp_primitives::parser::EncodeError;
|
||||
use bgp_primitives::parser::ParserContext;
|
||||
@@ -352,7 +342,7 @@ fn fill_length(out: &mut BytesMut, at: usize, what: &'static str) -> Result<(),
|
||||
/// Writes prefixes as an UPDATE names them: for each, its path identifier
|
||||
/// if the session uses them for the family (RFC 7911 §3), its length in
|
||||
/// bits, and the bytes that length covers.
|
||||
fn write_nlri(nlri: &[Nlri], with_path_id: bool, out: &mut BytesMut) {
|
||||
pub(crate) fn write_nlri(nlri: &[Nlri], with_path_id: bool, out: &mut BytesMut) {
|
||||
for n in nlri {
|
||||
if with_path_id {
|
||||
out.put_u32(n.path_id);
|
||||
@@ -376,918 +366,7 @@ pub(crate) fn describe(e: &nom::Err<BgpParserError<&[u8]>>) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
bitfield! {
|
||||
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
|
||||
pub struct PathAttributeFlags(u8);
|
||||
impl new;
|
||||
u8;
|
||||
pub optional, set_optional: 7;
|
||||
pub transitive, set_transitive: 6;
|
||||
pub partial, set_partial: 5;
|
||||
pub extended_length, set_extended_length: 4;
|
||||
}
|
||||
|
||||
/// Path attribute type codes.
|
||||
pub mod attr_type {
|
||||
pub const ORIGIN: u8 = 1;
|
||||
pub const AS_PATH: u8 = 2;
|
||||
pub const NEXT_HOP: u8 = 3;
|
||||
pub const MULTI_EXIT_DISC: u8 = 4;
|
||||
pub const LOCAL_PREF: u8 = 5;
|
||||
pub const ATOMIC_AGGREGATE: u8 = 6;
|
||||
pub const AGGREGATOR: u8 = 7;
|
||||
pub const COMMUNITIES: u8 = 8;
|
||||
pub const ORIGINATOR_ID: u8 = 9;
|
||||
pub const CLUSTER_LIST: u8 = 10;
|
||||
pub const MP_REACH_NLRI: u8 = 14;
|
||||
pub const MP_UNREACH_NLRI: u8 = 15;
|
||||
pub const EXTENDED_COMMUNITIES: u8 = 16;
|
||||
pub const AS4_PATH: u8 = 17;
|
||||
pub const AS4_AGGREGATOR: u8 = 18;
|
||||
pub const LARGE_COMMUNITIES: u8 = 32;
|
||||
pub const ONLY_TO_CUSTOMER: u8 = 35;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
pub enum PathAttribute {
|
||||
Origin(OriginPathAttribute),
|
||||
AsPath(AsPathAttribute),
|
||||
NextHop(NextHopPathAttribute),
|
||||
MultiExitDisc(MultiExitDiscPathAttribute),
|
||||
LocalPref(LocalPrefPathAttribute),
|
||||
AtomicAggregate(AtomicAggregatePathAttribute),
|
||||
Aggregator(AggregatorPathAttribute),
|
||||
Communities(CommunitiesPathAttribute),
|
||||
MpReachNlri(MpReachNlriPathAttribute),
|
||||
MpUnreachNlri(MpUnreachNlriPathAttribute),
|
||||
ExtendedCommunities(ExtendedCommunitiesPathAttribute),
|
||||
/// The AS path with four-octet AS numbers, for a route that crosses
|
||||
/// speakers that only handle two-octet ones (RFC 6793 §3).
|
||||
As4Path(AsPathAttribute),
|
||||
LargeCommunities(LargeCommunitiesPathAttribute),
|
||||
/// Only to Customer (RFC 9234 §5): the route has been handed to a
|
||||
/// customer, a peer or a route server client by the AS with this
|
||||
/// number, and from there on may only go down to customers.
|
||||
OnlyToCustomer(u32),
|
||||
/// An attribute this implementation does not interpret, kept as it
|
||||
/// came so that it can be passed on.
|
||||
UnknownPathAttribute {
|
||||
flags: PathAttributeFlags,
|
||||
type_code: u8,
|
||||
payload: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
/// The Optional and Transitive flags that the attribute with this type code
|
||||
/// is defined to have, for the attributes whose definition we know. A
|
||||
/// received attribute with other values is in error (RFC 7606 §3.c).
|
||||
pub(crate) fn defined_flags(type_code: u8) -> Option<(bool, bool)> {
|
||||
use attr_type::*;
|
||||
const WELL_KNOWN: (bool, bool) = (false, true);
|
||||
const OPTIONAL_TRANSITIVE: (bool, bool) = (true, true);
|
||||
const OPTIONAL_NON_TRANSITIVE: (bool, bool) = (true, false);
|
||||
Some(match type_code {
|
||||
ORIGIN | AS_PATH | NEXT_HOP | LOCAL_PREF | ATOMIC_AGGREGATE => WELL_KNOWN,
|
||||
MULTI_EXIT_DISC | ORIGINATOR_ID | CLUSTER_LIST | MP_REACH_NLRI | MP_UNREACH_NLRI => {
|
||||
OPTIONAL_NON_TRANSITIVE
|
||||
}
|
||||
AGGREGATOR | COMMUNITIES | EXTENDED_COMMUNITIES | AS4_PATH | AS4_AGGREGATOR
|
||||
| LARGE_COMMUNITIES | ONLY_TO_CUSTOMER => OPTIONAL_TRANSITIVE,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// A parsed value, provided the parser used all of it.
|
||||
fn whole<T>((rest, parsed): (&[u8], T)) -> Result<T, nom::Err<BgpParserError<&[u8]>>> {
|
||||
if rest.is_empty() {
|
||||
Ok(parsed)
|
||||
} else {
|
||||
Err(Failure(BgpParserError::Malformed(format!(
|
||||
"{} octets left over",
|
||||
rest.len()
|
||||
))))
|
||||
}
|
||||
}
|
||||
|
||||
/// One attribute as it lies on the wire, split out but not interpreted.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RawAttribute<'a> {
|
||||
pub flags: PathAttributeFlags,
|
||||
pub type_code: u8,
|
||||
pub value: &'a [u8],
|
||||
/// The whole attribute, header and value.
|
||||
pub wire: &'a [u8],
|
||||
}
|
||||
|
||||
/// Why the bytes at the front of a buffer are not an attribute.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum NotAnAttribute {
|
||||
/// Too few octets for flags, type and length.
|
||||
NoHeader,
|
||||
/// The length says more than there is.
|
||||
Truncated {
|
||||
type_code: u8,
|
||||
length: usize,
|
||||
available: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Display for NotAnAttribute {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NoHeader => f.write_str("too few octets for an attribute"),
|
||||
Self::Truncated {
|
||||
type_code,
|
||||
length,
|
||||
available,
|
||||
} => write!(
|
||||
f,
|
||||
"attribute {type_code} has length {length} but only {available} octets remain"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits the attribute at the front of `buf` from what follows it.
|
||||
pub fn split_attribute(buf: &[u8]) -> Result<(RawAttribute<'_>, &[u8]), NotAnAttribute> {
|
||||
let [flags, type_code, rest @ ..] = buf else {
|
||||
return Err(NotAnAttribute::NoHeader);
|
||||
};
|
||||
let flags = PathAttributeFlags(*flags);
|
||||
let (length, rest) = match (flags.extended_length(), rest) {
|
||||
(true, [hi, lo, rest @ ..]) => (usize::from(u16::from_be_bytes([*hi, *lo])), rest),
|
||||
(false, [length, rest @ ..]) => (usize::from(*length), rest),
|
||||
_ => return Err(NotAnAttribute::NoHeader),
|
||||
};
|
||||
let Some((value, rest)) = rest.split_at_checked(length) else {
|
||||
return Err(NotAnAttribute::Truncated {
|
||||
type_code: *type_code,
|
||||
length,
|
||||
available: rest.len(),
|
||||
});
|
||||
};
|
||||
let raw = RawAttribute {
|
||||
flags,
|
||||
type_code: *type_code,
|
||||
value,
|
||||
wire: &buf[..buf.len() - rest.len()],
|
||||
};
|
||||
Ok((raw, rest))
|
||||
}
|
||||
|
||||
impl PathAttribute {
|
||||
pub fn type_code(&self) -> u8 {
|
||||
use attr_type::*;
|
||||
match self {
|
||||
Self::Origin(_) => ORIGIN,
|
||||
Self::AsPath(_) => AS_PATH,
|
||||
Self::NextHop(_) => NEXT_HOP,
|
||||
Self::MultiExitDisc(_) => MULTI_EXIT_DISC,
|
||||
Self::LocalPref(_) => LOCAL_PREF,
|
||||
Self::AtomicAggregate(_) => ATOMIC_AGGREGATE,
|
||||
Self::Aggregator(_) => AGGREGATOR,
|
||||
Self::Communities(_) => COMMUNITIES,
|
||||
Self::MpReachNlri(_) => MP_REACH_NLRI,
|
||||
Self::MpUnreachNlri(_) => MP_UNREACH_NLRI,
|
||||
Self::ExtendedCommunities(_) => EXTENDED_COMMUNITIES,
|
||||
Self::As4Path(_) => AS4_PATH,
|
||||
Self::LargeCommunities(_) => LARGE_COMMUNITIES,
|
||||
Self::OnlyToCustomer(_) => ONLY_TO_CUSTOMER,
|
||||
Self::UnknownPathAttribute { type_code, .. } => *type_code,
|
||||
}
|
||||
}
|
||||
|
||||
/// The flags to send the attribute with, but for Extended Length, which
|
||||
/// depends on how long the value turns out to be.
|
||||
fn flags(&self) -> PathAttributeFlags {
|
||||
match self {
|
||||
// As they came: the Partial bit has to survive (RFC 4271 §5).
|
||||
Self::UnknownPathAttribute { flags, .. } => flags.clone(),
|
||||
known => {
|
||||
let (optional, transitive) = defined_flags(known.type_code())
|
||||
.expect("every attribute with a variant has defined flags");
|
||||
PathAttributeFlags::new(optional, transitive, false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses one attribute, header and value, from the front of `buf`.
|
||||
/// The flags are not checked against the attribute's definition;
|
||||
/// [`UpdateMessage::decode`] does that for attributes off a session.
|
||||
pub fn from_wire<'a>(
|
||||
ctx: &ParserContext,
|
||||
buf: &'a [u8],
|
||||
) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> {
|
||||
let (raw, rest) =
|
||||
split_attribute(buf).map_err(|e| Failure(BgpParserError::Malformed(e.to_string())))?;
|
||||
let attribute = Self::parse_value(ctx, raw.flags, raw.type_code, raw.value)?;
|
||||
Ok((rest, attribute))
|
||||
}
|
||||
|
||||
/// Makes the attribute with this type code from its value. `value` is
|
||||
/// exactly the value, and all of it has to be used. A type code without
|
||||
/// a variant of its own is kept as bytes.
|
||||
pub(crate) fn parse_value<'a>(
|
||||
ctx: &ParserContext,
|
||||
flags: PathAttributeFlags,
|
||||
type_code: u8,
|
||||
value: &'a [u8],
|
||||
) -> Result<Self, nom::Err<BgpParserError<&'a [u8]>>> {
|
||||
use attr_type::*;
|
||||
Ok(match type_code {
|
||||
ORIGIN => Self::Origin(whole(OriginPathAttribute::from_wire(value)?)?),
|
||||
AS_PATH => Self::AsPath(whole(AsPathAttribute::from_wire(ctx, value)?)?),
|
||||
NEXT_HOP => Self::NextHop(whole(NextHopPathAttribute::from_wire(value)?)?),
|
||||
MULTI_EXIT_DISC => {
|
||||
Self::MultiExitDisc(whole(MultiExitDiscPathAttribute::from_wire(value)?)?)
|
||||
}
|
||||
LOCAL_PREF => Self::LocalPref(whole(LocalPrefPathAttribute::from_wire(value)?)?),
|
||||
ATOMIC_AGGREGATE => {
|
||||
Self::AtomicAggregate(whole((value, AtomicAggregatePathAttribute {}))?)
|
||||
}
|
||||
AGGREGATOR => Self::Aggregator(whole(AggregatorPathAttribute::from_wire(ctx, value)?)?),
|
||||
COMMUNITIES => Self::Communities(whole(CommunitiesPathAttribute::from_wire(value)?)?),
|
||||
MP_REACH_NLRI => {
|
||||
Self::MpReachNlri(whole(MpReachNlriPathAttribute::from_wire(ctx, value)?)?)
|
||||
}
|
||||
MP_UNREACH_NLRI => {
|
||||
Self::MpUnreachNlri(whole(MpUnreachNlriPathAttribute::from_wire(ctx, value)?)?)
|
||||
}
|
||||
EXTENDED_COMMUNITIES => Self::ExtendedCommunities(whole(
|
||||
ExtendedCommunitiesPathAttribute::from_wire(value)?,
|
||||
)?),
|
||||
// Always four octets wide, whatever the session's AS_PATH is.
|
||||
AS4_PATH => Self::As4Path(whole(AsPathAttribute::from_wire_with_width(true, value)?)?),
|
||||
LARGE_COMMUNITIES => {
|
||||
Self::LargeCommunities(whole(LargeCommunitiesPathAttribute::from_wire(value)?)?)
|
||||
}
|
||||
ONLY_TO_CUSTOMER => {
|
||||
let asn = <[u8; 4]>::try_from(value).map_err(|_| {
|
||||
malformed(format!(
|
||||
"Only to Customer is {} octets long, not four",
|
||||
value.len()
|
||||
))
|
||||
})?;
|
||||
Self::OnlyToCustomer(u32::from_be_bytes(asn))
|
||||
}
|
||||
_ => Self::UnknownPathAttribute {
|
||||
flags,
|
||||
type_code,
|
||||
payload: value.to_vec(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Writes the attribute, header and value, to the end of `out`.
|
||||
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<(), EncodeError> {
|
||||
// A header with a one-octet length first; whether the value needs
|
||||
// the two-octet one is known once it is written.
|
||||
let start = out.len();
|
||||
let mut flags = self.flags();
|
||||
out.put_u8(0);
|
||||
out.put_u8(self.type_code());
|
||||
out.put_u8(0);
|
||||
let value_start = out.len();
|
||||
self.value_to_wire(ctx, out)?;
|
||||
let length = out.len() - value_start;
|
||||
|
||||
if let Ok(short) = u8::try_from(length)
|
||||
&& !flags.extended_length()
|
||||
{
|
||||
out[start + 2] = short;
|
||||
} else {
|
||||
let long = u16::try_from(length).map_err(|_| EncodeError::TooLong {
|
||||
what: "path attribute",
|
||||
len: length,
|
||||
max: u16::MAX.into(),
|
||||
})?;
|
||||
flags.set_extended_length(true);
|
||||
// Move the value up by one octet to make room.
|
||||
out.put_u8(0);
|
||||
out.copy_within(value_start..value_start + length, value_start + 1);
|
||||
out[start + 2..start + 4].copy_from_slice(&long.to_be_bytes());
|
||||
}
|
||||
out[start] = flags.0;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn value_to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<(), EncodeError> {
|
||||
match self {
|
||||
Self::Origin(a) => a.to_wire(out),
|
||||
Self::AsPath(a) => a.to_wire(ctx, out)?,
|
||||
Self::NextHop(a) => a.to_wire(out),
|
||||
Self::MultiExitDisc(a) => a.to_wire(out),
|
||||
Self::LocalPref(a) => a.to_wire(out),
|
||||
Self::AtomicAggregate(_) => {}
|
||||
Self::Aggregator(a) => a.to_wire(ctx, out),
|
||||
Self::Communities(a) => a.to_wire(out),
|
||||
Self::MpReachNlri(a) => a.to_wire(ctx, out),
|
||||
Self::MpUnreachNlri(a) => a.to_wire(ctx, out),
|
||||
Self::ExtendedCommunities(a) => a.to_wire(out),
|
||||
Self::As4Path(a) => a.to_wire_with_width(true, out)?,
|
||||
Self::LargeCommunities(a) => a.to_wire(out),
|
||||
Self::OnlyToCustomer(asn) => out.put_u32(*asn),
|
||||
Self::UnknownPathAttribute { payload, .. } => out.put_slice(payload),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// An Only to Customer attribute with this AS number, as it is on the
|
||||
/// wire: for adding to attributes that are kept as bytes.
|
||||
pub fn only_to_customer_wire(asn: u32) -> [u8; 7] {
|
||||
const OPTIONAL_TRANSITIVE: u8 = 0xc0;
|
||||
let [a, b, c, d] = asn.to_be_bytes();
|
||||
[
|
||||
OPTIONAL_TRANSITIVE,
|
||||
attr_type::ONLY_TO_CUSTOMER,
|
||||
4,
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
d,
|
||||
]
|
||||
}
|
||||
|
||||
type Parsed<'a, T> = IResult<&'a [u8], T, BgpParserError<&'a [u8]>>;
|
||||
|
||||
fn malformed<'a>(detail: String) -> nom::Err<BgpParserError<&'a [u8]>> {
|
||||
Failure(BgpParserError::Malformed(detail))
|
||||
}
|
||||
|
||||
/// ORIGIN: how the route came into BGP (RFC 4271 §5.1.1).
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize_repr)]
|
||||
#[repr(u8)]
|
||||
pub enum OriginPathAttribute {
|
||||
IGP = 0,
|
||||
EGP = 1,
|
||||
INCOMPLETE = 2,
|
||||
}
|
||||
|
||||
impl OriginPathAttribute {
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let (buf, byte) = be_u8(buf)?;
|
||||
let origin = match byte {
|
||||
0 => Self::IGP,
|
||||
1 => Self::EGP,
|
||||
2 => Self::INCOMPLETE,
|
||||
other => return Err(malformed(format!("unknown ORIGIN {other}"))),
|
||||
};
|
||||
Ok((buf, origin))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
out.put_u8(*self as u8);
|
||||
}
|
||||
}
|
||||
|
||||
/// AS_PATH: the ASes a route has passed through, as a list of segments
|
||||
/// (RFC 4271 §4.3). Each segment is a type (1 for AS_SET, 2 for
|
||||
/// AS_SEQUENCE), a count in one octet, and that many AS numbers.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct AsPathAttribute {
|
||||
pub segments: Vec<AsPathSegment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct AsPathSegment {
|
||||
/// True for an AS_SEQUENCE, false for an AS_SET.
|
||||
pub ordered: bool,
|
||||
pub path: Vec<u32>,
|
||||
}
|
||||
|
||||
impl AsPathAttribute {
|
||||
pub fn from_asns(asns: Vec<u32>) -> PathAttribute {
|
||||
let segment = AsPathSegment {
|
||||
ordered: true,
|
||||
path: asns,
|
||||
};
|
||||
PathAttribute::AsPath(AsPathAttribute {
|
||||
segments: vec![segment],
|
||||
})
|
||||
}
|
||||
|
||||
/// How long the path is, as RFC 4271 §9.1.2.2 counts: one for each AS
|
||||
/// of a sequence, one for a whole set.
|
||||
pub fn hops(&self) -> usize {
|
||||
self.segments
|
||||
.iter()
|
||||
.map(|segment| {
|
||||
if segment.ordered {
|
||||
segment.path.len()
|
||||
} else {
|
||||
1
|
||||
}
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// The path as it really is, given what a two-octet session carried:
|
||||
/// this, with AS_TRANS wherever a number did not fit, and `wide`, the
|
||||
/// AS4_PATH that has the numbers themselves for the path's tail
|
||||
/// (RFC 6793 §4.2.3). What `wide` does not cover is kept from the
|
||||
/// front of this.
|
||||
///
|
||||
/// `None` if `wide` is longer than the path, which is not to be used:
|
||||
/// some speaker on the way that knew nothing of AS4_PATH changed the
|
||||
/// one and not the other.
|
||||
pub fn with_as4_path(&self, wide: &AsPathAttribute) -> Option<AsPathAttribute> {
|
||||
let mut keep = self.hops().checked_sub(wide.hops())?;
|
||||
let mut segments = Vec::with_capacity(self.segments.len() + wide.segments.len());
|
||||
for segment in &self.segments {
|
||||
if keep == 0 {
|
||||
break;
|
||||
}
|
||||
let (hops, taken) = if segment.ordered {
|
||||
let taken = segment.path.len().min(keep);
|
||||
(taken, &segment.path[..taken])
|
||||
} else {
|
||||
(1, &segment.path[..])
|
||||
};
|
||||
keep -= hops;
|
||||
segments.push(AsPathSegment {
|
||||
ordered: segment.ordered,
|
||||
path: taken.to_vec(),
|
||||
});
|
||||
}
|
||||
segments.extend(wide.segments.iter().cloned());
|
||||
Some(AsPathAttribute { segments })
|
||||
}
|
||||
|
||||
/// Parses an AS_PATH with AS numbers as wide as the session negotiated
|
||||
/// (RFC 6793).
|
||||
pub fn from_wire<'a>(ctx: &ParserContext, buf: &'a [u8]) -> Parsed<'a, Self> {
|
||||
Self::from_wire_with_width(ctx.four_octet_asn, buf)
|
||||
}
|
||||
|
||||
fn from_wire_with_width(four_octet: bool, buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let parse_segment = |buf| -> Parsed<'_, AsPathSegment> {
|
||||
let (buf, typ) = be_u8(buf)?;
|
||||
let (buf, len) = be_u8(buf)?;
|
||||
// AS_SET and AS_SEQUENCE. The confederation segment types of
|
||||
// RFC 5065 are an error from a peer outside a confederation,
|
||||
// and this speaker is never inside one.
|
||||
if typ != 1 && typ != 2 {
|
||||
return Err(malformed(format!(
|
||||
"unrecognised AS path segment type {typ}"
|
||||
)));
|
||||
}
|
||||
if len == 0 {
|
||||
return Err(malformed("AS path segment with no AS numbers".into()));
|
||||
}
|
||||
let count = usize::from(len);
|
||||
let (buf, path): (_, Vec<u32>) = if four_octet {
|
||||
nom::multi::many_m_n(count, count, be_u32).parse(buf)?
|
||||
} else {
|
||||
nom::multi::many_m_n(count, count, be_u16)
|
||||
.parse(buf)
|
||||
.map(|(buf, asns)| (buf, asns.into_iter().map(u32::from).collect()))?
|
||||
};
|
||||
Ok((
|
||||
buf,
|
||||
AsPathSegment {
|
||||
ordered: typ == 2,
|
||||
path,
|
||||
},
|
||||
))
|
||||
};
|
||||
|
||||
// Not many0: that stops quietly at a segment it cannot read, and a
|
||||
// truncated segment is an error.
|
||||
let mut segments = Vec::new();
|
||||
let mut buf = buf;
|
||||
while !buf.is_empty() {
|
||||
let (rest, segment) = parse_segment(buf)?;
|
||||
segments.push(segment);
|
||||
buf = rest;
|
||||
}
|
||||
Ok((buf, Self { segments }))
|
||||
}
|
||||
|
||||
/// Writes the path with AS numbers as wide as the session negotiated.
|
||||
/// On a two-octet session an AS number that does not fit is written as
|
||||
/// AS_TRANS (RFC 6793 §4.2.2); [`AsPathAttribute::as4_path`] is what
|
||||
/// carries the real numbers alongside.
|
||||
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<(), EncodeError> {
|
||||
self.to_wire_with_width(ctx.four_octet_asn, out)
|
||||
}
|
||||
|
||||
fn to_wire_with_width(&self, four_octet: bool, out: &mut BytesMut) -> Result<(), EncodeError> {
|
||||
for segment in &self.segments {
|
||||
out.put_u8(if segment.ordered { 2 } else { 1 });
|
||||
let count = u8::try_from(segment.path.len()).map_err(|_| EncodeError::TooLong {
|
||||
what: "AS path segment",
|
||||
len: segment.path.len(),
|
||||
max: u8::MAX.into(),
|
||||
})?;
|
||||
out.put_u8(count);
|
||||
for asn in &segment.path {
|
||||
if four_octet {
|
||||
out.put_u32(*asn);
|
||||
} else {
|
||||
out.put_u16(u16::try_from(*asn).unwrap_or(AS_TRANS));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The AS4_PATH to send with this path on a two-octet session, if one is
|
||||
/// needed: it is when some AS number does not fit in two octets and so
|
||||
/// goes out as AS_TRANS in the AS_PATH itself (RFC 6793 §4.2.2).
|
||||
pub fn as4_path(&self) -> Option<PathAttribute> {
|
||||
self.segments
|
||||
.iter()
|
||||
.flat_map(|segment| &segment.path)
|
||||
.any(|asn| u16::try_from(*asn).is_err())
|
||||
.then(|| PathAttribute::As4Path(self.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct NextHopPathAttribute(pub Ipv4Addr);
|
||||
|
||||
impl NextHopPathAttribute {
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let (buf, address) = be_u32(buf)?;
|
||||
Ok((buf, Self(Ipv4Addr::from(address))))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
out.put_u32(self.0.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct MultiExitDiscPathAttribute(pub u32);
|
||||
|
||||
impl MultiExitDiscPathAttribute {
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let (buf, value) = be_u32(buf)?;
|
||||
Ok((buf, Self(value)))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
out.put_u32(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct LocalPrefPathAttribute(pub u32);
|
||||
|
||||
impl LocalPrefPathAttribute {
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let (buf, value) = be_u32(buf)?;
|
||||
Ok((buf, Self(value)))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
out.put_u32(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// ATOMIC_AGGREGATE: a marker with no value (RFC 4271 §5.1.6).
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct AtomicAggregatePathAttribute {}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct AggregatorPathAttribute {
|
||||
pub asn: u32,
|
||||
pub ip: Ipv4Addr,
|
||||
}
|
||||
|
||||
impl AggregatorPathAttribute {
|
||||
/// The AS number is as wide as the session's AS numbers (RFC 6793), so
|
||||
/// the attribute is six or eight octets.
|
||||
pub fn from_wire<'a>(ctx: &ParserContext, buf: &'a [u8]) -> Parsed<'a, Self> {
|
||||
let (buf, asn) = if ctx.four_octet_asn {
|
||||
be_u32(buf)?
|
||||
} else {
|
||||
be_u16(buf).map(|(buf, asn)| (buf, u32::from(asn)))?
|
||||
};
|
||||
let (buf, ip) = be_u32(buf)?;
|
||||
Ok((
|
||||
buf,
|
||||
Self {
|
||||
asn,
|
||||
ip: Ipv4Addr::from(ip),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// On a two-octet session an AS number that does not fit is written as
|
||||
/// AS_TRANS (RFC 6793 §4.2.2).
|
||||
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) {
|
||||
if ctx.four_octet_asn {
|
||||
out.put_u32(self.asn);
|
||||
} else {
|
||||
out.put_u16(u16::try_from(self.asn).unwrap_or(AS_TRANS));
|
||||
}
|
||||
out.put_u32(self.ip.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct CommunitiesPathAttribute(pub Vec<(u16, u16)>);
|
||||
|
||||
impl CommunitiesPathAttribute {
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
// At least one: an empty COMMUNITIES is malformed (RFC 7606 §7.8).
|
||||
let (buf, values) = nom::multi::many1(|i| (be_u16, be_u16).parse(i)).parse(buf)?;
|
||||
Ok((buf, CommunitiesPathAttribute(values)))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
for value in &self.0 {
|
||||
out.put_u16(value.0);
|
||||
out.put_u16(value.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// EXTENDED COMMUNITIES (RFC 4360).
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||
pub struct ExtendedCommunitiesPathAttribute {
|
||||
pub extended_communities: Vec<ExtendedCommunity>,
|
||||
}
|
||||
|
||||
impl ExtendedCommunitiesPathAttribute {
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let (communities, rest) = buf.as_chunks::<8>();
|
||||
if communities.is_empty() || !rest.is_empty() {
|
||||
return Err(malformed(format!(
|
||||
"{} octets are not a list of extended communities",
|
||||
buf.len()
|
||||
)));
|
||||
}
|
||||
Ok((
|
||||
rest,
|
||||
Self {
|
||||
extended_communities: communities.iter().copied().map(ExtendedCommunity).collect(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
for community in &self.extended_communities {
|
||||
out.put_slice(&community.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One extended community: eight octets, of which the first is the type
|
||||
/// and, for most types, the second a sub-type (RFC 4360 §2). How the rest
|
||||
/// divides into an administrator and a value depends on the type, and is
|
||||
/// left to whoever acts on a particular community.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize)]
|
||||
pub struct ExtendedCommunity(pub [u8; 8]);
|
||||
|
||||
impl ExtendedCommunity {
|
||||
pub fn typ(&self) -> u8 {
|
||||
self.0[0]
|
||||
}
|
||||
|
||||
pub fn sub_typ(&self) -> u8 {
|
||||
self.0[1]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
pub struct LargeCommunitiesPathAttribute {
|
||||
pub communities: Vec<LargeCommunity>,
|
||||
}
|
||||
|
||||
impl LargeCommunitiesPathAttribute {
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let (buf, communities) = nom::multi::many1(LargeCommunity::from_wire).parse(buf)?;
|
||||
Ok((buf, Self { communities }))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
for community in &self.communities {
|
||||
community.to_wire(out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
pub struct LargeCommunity {
|
||||
pub global_admin: u32,
|
||||
pub data_1: u32,
|
||||
pub data_2: u32,
|
||||
}
|
||||
|
||||
impl LargeCommunity {
|
||||
pub fn from_wire(buf: &[u8]) -> Parsed<'_, Self> {
|
||||
let (buf, global_admin) = be_u32(buf)?;
|
||||
let (buf, data_1) = be_u32(buf)?;
|
||||
let (buf, data_2) = be_u32(buf)?;
|
||||
Ok((
|
||||
buf,
|
||||
Self {
|
||||
global_admin,
|
||||
data_1,
|
||||
data_2,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
out.put_u32(self.global_admin);
|
||||
out.put_u32(self.data_1);
|
||||
out.put_u32(self.data_2);
|
||||
}
|
||||
}
|
||||
|
||||
/// The next hop of routes in MP_REACH_NLRI.
|
||||
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Hash)]
|
||||
pub enum NlriNextHop {
|
||||
Ipv4(Ipv4Addr),
|
||||
/// A global IPv6 address.
|
||||
Ipv6(Ipv6Addr),
|
||||
/// A global IPv6 address and a link-local one (RFC 2545 §3).
|
||||
Ipv6WithLl {
|
||||
global: Ipv6Addr,
|
||||
link_local: Ipv6Addr,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Display for NlriNextHop {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
NlriNextHop::Ipv4(addr) => write!(f, "{addr}"),
|
||||
NlriNextHop::Ipv6(addr) => write!(f, "{addr}"),
|
||||
NlriNextHop::Ipv6WithLl { global, link_local } => {
|
||||
write!(f, "{global} (link-local {link_local})")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NlriNextHop {
|
||||
/// Reads a next hop that is all of `bytes`. `None` if they are not the
|
||||
/// length of one.
|
||||
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
|
||||
let v6 = |bytes: &[u8]| <[u8; 16]>::try_from(bytes).ok().map(Ipv6Addr::from);
|
||||
match bytes.len() {
|
||||
4 => <[u8; 4]>::try_from(bytes)
|
||||
.ok()
|
||||
.map(|a| Self::Ipv4(a.into())),
|
||||
16 => v6(bytes).map(Self::Ipv6),
|
||||
32 => Some(Self::Ipv6WithLl {
|
||||
global: v6(&bytes[..16])?,
|
||||
link_local: v6(&bytes[16..])?,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, out: &mut BytesMut) {
|
||||
match self {
|
||||
NlriNextHop::Ipv4(address) => out.put_slice(&address.octets()),
|
||||
NlriNextHop::Ipv6(address) => out.put_slice(&address.octets()),
|
||||
NlriNextHop::Ipv6WithLl { global, link_local } => {
|
||||
out.put_slice(&global.octets());
|
||||
out.put_slice(&link_local.octets());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// How many octets the next hop is on the wire.
|
||||
pub fn wire_len(&self) -> u8 {
|
||||
match self {
|
||||
NlriNextHop::Ipv4(_) => 4,
|
||||
NlriNextHop::Ipv6(_) => 16,
|
||||
NlriNextHop::Ipv6WithLl { .. } => 32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses one prefix as an UPDATE names it: with a path identifier in
|
||||
/// front if the session uses them for the family (RFC 7911 §3).
|
||||
fn parse_nlri(version: IpVersion, with_path_id: bool, buf: &[u8]) -> Parsed<'_, Nlri> {
|
||||
let (buf, path_id) = if with_path_id { be_u32(buf)? } else { (buf, 0) };
|
||||
let (buf, prefix_len) = be_u8(buf)?;
|
||||
let byte_len = usize::from(prefix_len.div_ceil(8));
|
||||
let (buf, prefix_bytes) = nom::bytes::take(byte_len).parse(buf)?;
|
||||
let prefix =
|
||||
IpPrefix::new(version, prefix_bytes, prefix_len).map_err(|e| malformed(e.to_string()))?;
|
||||
Ok((buf, Nlri { prefix, path_id }))
|
||||
}
|
||||
|
||||
/// Parses a field that is nothing but prefixes, to its end.
|
||||
pub(crate) fn parse_nlri_list(
|
||||
version: IpVersion,
|
||||
with_path_id: bool,
|
||||
mut buf: &[u8],
|
||||
) -> Parsed<'_, Vec<Nlri>> {
|
||||
// Not many0: that stops quietly at a prefix it cannot read.
|
||||
let mut nlri = Vec::new();
|
||||
while !buf.is_empty() {
|
||||
let (rest, parsed) = parse_nlri(version, with_path_id, buf)?;
|
||||
nlri.push(parsed);
|
||||
buf = rest;
|
||||
}
|
||||
Ok((buf, nlri))
|
||||
}
|
||||
|
||||
/// The IP version of the prefixes that make up the NLRI of `family`.
|
||||
///
|
||||
/// Any family can be named in MP_REACH_NLRI and MP_UNREACH_NLRI, but only
|
||||
/// unicast and multicast routes of IPv4 and IPv6 are plain prefixes, and
|
||||
/// those are the only NLRI this parser reads. A peer does not send routes
|
||||
/// of a family that was not negotiated, so the others do not come up.
|
||||
fn ip_version_of_nlri<'a>(
|
||||
family: AfiSafi,
|
||||
) -> Result<IpVersion, nom::Err<BgpParserError<&'a [u8]>>> {
|
||||
let unsupported = || malformed(format!("NLRI of {family} is not supported"));
|
||||
if family.safi != Safi::UNICAST && family.safi != Safi::MULTICAST {
|
||||
return Err(unsupported());
|
||||
}
|
||||
IpVersion::try_from(family.afi).map_err(|_| unsupported())
|
||||
}
|
||||
|
||||
fn parse_family(buf: &[u8]) -> Parsed<'_, AfiSafi> {
|
||||
let (buf, afi) = be_u16(buf)?;
|
||||
let (buf, safi) = be_u8(buf)?;
|
||||
Ok((buf, AfiSafi::new(Afi(afi), Safi(safi))))
|
||||
}
|
||||
|
||||
/// MP_REACH_NLRI: routes of a family other than the one in the message
|
||||
/// body, with their next hop (RFC 4760 §3).
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
pub struct MpReachNlriPathAttribute {
|
||||
pub family: AfiSafi,
|
||||
pub next_hop: NlriNextHop,
|
||||
pub prefixes: Vec<Nlri>,
|
||||
}
|
||||
|
||||
impl MpReachNlriPathAttribute {
|
||||
pub fn from_wire<'a>(ctx: &ParserContext, buf: &'a [u8]) -> Parsed<'a, Self> {
|
||||
let (buf, family) = parse_family(buf)?;
|
||||
let version = ip_version_of_nlri(family)?;
|
||||
|
||||
let (buf, next_hop_len) = be_u8(buf)?;
|
||||
let (buf, next_hop) = nom::bytes::take(usize::from(next_hop_len)).parse(buf)?;
|
||||
// For IPv6 routes, a global address, optionally followed by a
|
||||
// link-local one (RFC 2545). For IPv4 routes an IPv4 address, or
|
||||
// the same as for IPv6: routes over a link that has only IPv6
|
||||
// addresses (RFC 8950). Which of the two it is shows in its length.
|
||||
let next_hop = NlriNextHop::from_bytes(next_hop)
|
||||
.filter(|next_hop| {
|
||||
version == IpVersion::Ipv4 || !matches!(next_hop, NlriNextHop::Ipv4(_))
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
malformed(format!(
|
||||
"a next hop of {next_hop_len} octets is not valid for {family}"
|
||||
))
|
||||
})?;
|
||||
|
||||
// Reserved octet (formerly the SNPA count), for every family.
|
||||
let (buf, _) = be_u8(buf)?;
|
||||
let (buf, prefixes) = parse_nlri_list(version, ctx.has_add_path(family), buf)?;
|
||||
Ok((
|
||||
buf,
|
||||
Self {
|
||||
family,
|
||||
next_hop,
|
||||
prefixes,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) {
|
||||
out.put_u16(self.family.afi.0);
|
||||
out.put_u8(self.family.safi.0);
|
||||
out.put_u8(self.next_hop.wire_len());
|
||||
self.next_hop.to_wire(out);
|
||||
out.put_u8(0);
|
||||
write_nlri(&self.prefixes, ctx.has_add_path(self.family), out);
|
||||
}
|
||||
}
|
||||
|
||||
/// MP_UNREACH_NLRI: withdrawals of routes of a family other than the one
|
||||
/// in the message body (RFC 4760 §4).
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
pub struct MpUnreachNlriPathAttribute {
|
||||
pub family: AfiSafi,
|
||||
pub prefixes: Vec<Nlri>,
|
||||
}
|
||||
|
||||
impl MpUnreachNlriPathAttribute {
|
||||
pub fn from_wire<'a>(ctx: &ParserContext, buf: &'a [u8]) -> Parsed<'a, Self> {
|
||||
let (buf, family) = parse_family(buf)?;
|
||||
let version = ip_version_of_nlri(family)?;
|
||||
let (buf, prefixes) = parse_nlri_list(version, ctx.has_add_path(family), buf)?;
|
||||
Ok((buf, Self { family, prefixes }))
|
||||
}
|
||||
|
||||
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) {
|
||||
out.put_u16(self.family.afi.0);
|
||||
out.put_u8(self.family.safi.0);
|
||||
write_nlri(&self.prefixes, ctx.has_add_path(self.family), out);
|
||||
}
|
||||
}
|
||||
pub use crate::attributes::*;
|
||||
|
||||
/// Represents a BGP NOTIFICATION message per RFC 4271 §4.5.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
|
||||
@@ -9,13 +9,11 @@ repository.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
bitfield.workspace = true
|
||||
bytes.workspace = true
|
||||
nom.workspace = true
|
||||
serde.workspace = true
|
||||
serde_repr.workspace = true
|
||||
thiserror.workspace = true
|
||||
strum.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
proptest.workspace = true
|
||||
|
||||
@@ -16,7 +16,6 @@ hashbrown.workspace = true
|
||||
serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tracing.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
proptest.workspace = true
|
||||
|
||||
@@ -48,7 +48,15 @@ pub struct SelectionKey {
|
||||
/// Reads the selection key straight off attributes in the form an
|
||||
/// [`AttrSet`] keeps, without building anything. `None` if the bytes are
|
||||
/// not a well-formed list of attributes.
|
||||
///
|
||||
/// A list with the same attribute twice is not well formed (RFC 4271
|
||||
/// §5). The decoder keeps only the first of such, so none comes from a
|
||||
/// session; were one to get in some other way, the readers of a set, of
|
||||
/// which this is one, could each take a different one of the two for the
|
||||
/// route's path or next hop.
|
||||
fn scan(mut wire: &[u8]) -> Option<SelectionKey> {
|
||||
// One bit for each type of attribute there can be.
|
||||
let mut seen = [0u64; 4];
|
||||
let mut key = SelectionKey {
|
||||
local_pref: None,
|
||||
med: None,
|
||||
@@ -64,6 +72,14 @@ fn scan(mut wire: &[u8]) -> Option<SelectionKey> {
|
||||
let (attribute, rest) = split_attribute(wire).ok()?;
|
||||
wire = rest;
|
||||
let value = attribute.value;
|
||||
let (word, bit) = (
|
||||
usize::from(attribute.type_code / 64),
|
||||
1u64 << (attribute.type_code % 64),
|
||||
);
|
||||
if seen[word] & bit != 0 {
|
||||
return None;
|
||||
}
|
||||
seen[word] |= bit;
|
||||
match attribute.type_code {
|
||||
attr_type::ORIGIN => key.origin = *value.first()?,
|
||||
attr_type::AS_PATH => {
|
||||
@@ -90,7 +106,15 @@ struct PathScan {
|
||||
last: u32,
|
||||
}
|
||||
|
||||
/// Reads an AS_PATH or AS4_PATH value with four-octet AS numbers.
|
||||
impl SelectionKey {
|
||||
/// The AS the route originates from: the last of its path. `None` if
|
||||
/// the path is empty or ends in a set, which has no one last AS.
|
||||
pub fn origin(&self) -> Option<u32> {
|
||||
Some(self.origin_as).filter(|origin| *origin != 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads an AS_PATH value with four-octet AS numbers.
|
||||
fn scan_as_path(mut segments: &[u8]) -> Option<PathScan> {
|
||||
const AS_SEQUENCE: u8 = 2;
|
||||
let mut hops = 0usize;
|
||||
@@ -571,6 +595,27 @@ mod tests {
|
||||
assert!(path.is_empty());
|
||||
}
|
||||
|
||||
/// Two of the same attribute are not a set: its readers could each
|
||||
/// take a different one.
|
||||
#[test]
|
||||
fn the_same_attribute_twice_is_not_a_set() {
|
||||
let store = AttrStore::new();
|
||||
let origin = [0x40, 1, 1, 0];
|
||||
let path = |asn: u8| [0x40, 2, 6, 2, 1, 0, 0, 0, asn];
|
||||
let once = [&origin[..], &path(1)].concat();
|
||||
assert!(store.intern_wire(&once).is_some());
|
||||
let twice = [&origin[..], &path(1), &path(2)].concat();
|
||||
assert!(store.intern_wire(&twice).is_none());
|
||||
// Whatever the type, known here or not.
|
||||
let unknown = [0xc0, 200, 1, 7];
|
||||
assert!(store.intern_wire(&[&once[..], &unknown].concat()).is_some());
|
||||
assert!(
|
||||
store
|
||||
.intern_wire(&[&once[..], &unknown, &unknown].concat())
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_on_the_wire_are_interned_without_parsing_them() {
|
||||
let store = AttrStore::new();
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
//! - **Readers do not stop the table.** Reading takes a shard's lock for
|
||||
//! reading, one shard at a time, and looks at the rows in place.
|
||||
//! - **Followers go at their own pace.** What acts on the best routes reads
|
||||
//! the changes from a [`journal`], and a slow reader holds nothing up.
|
||||
//! the changes from a `journal`, and a slow reader holds nothing up.
|
||||
|
||||
pub mod attrs;
|
||||
mod journal;
|
||||
|
||||
@@ -14,12 +14,10 @@ bgp-rib.workspace = true
|
||||
bgp-fib.workspace = true
|
||||
bgp-rpki.workspace = true
|
||||
bytes.workspace = true
|
||||
clap.workspace = true
|
||||
futures.workspace = true
|
||||
prometheus.workspace = true
|
||||
socket2.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
protobuf.workspace = true
|
||||
thiserror.workspace = true
|
||||
|
||||
@@ -30,7 +30,7 @@ pub struct PeerAddress {
|
||||
impl From<SocketAddr> for PeerAddress {
|
||||
fn from(address: SocketAddr) -> Self {
|
||||
let interface = match address {
|
||||
SocketAddr::V6(v6) if is_link_local(v6.ip()) => v6.scope_id(),
|
||||
SocketAddr::V6(v6) if v6.ip().is_unicast_link_local() => v6.scope_id(),
|
||||
_ => 0,
|
||||
};
|
||||
Self {
|
||||
@@ -77,10 +77,6 @@ pub fn socket_text(address: SocketAddr) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_link_local(address: &Ipv6Addr) -> bool {
|
||||
address.segments()[0] & 0xffc0 == 0xfe80
|
||||
}
|
||||
|
||||
/// The number of the interface called `name`, if there is one now.
|
||||
pub fn interface_index(name: &str) -> Option<u32> {
|
||||
nix::net::if_::if_nametoindex(name).ok()
|
||||
@@ -131,7 +127,7 @@ impl Endpoint {
|
||||
.parse()
|
||||
.map_err(|_| EndpointProblem::NotAnAddress(text.to_string()))?;
|
||||
let link_local = match ip {
|
||||
IpAddr::V6(v6) if is_link_local(&v6) => Some(v6),
|
||||
IpAddr::V6(v6) if v6.is_unicast_link_local() => Some(v6),
|
||||
_ => None,
|
||||
};
|
||||
let interface = match (link_local, interface) {
|
||||
@@ -199,7 +195,9 @@ impl Endpoint {
|
||||
)
|
||||
})?;
|
||||
Ok(match ip {
|
||||
IpAddr::V6(v6) if is_link_local(&v6) => SocketAddrV6::new(v6, port, 0, scope).into(),
|
||||
IpAddr::V6(v6) if v6.is_unicast_link_local() => {
|
||||
SocketAddrV6::new(v6, port, 0, scope).into()
|
||||
}
|
||||
ip => SocketAddr::new(ip, port),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ use tokio::sync::broadcast::error::RecvError;
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::address::is_link_local;
|
||||
use crate::policy::PeerRole;
|
||||
use crate::session::{PeerId, SessionClosed, SessionEvent, SessionHandle, SessionRegistry};
|
||||
|
||||
@@ -404,7 +403,7 @@ pub fn build_announce(route: &LocalRoute, towards: &Towards) -> Option<UpdateMes
|
||||
/// has no form for a link-local address alone). FRR sends it so, and takes
|
||||
/// the route for unreachable if it comes by itself; BIRD takes either.
|
||||
fn ipv6_next_hop(ours: Ipv6Addr) -> NlriNextHop {
|
||||
if is_link_local(&ours) {
|
||||
if ours.is_unicast_link_local() {
|
||||
NlriNextHop::Ipv6WithLl {
|
||||
global: ours,
|
||||
link_local: ours,
|
||||
|
||||
@@ -27,7 +27,8 @@ const MAX_AS_PATH_PREPEND: u32 = 16;
|
||||
pub struct Config {
|
||||
pub router_id: Ipv4Addr,
|
||||
pub local_asn: u32,
|
||||
/// Default hold time advertised to peers that have no per-peer override.
|
||||
/// The hold time offered to a peer that is not in the configuration
|
||||
/// (see `accept_unconfigured_peers`). Configured peers have their own.
|
||||
pub hold_time_secs: u16,
|
||||
pub listen_endpoints: Vec<SocketAddr>,
|
||||
pub peers: Vec<PeerConfig>,
|
||||
@@ -461,6 +462,45 @@ mod tests {
|
||||
parse(text).err().expect(text)
|
||||
}
|
||||
|
||||
/// The example that the documentation points to, and that operators
|
||||
/// start from: as it is, and with what it shows commented out put in.
|
||||
#[test]
|
||||
fn the_example_configuration_is_in_order() {
|
||||
let example = include_str!("../../../testdata/server.textpb");
|
||||
let config = parse(example).expect("the example as it is");
|
||||
assert_eq!(config.peers.len(), 1);
|
||||
assert!(config.peers[0].import.accept_third_party_next_hop);
|
||||
assert!(config.rpki.is_none() && config.kernel.is_none());
|
||||
|
||||
// A commented line that is a field, or a block on one line, is
|
||||
// configuration shown for the taking.
|
||||
let shown: String = example
|
||||
.lines()
|
||||
.map(|line| {
|
||||
let (indent, rest) = line.split_at(line.len() - line.trim_start().len());
|
||||
let field = rest.strip_prefix("# ").filter(|field| {
|
||||
let name_ends = field.find([':', ' ']).unwrap_or(0);
|
||||
let is_name = name_ends > 0
|
||||
&& field[..name_ends]
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_lowercase() || c == '_');
|
||||
let after = &field[name_ends..];
|
||||
is_name
|
||||
&& (after.starts_with(": ") && !after[2..].contains(' ')
|
||||
|| after.starts_with(" {") && after.ends_with('}'))
|
||||
});
|
||||
format!("{indent}{}\n", field.unwrap_or(rest))
|
||||
})
|
||||
.collect();
|
||||
assert_ne!(shown, example, "nothing was taken for configuration");
|
||||
let config = parse(&shown).unwrap_or_else(|e| panic!("{e}\n{shown}"));
|
||||
assert!(config.rpki.is_some(), "{shown}");
|
||||
assert!(config.kernel.is_some(), "{shown}");
|
||||
assert_eq!(config.peers[0].role, Some(PeerRole::Peer));
|
||||
assert!(config.peers[0].require_role);
|
||||
assert!(config.peers[0].import.prefixes.is_some());
|
||||
}
|
||||
|
||||
const BASE: &str = "router_id: \"192.0.2.1\" local_asn: 65000\n";
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -239,7 +239,7 @@ struct Established {
|
||||
/// The routes learned from the peer, which go when the session does.
|
||||
routes: PeerRoutes,
|
||||
/// Whose routes the import policy is judging.
|
||||
from: policy::From,
|
||||
from: policy::RouteSource,
|
||||
/// How many routes the peer has, and how many were refused.
|
||||
counters: Arc<SessionCounters>,
|
||||
/// The same number of routes, where metrics are read from.
|
||||
@@ -258,6 +258,26 @@ struct Established {
|
||||
}
|
||||
|
||||
impl Established {
|
||||
/// Whose routes are judged and by what rules, and where those that
|
||||
/// are invalid for now are held.
|
||||
fn import<'a>(
|
||||
&'a mut self,
|
||||
policy: &'a ImportPolicy,
|
||||
refusals: &'a PeerRefusals,
|
||||
) -> (routes::Import<'a>, &'a mut Held) {
|
||||
let import = routes::Import {
|
||||
peer: self.rib_peer,
|
||||
from: self.from,
|
||||
policy,
|
||||
checks: self
|
||||
.validation
|
||||
.as_mut()
|
||||
.map_or(Checks::NONE, |validation| validation.validator.checks()),
|
||||
refusals,
|
||||
};
|
||||
(import, &mut self.held)
|
||||
}
|
||||
|
||||
/// Keeps count after the peer's routes have changed, and says whether
|
||||
/// the peer now has more than it may.
|
||||
fn account(
|
||||
@@ -787,7 +807,7 @@ impl Session {
|
||||
rib_peer,
|
||||
outbound,
|
||||
routes: PeerRoutes::new(Arc::clone(&self.server.rib), rib_peer),
|
||||
from: policy::From {
|
||||
from: policy::RouteSource {
|
||||
peer_asn: negotiated.peer_asn,
|
||||
local_asn: self.server.local_asn,
|
||||
ibgp: negotiated.ibgp,
|
||||
@@ -813,23 +833,9 @@ impl Session {
|
||||
let Some(established) = &mut self.established else {
|
||||
return Ok(());
|
||||
};
|
||||
let import = routes::Import {
|
||||
peer: established.rib_peer,
|
||||
from: established.from,
|
||||
policy: &self.import,
|
||||
checks: established
|
||||
.validation
|
||||
.as_mut()
|
||||
.map_or(Checks::NONE, |validation| validation.validator.checks()),
|
||||
refusals: &self.refusals,
|
||||
};
|
||||
let routes::Applied { outcome, rejected } = routes::apply(
|
||||
&self.server.rib,
|
||||
&import,
|
||||
&mut established.held,
|
||||
update,
|
||||
attributes_wire,
|
||||
)?;
|
||||
let (import, held) = established.import(&self.import, &self.refusals);
|
||||
let routes::Applied { outcome, rejected } =
|
||||
routes::apply(&self.server.rib, &import, held, update, attributes_wire)?;
|
||||
// One line for every UPDATE is too much to log by default: a full
|
||||
// table is a million of them.
|
||||
debug!(
|
||||
@@ -851,18 +857,9 @@ impl Session {
|
||||
let Some(established) = &mut self.established else {
|
||||
return vec![];
|
||||
};
|
||||
let import = routes::Import {
|
||||
peer: established.rib_peer,
|
||||
from: established.from,
|
||||
policy: &self.import,
|
||||
checks: established
|
||||
.validation
|
||||
.as_mut()
|
||||
.map_or(Checks::NONE, |validation| validation.validator.checks()),
|
||||
refusals: &self.refusals,
|
||||
};
|
||||
let (import, held) = established.import(&self.import, &self.refusals);
|
||||
let routes::Applied { outcome, rejected } =
|
||||
routes::revalidate(&self.server.rib, &import, &mut established.held, suspects);
|
||||
routes::revalidate(&self.server.rib, &import, held, suspects);
|
||||
if outcome != Outcome::default() {
|
||||
info!(
|
||||
peer = %established.peer_id,
|
||||
@@ -1274,7 +1271,7 @@ mod tests {
|
||||
};
|
||||
let import = routes::Import {
|
||||
peer,
|
||||
from: policy::From {
|
||||
from: policy::RouteSource {
|
||||
peer_asn: 65001,
|
||||
local_asn: 65000,
|
||||
ibgp: false,
|
||||
|
||||
@@ -2,11 +2,11 @@ pub mod address;
|
||||
pub mod advertise;
|
||||
pub mod collision;
|
||||
pub mod config;
|
||||
pub mod connection;
|
||||
pub mod dialer;
|
||||
mod connection;
|
||||
mod dialer;
|
||||
pub mod error;
|
||||
pub mod family;
|
||||
pub mod fsm;
|
||||
mod fsm;
|
||||
pub mod metrics;
|
||||
mod panic;
|
||||
pub mod policy;
|
||||
@@ -17,7 +17,6 @@ pub mod server;
|
||||
pub mod session;
|
||||
mod sync;
|
||||
mod throttle;
|
||||
pub mod tracing;
|
||||
|
||||
pub mod proto {
|
||||
include!(concat!(env!("OUT_DIR"), "/protos/mod.rs"));
|
||||
|
||||
@@ -164,20 +164,53 @@ pub static RIB_JOURNAL_ENTRIES: LazyLock<IntGauge> = LazyLock::new(|| {
|
||||
.expect("metric registration")
|
||||
});
|
||||
|
||||
/// Whether each configured peer's session is up: what to alert on when a
|
||||
/// peer goes away, which a count of sessions cannot say.
|
||||
pub static PEER_UP: LazyLock<IntGaugeVec> = LazyLock::new(|| {
|
||||
register_int_gauge_vec!(
|
||||
"bgp_peer_up",
|
||||
"Whether the session with a configured peer is Established (1) or not (0)",
|
||||
&["peer"]
|
||||
)
|
||||
.expect("metric registration")
|
||||
});
|
||||
|
||||
pub static BUILD_INFO: LazyLock<IntGaugeVec> = LazyLock::new(|| {
|
||||
register_int_gauge_vec!(
|
||||
"bgp_build_info",
|
||||
"Always 1; the version of the daemon is in the label",
|
||||
&["version"]
|
||||
)
|
||||
.expect("metric registration")
|
||||
});
|
||||
|
||||
/// Forces registration of every metric so they appear in scrapes before
|
||||
/// their first observation.
|
||||
/// their first observation: a counter that is not there until it first
|
||||
/// counts hides that first count from `increase()`.
|
||||
pub fn init() {
|
||||
LazyLock::force(&MESSAGES_SENT);
|
||||
LazyLock::force(&MESSAGES_RECEIVED);
|
||||
LazyLock::force(&UPDATE_ERRORS);
|
||||
LazyLock::force(&SESSIONS_ESTABLISHED_TOTAL);
|
||||
LazyLock::force(&SESSIONS_ESTABLISHED);
|
||||
LazyLock::force(&ROUTES_REJECTED);
|
||||
LazyLock::force(&PEER_ROUTES);
|
||||
LazyLock::force(&PEER_MAX_PREFIXES);
|
||||
LazyLock::force(&PREFIX_LIMIT_EXCEEDED);
|
||||
LazyLock::force(&RPKI_VRPS);
|
||||
LazyLock::force(&RPKI_ASPAS);
|
||||
LazyLock::force(&RPKI_UPDATES);
|
||||
LazyLock::force(&CONNECTIONS_REFUSED);
|
||||
LazyLock::force(&COLLISIONS);
|
||||
LazyLock::force(&LOCAL_ROUTES);
|
||||
LazyLock::force(&RIB_ROUTES);
|
||||
LazyLock::force(&RIB_PREFIXES);
|
||||
LazyLock::force(&RIB_ATTRIBUTE_SETS);
|
||||
LazyLock::force(&RIB_JOURNAL_ENTRIES);
|
||||
LazyLock::force(&PEER_UP);
|
||||
BUILD_INFO
|
||||
.with_label_values(&[env!("CARGO_PKG_VERSION")])
|
||||
.set(1);
|
||||
}
|
||||
|
||||
pub fn message_type_name(msg: &Message) -> &'static str {
|
||||
|
||||
+18
-17
@@ -318,7 +318,7 @@ impl PeerRole {
|
||||
|
||||
/// Whose routes are being judged.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct From {
|
||||
pub struct RouteSource {
|
||||
pub peer_asn: u32,
|
||||
pub local_asn: u32,
|
||||
/// Whether the peer is in our own AS. The checks on the path are about
|
||||
@@ -343,7 +343,7 @@ impl ImportPolicy {
|
||||
pub fn check_path(
|
||||
&self,
|
||||
attributes: &[PathAttribute],
|
||||
from: &From,
|
||||
from: &RouteSource,
|
||||
) -> Result<Option<u32>, Rejection> {
|
||||
let path = attributes.iter().find_map(|attribute| match attribute {
|
||||
PathAttribute::AsPath(path) => Some(path),
|
||||
@@ -451,7 +451,7 @@ impl ImportPolicy {
|
||||
&self,
|
||||
next_hop: &NlriNextHop,
|
||||
routes: IpVersion,
|
||||
from: &From,
|
||||
from: &RouteSource,
|
||||
) -> Result<(), Rejection> {
|
||||
let (first, link_local): (IpAddr, Option<Ipv6Addr>) = match *next_hop {
|
||||
NlriNextHop::Ipv4(address) => (address.into(), None),
|
||||
@@ -515,7 +515,7 @@ impl PathFacts {
|
||||
/// Reads the path. It is the whole path: what a session with
|
||||
/// two-octet AS numbers brought in AS4_PATH the decoder has put in its
|
||||
/// place (RFC 6793 §4.2.3).
|
||||
fn of(path: Option<&AsPathAttribute>, from: &From) -> Self {
|
||||
fn of(path: Option<&AsPathAttribute>, from: &RouteSource) -> Self {
|
||||
let mut facts = Self {
|
||||
length: 0,
|
||||
has_local_asn: false,
|
||||
@@ -805,7 +805,7 @@ mod tests {
|
||||
IpPrefix::try_from(s).unwrap()
|
||||
}
|
||||
|
||||
const EXTERNAL: From = From {
|
||||
const EXTERNAL: RouteSource = RouteSource {
|
||||
peer_asn: 3333,
|
||||
local_asn: 1299,
|
||||
ibgp: false,
|
||||
@@ -937,7 +937,7 @@ mod tests {
|
||||
);
|
||||
}
|
||||
// From a peer in our own AS the path says nothing of the kind.
|
||||
let internal = From {
|
||||
let internal = RouteSource {
|
||||
peer_asn: 1299,
|
||||
ibgp: true,
|
||||
..EXTERNAL
|
||||
@@ -1025,7 +1025,7 @@ mod tests {
|
||||
attributes.push(PathAttribute::OnlyToCustomer(asn));
|
||||
attributes
|
||||
};
|
||||
let from = |role| From {
|
||||
let from = |role| RouteSource {
|
||||
role: Some(role),
|
||||
..EXTERNAL
|
||||
};
|
||||
@@ -1213,7 +1213,7 @@ mod tests {
|
||||
fn the_next_hop_has_to_be_the_peer() {
|
||||
let policy = ImportPolicy::default();
|
||||
let v4 = |s: &str| NlriNextHop::Ipv4(s.parse().unwrap());
|
||||
let check = |policy: &ImportPolicy, next_hop: &NlriNextHop, from: &From| {
|
||||
let check = |policy: &ImportPolicy, next_hop: &NlriNextHop, from: &RouteSource| {
|
||||
policy.check_next_hop(next_hop, IpVersion::Ipv4, from)
|
||||
};
|
||||
assert_eq!(check(&policy, &v4("192.0.2.2"), &EXTERNAL), Ok(()));
|
||||
@@ -1242,11 +1242,11 @@ mod tests {
|
||||
// Where it may be someone else: said so, a route server, our own AS.
|
||||
assert_eq!(check(&lenient, &v4("192.0.2.3"), &EXTERNAL), Ok(()));
|
||||
for from in [
|
||||
From {
|
||||
RouteSource {
|
||||
role: Some(PeerRole::RouteServer),
|
||||
..EXTERNAL
|
||||
},
|
||||
From {
|
||||
RouteSource {
|
||||
ibgp: true,
|
||||
..EXTERNAL
|
||||
},
|
||||
@@ -1254,7 +1254,7 @@ mod tests {
|
||||
assert_eq!(check(&policy, &v4("192.0.2.3"), &from), Ok(()));
|
||||
}
|
||||
// A provider is not a route server.
|
||||
let provider = From {
|
||||
let provider = RouteSource {
|
||||
role: Some(PeerRole::Provider),
|
||||
..EXTERNAL
|
||||
};
|
||||
@@ -1269,12 +1269,12 @@ mod tests {
|
||||
let policy = ImportPolicy::default();
|
||||
let peer: Ipv6Addr = "2001:db8::2".parse().unwrap();
|
||||
let peer_ll: Ipv6Addr = "fe80::2".parse().unwrap();
|
||||
let over_v6 = From {
|
||||
let over_v6 = RouteSource {
|
||||
peer_address: peer.into(),
|
||||
local_address: "2001:db8::1".parse().unwrap(),
|
||||
..EXTERNAL
|
||||
};
|
||||
let v6 = |policy: &ImportPolicy, next_hop: NlriNextHop, from: &From| {
|
||||
let v6 = |policy: &ImportPolicy, next_hop: NlriNextHop, from: &RouteSource| {
|
||||
policy.check_next_hop(&next_hop, IpVersion::Ipv6, from)
|
||||
};
|
||||
assert_eq!(v6(&policy, NlriNextHop::Ipv6(peer), &over_v6), Ok(()));
|
||||
@@ -1305,7 +1305,7 @@ mod tests {
|
||||
|
||||
// A session between link-local addresses: the peer's own, alone,
|
||||
// twice, or after nothing.
|
||||
let link_local = From {
|
||||
let link_local = RouteSource {
|
||||
peer_address: peer_ll.into(),
|
||||
local_address: "fe80::1".parse().unwrap(),
|
||||
..EXTERNAL
|
||||
@@ -1345,10 +1345,11 @@ mod tests {
|
||||
|
||||
// An IPv4 route with an IPv6 next hop only where that was agreed
|
||||
// on (RFC 8950).
|
||||
let v4_route =
|
||||
|from: &From| policy.check_next_hop(&NlriNextHop::Ipv6(peer), IpVersion::Ipv4, from);
|
||||
let v4_route = |from: &RouteSource| {
|
||||
policy.check_next_hop(&NlriNextHop::Ipv6(peer), IpVersion::Ipv4, from)
|
||||
};
|
||||
assert_eq!(v4_route(&over_v6), Err(Rejection::NextHopUnusable));
|
||||
let agreed = From {
|
||||
let agreed = RouteSource {
|
||||
extended_next_hop: true,
|
||||
..over_v6
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ use bgp_primitives::parser::EncodeError;
|
||||
use bgp_rib::table::{AttrSet, Outcome, PeerIndex, RibTable};
|
||||
use bgp_rpki::{PathValidity, Validity};
|
||||
|
||||
use crate::policy::{self, From, ImportPolicy, Rejection};
|
||||
use crate::policy::{self, ImportPolicy, Rejection, RouteSource};
|
||||
use crate::refusals::PeerRefusals;
|
||||
use crate::rpki::Checks;
|
||||
|
||||
@@ -34,7 +34,7 @@ pub(crate) struct Applied {
|
||||
/// Whose UPDATE is being applied, and by what rules.
|
||||
pub(crate) struct Import<'a> {
|
||||
pub(crate) peer: PeerIndex,
|
||||
pub(crate) from: From,
|
||||
pub(crate) from: RouteSource,
|
||||
pub(crate) policy: &'a ImportPolicy,
|
||||
/// What the peer's routes are validated against, with the data of the
|
||||
/// RPKI.
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
//! should (draft-ietf-sidrops-aspa-verification), which the ASPAs say. A
|
||||
//! route that fails either is not taken.
|
||||
//!
|
||||
//! A route is validated when it arrives (see [`crate::routes`]). But
|
||||
//! A route is validated when it arrives (see `crate::routes`). But
|
||||
//! whether a route is valid is not settled once: the data changes every
|
||||
//! few minutes, and each change can make routes that are in the table
|
||||
//! invalid, and routes that were refused as invalid acceptable. The
|
||||
//! [revalidator](spawn_revalidator) deals with the first by looking
|
||||
//! revalidator (`spawn_revalidator`) deals with the first by looking
|
||||
//! through the table; each session deals with the second, from the routes
|
||||
//! it has [held](crate::routes::Held).
|
||||
//! it has held (`crate::routes::Held`).
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
@@ -142,7 +142,7 @@ impl<'a> Checks<'a> {
|
||||
path: &mut Vec<u32>,
|
||||
) -> Option<Rejection> {
|
||||
if let Some(vrps) = self.vrps {
|
||||
let origin = Some(attrs.key().origin_as).filter(|origin| *origin != 0);
|
||||
let origin = attrs.key().origin();
|
||||
if vrps.validate(prefix, origin) == Validity::Invalid {
|
||||
return Some(Rejection::RpkiInvalid);
|
||||
}
|
||||
@@ -295,7 +295,7 @@ fn newly_invalid(
|
||||
&& vrps_may_matter
|
||||
&& changed.is_none_or(|changed| changed.vrps.covers(&route.prefix))
|
||||
{
|
||||
let origin = Some(route.attrs.key().origin_as).filter(|origin| *origin != 0);
|
||||
let origin = route.attrs.key().origin();
|
||||
if data.vrps.validate(&route.prefix, origin) == Validity::Invalid {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ use crate::address::{Endpoint, PeerAddress, socket_text};
|
||||
use crate::collision::Direction;
|
||||
use crate::family::FamilySet;
|
||||
use crate::fsm::Negotiated;
|
||||
use crate::metrics::SESSIONS_ESTABLISHED;
|
||||
use crate::metrics::{PEER_UP, SESSIONS_ESTABLISHED};
|
||||
use crate::policy::PeerRole;
|
||||
use crate::rpki::Subject;
|
||||
use crate::sync::IgnorePoisonRw;
|
||||
@@ -262,6 +262,9 @@ impl SessionRegistry {
|
||||
.iter()
|
||||
.map(|p| (p.name.clone(), DialState::Idle))
|
||||
.collect();
|
||||
for peer in &configured {
|
||||
PEER_UP.with_label_values(&[&peer.name]).set(0);
|
||||
}
|
||||
Self {
|
||||
inner: Arc::new(RwLock::new(HashMap::new())),
|
||||
configured: Arc::new(configured),
|
||||
@@ -306,6 +309,9 @@ impl SessionRegistry {
|
||||
let peer = handle.peer;
|
||||
self.inner.writing().insert(peer, handle.clone());
|
||||
SESSIONS_ESTABLISHED.inc();
|
||||
if let Some(name) = &handle.name {
|
||||
PEER_UP.with_label_values(&[name]).set(1);
|
||||
}
|
||||
let _ = self.events.send(SessionEvent::Up(handle));
|
||||
SessionGuard {
|
||||
registry: self.clone(),
|
||||
@@ -362,8 +368,11 @@ impl SessionRegistry {
|
||||
|
||||
fn remove(&self, peer: &PeerId) {
|
||||
let removed = self.inner.writing().remove(peer);
|
||||
if removed.is_some() {
|
||||
if let Some(handle) = &removed {
|
||||
SESSIONS_ESTABLISHED.dec();
|
||||
if let Some(name) = &handle.name {
|
||||
PEER_UP.with_label_values(&[name]).set(0);
|
||||
}
|
||||
let _ = self.events.send(SessionEvent::Down(*peer));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
use clap::Args;
|
||||
use tracing::Level;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use tracing_subscriber::filter::ParseError;
|
||||
|
||||
/// Default targets that we enable logging for.
|
||||
const DEFAULT_TARGETS: [&str; 3] = ["bgp", "bgp_server", "bgp_web"];
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct TracingArgs {
|
||||
/// Set the minimum log level. At `debug` and below there is a line for
|
||||
/// every UPDATE and every route announced.
|
||||
#[arg(short, long, global = true, default_value_t = Level::INFO, help_heading = "Tracing")]
|
||||
level: Level,
|
||||
}
|
||||
|
||||
impl TracingArgs {
|
||||
pub fn install(&self) -> Result<(), ParseError> {
|
||||
let filter = DEFAULT_TARGETS.into_iter().try_fold(
|
||||
EnvFilter::builder().from_env_lossy(),
|
||||
|filter, target| -> Result<_, ParseError> {
|
||||
Ok(filter.add_directive(format!("{}={}", target, self.level).parse()?))
|
||||
},
|
||||
)?;
|
||||
|
||||
tracing_subscriber::fmt().with_env_filter(filter).init();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -346,7 +346,7 @@ impl PeerNames {
|
||||
/// the route is on its way out of the table.
|
||||
pub(crate) fn view(&self, route: &Route) -> Option<ReceivedRouteView> {
|
||||
let (peer, name, check) = self.peers.get(&route.peer)?;
|
||||
let origin = Some(route.attrs.key().origin_as).filter(|origin| *origin != 0);
|
||||
let origin = route.attrs.key().origin();
|
||||
// What a session stored was accepted by the decoder, and parses.
|
||||
let (attributes, next_hop) = route.attrs.attributes().unwrap_or_default();
|
||||
Some(ReceivedRouteView {
|
||||
|
||||
@@ -63,7 +63,7 @@ impl WireEvent {
|
||||
peer: PeerView::from(&h),
|
||||
},
|
||||
Ok(SessionEvent::Down(p)) => Self::SessionDown {
|
||||
peer_addr: p.addr.to_string(),
|
||||
peer_addr: bgp_server::address::socket_text(p.addr),
|
||||
peer_asn: p.asn,
|
||||
},
|
||||
Ok(SessionEvent::RefreshRequested(h)) => Self::RouteRefreshRequested {
|
||||
|
||||
+3
-13
@@ -9,7 +9,7 @@ mod api;
|
||||
mod events;
|
||||
mod metrics;
|
||||
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::net::Ipv4Addr;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Instant, SystemTime};
|
||||
|
||||
@@ -123,18 +123,8 @@ pub fn router(state: AppState) -> Router {
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
/// Binds `listen` and serves the API until the task is dropped.
|
||||
pub async fn serve(state: AppState, listen: SocketAddr) -> std::io::Result<()> {
|
||||
let listener = TcpListener::bind(listen).await.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
e.kind(),
|
||||
format!("failed to bind HTTP listener on {listen}: {e}"),
|
||||
)
|
||||
})?;
|
||||
serve_on(state, listener).await
|
||||
}
|
||||
|
||||
/// Serves the API on an already-bound listener (lets tests use port 0).
|
||||
/// Serves the API on `listener` until the task is dropped. The caller
|
||||
/// binds it, and so finds out before anything else starts if it cannot.
|
||||
pub async fn serve_on(state: AppState, listener: TcpListener) -> std::io::Result<()> {
|
||||
info!(addr = %listener.local_addr()?, "HTTP API listening");
|
||||
axum::serve(listener, router(state)).await?;
|
||||
|
||||
Reference in new issue
Block a user