Files
ferrix/crates/packet/src/message.rs
T
Rayhaan JaufeerallyandClaude Fable 5.1 9ca86ede2f Harden import, the kernel task and the RTR client after review
Three independent reviews (hijack resistance, reliability, code health)
of the whole tree. This is what they found that could let a bad route in
or take the daemon down.

Hijacks:
- A route had two paths on a two-octet session, AS_PATH and AS4_PATH,
  read in two places by two rules. A peer could make the origin checked
  on import differ from the one checked at revalidation, and get an
  RPKI-invalid route into the table. AS4_PATH is now merged into AS_PATH
  once, on receipt (RFC 6793 §4.2.3), and nothing else knows of it.
- The next hop is judged on import: it must be usable, agreed on
  (RFC 8950), and by default the peer itself.
- Our own originated prefixes are not taken from other networks.
- A route for a network the machine is on is never put in the kernel.
- Paths with an AS_SET are refused (RFC 9774).
- A configured peer gets one session, not one per BGP Identifier.
- More IPv6 special-purpose prefixes are bogons.

Reliability:
- A batch of requests the kernel refused could overflow the netlink
  socket and end the daemon. The socket has room now, a lost answer or a
  silent kernel means asking what is there, and neither is fatal.
- Working the routes out again within the grace period after a restart
  no longer drops the routes being kept.
- That work can no longer be put off for ever by interface changes.
- Peers whose sessions keep failing are slowed down.
- Unused attribute sets are swept when they pile up, not only each
  minute.
- Log lines a peer can cause are said once in a while.
- An RTR answer is bounded in size and time; data cannot expire between
  two refreshes.
- The shard of a key is taken from a stirred hash: neighbouring bits of
  the hash proved alike for prefixes in a row. The test that showed it,
  failing one run in sixteen, is deterministic now.

The lab has a phase, hijacks, in which FRR tries the first three.
route::apply is a struct with methods instead of nested closures, and
the kernel task's loop is shorter.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 19:13:40 +02:00

2190 lines
78 KiB
Rust

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::parser::BgpParserError;
use bgp_primitives::parser::EncodeError;
use bgp_primitives::parser::ParserContext;
use bgp_primitives::parser::ProtocolErrorValues;
use crate::errors::BgpError;
use crate::errors::DecodeError;
use crate::errors::MsgHeaderSubcode;
use crate::errors::OpenMsgSubcode;
use crate::open::OpenMessage;
/// A sentinel AS number used to denote that the actual AS number is provided as
/// a 4 byte value in the capabilities instead.
pub const AS_TRANS: u16 = 23456;
/// Version number used in the BGP4 protocol.
pub const BGP4_VERSION: u8 = 4;
/// Total length of the BGP message header (marker + length + type).
pub const BGP_HEADER_LEN: u16 = 19;
/// Maximum BGP message length per RFC 4271 §4.1.
pub const BGP_MAX_MSG_LEN: u16 = 4096;
const BGP_MARKER: [u8; 16] = [0xFF; 16];
/// BGP message type codes (RFC 4271 §4.1, RFC 2918).
mod msg_type {
pub const OPEN: u8 = 1;
pub const UPDATE: u8 = 2;
pub const NOTIFICATION: u8 = 3;
pub const KEEPALIVE: u8 = 4;
pub const ROUTE_REFRESH: u8 = 5;
}
/// Message represents the top-level messages in the BGP protocol.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum Message {
Open(OpenMessage),
Update(UpdateMessage),
Notification(NotificationMessage),
KeepAlive,
/// BGP Route Refresh message (RFC 2918).
RouteRefresh(RouteRefreshMessage),
}
fn header_error(subcode: MsgHeaderSubcode, data: Vec<u8>, detail: String) -> DecodeError {
DecodeError {
notification: BgpError::MsgHeader(subcode),
data,
detail,
}
}
impl Message {
/// Decodes one complete BGP message, header included, from the start of
/// `buf`. Bytes after the message are ignored.
///
/// A failure says which NOTIFICATION reports it, following RFC 4271 §6.1
/// for the header and §6.2 and §6.3 for OPEN and UPDATE bodies.
///
/// An UPDATE comes back as it is to be acted on: errors that RFC 7606
/// handles without resetting the session have been handled. Use
/// [`Message::decode_with_report`] to learn of them.
pub fn decode(ctx: &ParserContext, buf: &[u8]) -> Result<Self, DecodeError> {
Self::decode_with_report(ctx, buf).map(|decoded| decoded.message)
}
/// As [`Message::decode`], and also returns what else decoding an
/// UPDATE found out: the errors in it that were handled without
/// resetting the session, and its attributes as they were on the wire.
pub fn decode_with_report(ctx: &ParserContext, buf: &[u8]) -> Result<Decoded, DecodeError> {
let header_len = usize::from(BGP_HEADER_LEN);
if buf.len() < header_len {
return Err(header_error(
MsgHeaderSubcode::BadMessageLength,
vec![],
format!("{} bytes is shorter than a BGP header", buf.len()),
));
}
if buf[..16] != BGP_MARKER {
return Err(header_error(
MsgHeaderSubcode::ConnNotSynchronized,
vec![],
"invalid BGP marker: expected 16 x 0xFF".into(),
));
}
let length = u16::from_be_bytes([buf[16], buf[17]]);
let type_code = buf[18];
if !(BGP_HEADER_LEN..=BGP_MAX_MSG_LEN).contains(&length) || usize::from(length) > buf.len()
{
return Err(header_error(
MsgHeaderSubcode::BadMessageLength,
length.to_be_bytes().to_vec(),
format!(
"BGP message length {length} is not between {BGP_HEADER_LEN} and \
{BGP_MAX_MSG_LEN}, or exceeds the {} bytes received",
buf.len()
),
));
}
let body = &buf[header_len..usize::from(length)];
let bad_length = |what: &str| {
header_error(
MsgHeaderSubcode::BadMessageLength,
length.to_be_bytes().to_vec(),
format!("{what} with a length of {length}"),
)
};
let message = match type_code {
msg_type::OPEN => Self::Open(decode_open(body)?),
msg_type::UPDATE => {
let decoded = UpdateMessage::decode(ctx, body)?;
return Ok(Decoded {
message: Self::Update(decoded.update),
update_errors: decoded.errors,
attributes_wire: decoded.attributes_wire,
});
}
msg_type::NOTIFICATION => match body {
[code, subcode, data @ ..] => Self::Notification(NotificationMessage {
error: BgpError::from_code_subcode(*code, *subcode),
data: data.to_vec(),
}),
_ => return Err(bad_length("NOTIFICATION")),
},
msg_type::KEEPALIVE if body.is_empty() => Self::KeepAlive,
msg_type::KEEPALIVE => return Err(bad_length("KEEPALIVE with a body")),
msg_type::ROUTE_REFRESH => match body {
[afi_hi, afi_lo, subtype, safi] => Self::RouteRefresh(RouteRefreshMessage {
afi: Afi(u16::from_be_bytes([*afi_hi, *afi_lo])),
subtype: *subtype,
safi: Safi(*safi),
}),
_ => return Err(bad_length("ROUTE-REFRESH")),
},
other => {
return Err(header_error(
MsgHeaderSubcode::BadMessageType,
vec![other],
format!("unknown BGP message type {other}"),
));
}
};
Ok(Decoded {
message,
update_errors: Vec::new(),
attributes_wire: None,
})
}
/// Writes the message, header and body, to the end of `out`. If it
/// cannot be encoded, `out` is left as it was.
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<(), EncodeError> {
let start = out.len();
out.put_slice(&BGP_MARKER);
// The length, filled in once the body is written.
out.put_u16(0);
out.put_u8(self.type_code());
let written = self.body_to_wire(ctx, out).and_then(|()| {
let length = out.len() - start;
u16::try_from(length)
.ok()
.filter(|length| *length <= BGP_MAX_MSG_LEN)
.ok_or(EncodeError::TooLong {
what: "BGP message",
len: length,
max: BGP_MAX_MSG_LEN.into(),
})
});
match written {
Ok(length) => {
out[start + 16..start + 18].copy_from_slice(&length.to_be_bytes());
Ok(())
}
Err(e) => {
out.truncate(start);
Err(e)
}
}
}
fn body_to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<(), EncodeError> {
match self {
Self::Open(m) => m.to_wire(out),
Self::Update(m) => m.to_wire(ctx, out),
Self::Notification(m) => {
m.to_wire(out);
Ok(())
}
Self::RouteRefresh(m) => {
out.put_u16(m.afi.0);
out.put_u8(m.subtype);
out.put_u8(m.safi.0);
Ok(())
}
Self::KeepAlive => Ok(()),
}
}
fn type_code(&self) -> u8 {
match self {
Self::Open(_) => msg_type::OPEN,
Self::Update(_) => msg_type::UPDATE,
Self::Notification(_) => msg_type::NOTIFICATION,
Self::KeepAlive => msg_type::KEEPALIVE,
Self::RouteRefresh(_) => msg_type::ROUTE_REFRESH,
}
}
}
/// Decodes the body of an OPEN, with the NOTIFICATION that reports a
/// failure (RFC 4271 §6.2).
fn decode_open(body: &[u8]) -> Result<OpenMessage, DecodeError> {
let refused = |subcode, data: Vec<u8>, detail: String| DecodeError {
notification: BgpError::OpenMsg(subcode),
data,
detail: format!("malformed OPEN: {detail}"),
};
match OpenMessage::from_wire(body) {
Ok(([], open)) => Ok(open),
Ok((rest, _)) => Err(refused(
OpenMsgSubcode::Unspecific,
vec![],
format!("{} octets after the optional parameters", rest.len()),
)),
Err(e) => {
let (subcode, data) = match &e {
nom::Err::Error(BgpParserError::ProtocolError(value))
| nom::Err::Failure(BgpParserError::ProtocolError(value)) => match value {
// The Data field is the largest version we do support.
ProtocolErrorValues::UnsupportedVersion(_) => (
OpenMsgSubcode::UnsupportedVersion,
u16::from(BGP4_VERSION).to_be_bytes().to_vec(),
),
ProtocolErrorValues::UnknownOpenOption(_) => {
(OpenMsgSubcode::UnsupportedOptionalParam, vec![])
}
},
_ => (OpenMsgSubcode::Unspecific, vec![]),
};
Err(refused(subcode, data, describe(&e)))
}
}
}
/// A message, and what else decoding it found out.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Decoded {
pub message: Message,
/// The errors in an UPDATE that were handled without resetting the
/// session (RFC 7606). Empty for a well-formed UPDATE and for every
/// other message.
pub update_errors: Vec<UpdateError>,
/// See [`crate::update_decode::DecodedUpdate::attributes_wire`].
pub attributes_wire: Option<Vec<u8>>,
}
/// A request to send again every route of one address family (RFC 2918).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub struct RouteRefreshMessage {
pub afi: Afi,
/// Zero for a plain request. RFC 7313 uses the octet, reserved in
/// RFC 2918, to mark the beginning and end of a refresh.
pub subtype: u8,
pub safi: Safi,
}
impl RouteRefreshMessage {
/// A plain request for the routes of `family`.
pub fn request(family: AfiSafi) -> Self {
Self {
afi: family.afi,
subtype: 0,
safi: family.safi,
}
}
pub fn family(&self) -> AfiSafi {
AfiSafi::new(self.afi, self.safi)
}
}
/// Represents a BGP Update message.
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct UpdateMessage {
pub withdrawn_routes: Vec<Nlri>,
pub path_attributes: Vec<PathAttribute>,
pub announced_routes: Vec<Nlri>,
}
impl UpdateMessage {
/// Writes the body of the UPDATE (what follows the 19-octet header).
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<(), EncodeError> {
// The prefixes in the body are IPv4 unicast (RFC 4271 §4.3).
let with_path_id = ctx.has_add_path(AfiSafi::IPV4_UNICAST);
let withdrawn = reserve_length(out);
write_nlri(&self.withdrawn_routes, with_path_id, out);
fill_length(out, withdrawn, "withdrawn routes")?;
let attributes = reserve_length(out);
for attribute in &self.path_attributes {
attribute.to_wire(ctx, out)?;
}
fill_length(out, attributes, "path attributes")?;
// The NLRI take up the rest of the message.
write_nlri(&self.announced_routes, with_path_id, out);
Ok(())
}
}
/// Leaves room at the end of `out` for a two-octet length that is known
/// only once what it counts has been written, and returns where.
fn reserve_length(out: &mut BytesMut) -> usize {
let at = out.len();
out.put_u16(0);
at
}
/// Fills in the length reserved at `at`: everything written since.
fn fill_length(out: &mut BytesMut, at: usize, what: &'static str) -> Result<(), EncodeError> {
let length = out.len() - at - 2;
let field = u16::try_from(length).map_err(|_| EncodeError::TooLong {
what,
len: length,
max: u16::MAX.into(),
})?;
out[at..at + 2].copy_from_slice(&field.to_be_bytes());
Ok(())
}
/// 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) {
for n in nlri {
if with_path_id {
out.put_u32(n.path_id);
}
out.put_u8(n.prefix.length());
out.put(n.prefix.bytes());
}
}
/// A parser failure as text for a log line.
pub(crate) fn describe(e: &nom::Err<BgpParserError<&[u8]>>) -> String {
match e {
nom::Err::Incomplete(_) => "truncated".into(),
nom::Err::Error(inner) | nom::Err::Failure(inner) => match inner {
BgpParserError::Malformed(text) => text.clone(),
BgpParserError::ProtocolError(value) => format!("{value:?}"),
BgpParserError::Nom(rest, kind) => {
format!("unexpected input ({kind:?}) with {} bytes left", rest.len())
}
},
}
}
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);
}
}
/// Represents a BGP NOTIFICATION message per RFC 4271 §4.5.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct NotificationMessage {
pub error: BgpError,
/// Optional diagnostic data; may be empty.
pub data: Vec<u8>,
}
impl NotificationMessage {
/// Writes the body of the NOTIFICATION.
pub fn to_wire(&self, out: &mut BytesMut) {
let (code, subcode) = self.error.code_subcode();
out.put_u8(code);
out.put_u8(subcode);
out.put_slice(&self.data);
}
}
#[cfg(test)]
mod tests {
use std::{
net::{Ipv4Addr, Ipv6Addr},
str::FromStr,
};
use bytes::BytesMut;
use eyre::{Result, eyre};
use crate::message::{
AggregatorPathAttribute, AsPathAttribute, AsPathSegment, AtomicAggregatePathAttribute,
CommunitiesPathAttribute, ExtendedCommunitiesPathAttribute, ExtendedCommunity,
LargeCommunitiesPathAttribute, LargeCommunity, LocalPrefPathAttribute, Message,
MpReachNlriPathAttribute, MpUnreachNlriPathAttribute, MultiExitDiscPathAttribute,
NextHopPathAttribute, NlriNextHop, NotificationMessage, OriginPathAttribute, PathAttribute,
PathAttributeFlags, UpdateMessage,
};
use bgp_primitives::{
constants::{AfiSafi, IpVersion},
ip_prefix::{IpPrefix, Nlri},
parser::ParserContext,
};
macro_rules! test_path_attribute_roundtrip {
($name:ident, $input_bytes: expr, $expected: expr) => {
#[test]
fn $name() -> Result<()> {
let ctx = &default_v6_context();
let (buf, parsed) = PathAttribute::from_wire(ctx, $input_bytes)?;
assert_eq!(parsed, $expected);
assert!(buf.is_empty());
let mut out = BytesMut::with_capacity(u16::MAX as usize);
parsed.to_wire(ctx, &mut out)?;
assert_eq!(out.to_vec(), $input_bytes);
Ok(())
}
};
}
macro_rules! ipv6 {
($addr_str: expr) => {
Ipv6Addr::from_str($addr_str)
.map_err(|e| eyre!("Failed to parse IPv6 address: {}", e))?
};
}
/// Creates a default context for evaluating the test cases below.
/// A session with four-octet AS numbers and an external peer.
fn default_v6_context() -> ParserContext {
ParserContext {
four_octet_asn: true,
ibgp: false,
add_path: Vec::new(),
}
}
// Origin Path Attribute.
test_path_attribute_roundtrip!(
test_origin_igp_roundtrip,
&[0x40, 0x01, 0x01, 0x00],
PathAttribute::Origin(OriginPathAttribute::IGP)
);
test_path_attribute_roundtrip!(
test_origin_egp_roundtrip,
&[0x40, 0x01, 0x01, 0x01],
PathAttribute::Origin(OriginPathAttribute::EGP)
);
test_path_attribute_roundtrip!(
test_origin_incomplete_roundtrip,
&[0x40, 0x01, 0x01, 0x02],
PathAttribute::Origin(OriginPathAttribute::INCOMPLETE)
);
// AS Path Attribute.
test_path_attribute_roundtrip!(
test_as_path_segment,
&[
0x40, 0x02, 0x12, 0x02, 0x04, 0x00, 0x00, 0xfd, 0xe8, 0x00, 0x00, 0xfd, 0xe9, 0x00,
0x00, 0xfd, 0xea, 0x00, 0x00, 0xfd, 0xeb
],
PathAttribute::AsPath(AsPathAttribute {
segments: vec![AsPathSegment {
ordered: true,
path: vec![65000, 65001, 65002, 65003],
}]
})
);
test_path_attribute_roundtrip!(
test_as_path_mixed_segments,
&[
0x40, 0x02, 0x1c, 0x02, 0x04, 0x00, 0x00, 0xfd, 0xe8, 0x00, 0x00, 0xfd, 0xe9, 0x00,
0x00, 0xfd, 0xea, 0x00, 0x00, 0xfd, 0xeb, 0x01, 0x02, 0x00, 0x00, 0xfd, 0xea, 0x00,
0x00, 0xfd, 0xeb,
],
PathAttribute::AsPath(AsPathAttribute {
segments: vec![
AsPathSegment {
ordered: true,
path: vec![65000, 65001, 65002, 65003],
},
AsPathSegment {
ordered: false,
path: vec![65002, 65003],
}
]
})
);
// Next Hop Path Attribute.
test_path_attribute_roundtrip!(
test_next_hop,
&[0x40, 0x03, 0x04, 0xc0, 0xa8, 0x01, 0x01],
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 168, 1, 1)))
);
// Multi Exit Discriminator Path Attribute.
test_path_attribute_roundtrip!(
test_multi_exit_discriminator,
&[0x80, 0x04, 0x04, 0xca, 0xfe, 0xba, 0xbe],
PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(0xcafebabe))
);
// Local Pref Path Attribute.
test_path_attribute_roundtrip!(
test_local_pref,
&[0x40, 0x05, 0x04, 0xca, 0xfe, 0xd0, 0x0d],
PathAttribute::LocalPref(LocalPrefPathAttribute(0xcafed00d))
);
// Atomic Aggregate Path Attribute.
test_path_attribute_roundtrip!(
test_atomic_aggregate,
&[0x40, 0x06, 0x00],
PathAttribute::AtomicAggregate(AtomicAggregatePathAttribute {})
);
// Aggregator Path Attribute.
test_path_attribute_roundtrip!(
test_aggregator,
&[
0xc0, 0x07, 0x08, 0x00, 0x00, 0xfd, 0xe8, 0xc0, 0xa8, 0x01, 0x01
],
PathAttribute::Aggregator(AggregatorPathAttribute {
asn: 65000,
ip: Ipv4Addr::new(192, 168, 1, 1)
})
);
// Communities Path Attribute.
test_path_attribute_roundtrip!(
test_communities,
&[0xc0, 0x08, 0x04, 0xca, 0xfe, 0xba, 0xbe],
PathAttribute::Communities(CommunitiesPathAttribute(vec![(0xcafe, 0xbabe)]))
);
// MP Reach NLRI Path Attribute.
test_path_attribute_roundtrip!(
test_mp_reach_nlri,
&[
0x80, 0x0e, 0x2a, 0x00, 0x02, 0x01, 0x20, 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xfe, 0x80, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x20, 0x20,
0x01, 0x0d, 0xb8
],
PathAttribute::MpReachNlri(MpReachNlriPathAttribute {
family: AfiSafi::IPV6_UNICAST,
next_hop: NlriNextHop::Ipv6WithLl {
global: ipv6!("2001:db8::1"),
link_local: ipv6!("fe80::12"),
},
prefixes: vec![
IpPrefix::new(IpVersion::Ipv6, [0x20, 0x01, 0x0d, 0xb8], 32)
.unwrap()
.into()
]
})
);
// MP Unreach NLRI Path Attribute.
test_path_attribute_roundtrip!(
mp_unreach_nlri,
&[
0x80, 0x0f, 0x08, 0x00, 0x02, 0x01, 0x20, 0x20, 0x01, 0x0d, 0xb8
],
PathAttribute::MpUnreachNlri(MpUnreachNlriPathAttribute {
family: AfiSafi::IPV6_UNICAST,
prefixes: vec![
IpPrefix::new(IpVersion::Ipv6, [0x20, 0x01, 0x0d, 0xb8], 32)
.unwrap()
.into()
],
})
);
// Extended Communities Path Attribute.
test_path_attribute_roundtrip!(
test_extended_communities,
&[
0xc0, 0x10, 0x18, 0x00, 0x03, 0xfd, 0xe8, 0x00, 0x00, 0x05, 0x39, 0x00, 0x03, 0xfd,
0xe9, 0x00, 0x00, 0x05, 0x39, 0x02, 0x03, 0xfd, 0xe8, 0x00, 0x00, 0x05, 0x39
],
PathAttribute::ExtendedCommunities(ExtendedCommunitiesPathAttribute {
extended_communities: vec![
ExtendedCommunity([0x00, 0x03, 0xfd, 0xe8, 0x00, 0x00, 0x05, 0x39]),
ExtendedCommunity([0x00, 0x03, 0xfd, 0xe9, 0x00, 0x00, 0x05, 0x39]),
ExtendedCommunity([0x02, 0x03, 0xfd, 0xe8, 0x00, 0x00, 0x05, 0x39]),
]
})
);
// Large Communities Path Attribute.
test_path_attribute_roundtrip!(
test_large_communities,
&[
0xc0, 0x20, 0x0c, 0x00, 0x00, 0xfd, 0xe8, 0x00, 0x00, 0xca, 0xfe, 0x00, 0x00, 0xf0,
0x0d
],
PathAttribute::LargeCommunities(LargeCommunitiesPathAttribute {
communities: vec![LargeCommunity {
global_admin: 65000,
data_1: 0xcafe,
data_2: 0xf00d,
}]
})
);
// Unknown Path Attribute. Ensures we both parse an unrecognized type code
// into the catch-all variant and re-serialize it with the correct field
// ordering (flags, type, length, payload) per RFC 4271 §4.3.
test_path_attribute_roundtrip!(
test_unknown_path_attribute,
&[0x80, 0x63, 0x04, 0xde, 0xad, 0xbe, 0xef],
PathAttribute::UnknownPathAttribute {
flags: PathAttributeFlags::new(
/*optional=*/ true, /*transitive=*/ false, /*partial=*/ false,
/*extended_length=*/ false,
),
type_code: 0x63,
payload: vec![0xde, 0xad, 0xbe, 0xef],
}
);
// -- UPDATE message roundtrip tests --
/// The UPDATE in `body`, as it is to be acted on.
fn decode_update(ctx: &ParserContext, body: &[u8]) -> Result<UpdateMessage> {
Ok(UpdateMessage::decode(ctx, body)?.update)
}
/// Asserts that an UPDATE message roundtrips byte-for-byte.
macro_rules! test_update_roundtrip {
($name:ident, $input_bytes:expr, $expected:expr) => {
#[test]
fn $name() -> Result<()> {
let ctx = &default_v6_context();
let parsed = decode_update(ctx, $input_bytes)?;
assert_eq!(parsed, $expected);
let mut out = BytesMut::with_capacity(u16::MAX as usize);
parsed.to_wire(ctx, &mut out)?;
assert_eq!(out.to_vec(), $input_bytes);
Ok(())
}
};
}
// Wrapper to generate a IPv4 prefix used in tests below.
fn v4_prefix(length: u8, bytes: &[u8]) -> IpPrefix {
IpPrefix::new(IpVersion::Ipv4, bytes, length).unwrap()
}
/// A small set of path attributes that commonly accompany an IPv4 unicast
/// announcement: Origin=IGP, AS_PATH=[65000], NEXT_HOP=192.0.2.1.
fn sample_ipv4_path_attributes() -> Vec<PathAttribute> {
vec![
PathAttribute::Origin(OriginPathAttribute::IGP),
PathAttribute::AsPath(AsPathAttribute {
segments: vec![AsPathSegment {
ordered: true,
path: vec![65000],
}],
}),
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
]
}
// Empty UPDATE (equivalent to a keepalive-shaped body): no withdrawn
// routes, no path attributes, no NLRI.
test_update_roundtrip!(
test_update_empty,
&[0x00, 0x00, 0x00, 0x00],
UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![],
announced_routes: vec![],
}
);
// Pure announcement: no withdrawn routes, standard v4 path attributes, one
// NLRI prefix (10.0.0.0/8).
test_update_roundtrip!(
test_update_announcement,
&[
// Withdrawn routes length = 0.
0x00, 0x00, // Total path attribute length = 20 bytes.
0x00, 0x14, // ORIGIN = IGP.
0x40, 0x01, 0x01, 0x00, // AS_PATH = AS_SEQUENCE [65000] (4-octet ASNs).
0x40, 0x02, 0x06, 0x02, 0x01, 0x00, 0x00, 0xfd, 0xe8,
// NEXT_HOP = 192.0.2.1.
0x40, 0x03, 0x04, 0xc0, 0x00, 0x02, 0x01, // NLRI: 10.0.0.0/8.
0x08, 0x0a,
],
UpdateMessage {
withdrawn_routes: vec![],
path_attributes: sample_ipv4_path_attributes(),
announced_routes: vec![v4_prefix(8, &[0x0a]).into()],
}
);
// Pure withdrawal: one withdrawn route, no path attributes, no NLRI. RFC
// 4271 allows (and indeed requires) this shape for unreach updates.
test_update_roundtrip!(
test_update_withdrawal,
&[
// Withdrawn routes length = 2.
0x00, 0x02, // 10.0.0.0/8.
0x08, 0x0a, // Total path attribute length = 0.
0x00, 0x00,
],
UpdateMessage {
withdrawn_routes: vec![v4_prefix(8, &[0x0a]).into()],
path_attributes: vec![],
announced_routes: vec![],
}
);
// Mixed withdrawal + announcement with multiple NLRI entries at different
// prefix lengths, exercising the variable-length NLRI parser tail.
test_update_roundtrip!(
test_update_mixed,
&[
// Withdrawn routes length = 2.
0x00, 0x02, // 10.0.0.0/8 withdrawn.
0x08, 0x0a, // Total path attribute length = 20.
0x00, 0x14, // ORIGIN = IGP.
0x40, 0x01, 0x01, 0x00, // AS_PATH = AS_SEQUENCE [65000].
0x40, 0x02, 0x06, 0x02, 0x01, 0x00, 0x00, 0xfd, 0xe8,
// NEXT_HOP = 192.0.2.1.
0x40, 0x03, 0x04, 0xc0, 0x00, 0x02, 0x01, // NLRI: 172.16.0.0/16.
0x10, 0xac, 0x10, // NLRI: 192.168.1.0/24.
0x18, 0xc0, 0xa8, 0x01,
],
UpdateMessage {
withdrawn_routes: vec![v4_prefix(8, &[0x0a]).into()],
path_attributes: sample_ipv4_path_attributes(),
announced_routes: vec![
v4_prefix(16, &[0xac, 0x10]).into(),
v4_prefix(24, &[0xc0, 0xa8, 0x01]).into(),
],
}
);
// An UPDATE carrying an unrecognized path attribute.
test_update_roundtrip!(
test_update_with_unknown_path_attribute,
&[
// Withdrawn routes length = 0.
0x00, 0x00, // Total path attribute length = 7.
0x00, 0x07, // Unknown attribute: flags=0x80 (optional), type=0x63, len=4.
0x80, 0x63, 0x04, 0xde, 0xad, 0xbe, 0xef,
],
UpdateMessage {
withdrawn_routes: vec![],
path_attributes: vec![PathAttribute::UnknownPathAttribute {
flags: PathAttributeFlags::new(true, false, false, false),
type_code: 0x63,
payload: vec![0xde, 0xad, 0xbe, 0xef],
}],
announced_routes: vec![],
}
);
// Rejects trailing garbage after the NLRI section (a partial prefix).
/// With ADD-PATH in use for a family, each of its prefixes has a
/// four-octet path identifier in front (RFC 7911 §3): in the body for
/// IPv4 unicast, in MP_REACH_NLRI and MP_UNREACH_NLRI for the others.
#[test]
fn prefixes_carry_a_path_identifier_where_add_path_is_in_use() {
let plain = default_v6_context();
let v4_only = ParserContext {
add_path: vec![AfiSafi::IPV4_UNICAST],
..default_v6_context()
};
let v6_only = ParserContext {
add_path: vec![AfiSafi::IPV6_UNICAST],
..default_v6_context()
};
// Withdrawing 10.0.0.0/8, path 7, in the body.
#[rustfmt::skip]
let body = [
0x00, 0x06, // withdrawn routes length
0x00, 0x00, 0x00, 0x07, 0x08, 0x0a, // path 7, 10.0.0.0/8
0x00, 0x00, // no attributes
];
let update = decode_update(&v4_only, &body).unwrap();
assert_eq!(
update.withdrawn_routes,
vec![Nlri {
prefix: v4_prefix(8, &[0x0a]),
path_id: 7,
}]
);
let mut out = BytesMut::new();
update.to_wire(&v4_only, &mut out).unwrap();
assert_eq!(&out[..], &body[..]);
// The same route on a session without ADD-PATH has no identifier.
let mut out = BytesMut::new();
update.to_wire(&plain, &mut out).unwrap();
assert_eq!(&out[..], [0x00, 0x02, 0x08, 0x0a, 0x00, 0x00]);
// And ADD-PATH for IPv6 says nothing about the IPv4 prefixes in
// the body: read that way, these bytes are not prefixes at all.
assert!(decode_update(&v6_only, &body).is_err());
// Two routes for 2001:db8::/32, paths 1 and 2, withdrawn.
#[rustfmt::skip]
let unreach = [
0x00, 0x02, 0x01,
0x00, 0x00, 0x00, 0x01, 0x20, 0x20, 0x01, 0x0d, 0xb8,
0x00, 0x00, 0x00, 0x02, 0x20, 0x20, 0x01, 0x0d, 0xb8,
];
let (rest, parsed) = MpUnreachNlriPathAttribute::from_wire(&v6_only, &unreach).unwrap();
assert!(rest.is_empty());
assert_eq!(
parsed
.prefixes
.iter()
.map(|n| n.path_id)
.collect::<Vec<_>>(),
[1, 2]
);
assert_eq!(parsed.prefixes[0].prefix, parsed.prefixes[1].prefix);
let mut out = BytesMut::new();
parsed.to_wire(&v6_only, &mut out);
assert_eq!(&out[..], &unreach[..]);
}
// Found by the property tests in tests/properties.rs.
/// A prefix longer than its address family allows was accepted, and
/// showing it (as the session does when it logs a route) panicked. A
/// peer could end the session task with one UPDATE.
#[test]
fn a_prefix_longer_than_an_address_is_malformed() {
let ctx = default_v6_context();
// No withdrawn routes, no attributes, NLRI of length 99.
let mut body = vec![0, 0, 0, 0, 99];
body.extend_from_slice(&[0xff; 13]);
assert!(decode_update(&ctx, &body).is_err());
let mut unreach = vec![0x00, 0x02, 0x01, 129];
unreach.extend_from_slice(&[0xff; 17]);
assert!(MpUnreachNlriPathAttribute::from_wire(&ctx, &unreach).is_err());
}
/// The bits of a prefix past its length mean nothing (RFC 4271 §4.3).
/// They used to be kept, which made 10.0.0.0/8 sent with stray bits a
/// different route from 10.0.0.0/8.
#[test]
fn bits_past_the_prefix_length_are_cleared() {
let ctx = default_v6_context();
// 10.255.0.0/12 withdrawn, with the low four bits of the second
// octet set.
let body = [0, 3, 12, 10, 0xff, 0, 0];
let update = decode_update(&ctx, &body).unwrap();
assert_eq!(update.withdrawn_routes, vec![v4_prefix(12, &[10, 0xf0])]);
assert_eq!(update.withdrawn_routes[0].to_string(), "10.240.0.0/12");
}
/// An extended community is eight octets whatever its type. The opaque
/// kind was read as seven, which put every community after it out of
/// step.
#[test]
fn an_opaque_extended_community_is_eight_octets() {
#[rustfmt::skip]
let wire = [
0x03, 0x0c, 1, 2, 3, 4, 5, 6, // opaque
0x00, 0x02, 0, 0, 0xfd, 0xe8, 0, 100, // route target 65000:100
];
let (rest, parsed) = ExtendedCommunitiesPathAttribute::from_wire(&wire).unwrap();
assert!(rest.is_empty());
assert_eq!(
parsed.extended_communities,
vec![
ExtendedCommunity([0x03, 0x0c, 1, 2, 3, 4, 5, 6]),
ExtendedCommunity([0x00, 0x02, 0, 0, 0xfd, 0xe8, 0, 100]),
]
);
assert_eq!(parsed.extended_communities[0].typ(), 0x03);
assert_eq!(parsed.extended_communities[0].sub_typ(), 0x0c);
let mut out = BytesMut::new();
parsed.to_wire(&mut out);
assert_eq!(&out[..], &wire[..]);
// A list that is not a whole number of them is malformed.
assert!(ExtendedCommunitiesPathAttribute::from_wire(&wire[..12]).is_err());
assert!(ExtendedCommunitiesPathAttribute::from_wire(&[]).is_err());
}
/// AGGREGATOR carries an AS number as wide as the session's. It could
/// neither be read from nor written to a two-octet peer, so one such
/// attribute ended the session.
#[test]
fn aggregator_is_six_octets_on_a_two_octet_session() {
let two_octet = ParserContext {
four_octet_asn: false,
ibgp: false,
add_path: Vec::new(),
};
let wire = [0xfd, 0xe8, 192, 0, 2, 1];
let (rest, parsed) = AggregatorPathAttribute::from_wire(&two_octet, &wire).unwrap();
assert!(rest.is_empty());
assert_eq!(
parsed,
AggregatorPathAttribute {
asn: 65000,
ip: Ipv4Addr::new(192, 0, 2, 1),
}
);
let mut out = BytesMut::new();
parsed.to_wire(&two_octet, &mut out);
assert_eq!(&out[..], &wire[..]);
}
/// The reserved octet after the next hop is there for every family. It
/// used to be skipped only for IPv6, so an IPv4 MP_REACH_NLRI gained a
/// 0.0.0.0/0 route that the peer never sent.
#[test]
fn mp_reach_for_ipv4_has_no_phantom_default_route() {
let ctx = default_v6_context();
#[rustfmt::skip]
let wire = [
0x00, 0x01, 0x01, // IPv4 unicast
0x04, 192, 0, 2, 1, // next hop
0x00, // reserved
0x18, 10, 1, 2, // 10.1.2.0/24
];
let (rest, parsed) = MpReachNlriPathAttribute::from_wire(&ctx, &wire).unwrap();
assert!(rest.is_empty());
assert_eq!(
parsed.family,
bgp_primitives::constants::AfiSafi::IPV4_UNICAST
);
assert_eq!(
parsed.next_hop,
NlriNextHop::Ipv4(Ipv4Addr::new(192, 0, 2, 1))
);
assert_eq!(parsed.prefixes, vec![v4_prefix(24, &[10, 1, 2])]);
let mut out = BytesMut::new();
parsed.to_wire(&ctx, &mut out);
assert_eq!(&out[..], &wire[..]);
}
/// Any family can be named, but only IP prefixes can be read as NLRI.
/// Reading the routes of another family as prefixes would invent routes.
#[test]
fn nlri_of_a_family_that_is_not_ip_prefixes_is_refused() {
let ctx = default_v6_context();
// L2VPN EVPN, and IPv4 with labels: neither is a list of prefixes.
for family in [[0x00, 0x19, 0x46], [0x00, 0x01, 0x04]] {
let mut reach = family.to_vec();
reach.extend_from_slice(&[0x04, 192, 0, 2, 1, 0x00, 0x18, 10, 1, 2]);
assert!(MpReachNlriPathAttribute::from_wire(&ctx, &reach).is_err());
let mut unreach = family.to_vec();
unreach.extend_from_slice(&[0x18, 10, 1, 2]);
assert!(MpUnreachNlriPathAttribute::from_wire(&ctx, &unreach).is_err());
}
}
/// AS_PATH is as wide as the session negotiated; AS4_PATH is always
/// four octets wide (RFC 6793).
#[test]
fn as_paths_are_encoded_as_wide_as_the_session_allows() {
let path = AsPathAttribute {
segments: vec![AsPathSegment {
ordered: true,
path: vec![65000, 4_200_000_001],
}],
};
let two_octet = ParserContext {
four_octet_asn: false,
ibgp: false,
add_path: Vec::new(),
};
let four_octet = ParserContext {
four_octet_asn: true,
ibgp: false,
add_path: Vec::new(),
};
let wire = |attribute: &PathAttribute, ctx: &ParserContext| {
let mut out = BytesMut::new();
attribute.to_wire(ctx, &mut out).unwrap();
out.to_vec()
};
// Two octets: the number that does not fit goes out as AS_TRANS.
let as_path = PathAttribute::AsPath(path.clone());
#[rustfmt::skip]
assert_eq!(
wire(&as_path, &two_octet),
[0x40, 0x02, 0x06, 0x02, 0x02, 0xfd, 0xe8, 0x5b, 0xa0]
);
#[rustfmt::skip]
assert_eq!(
wire(&as_path, &four_octet),
[0x40, 0x02, 0x0a, 0x02, 0x02, 0x00, 0x00, 0xfd, 0xe8, 0xfa, 0x56, 0xea, 0x01]
);
// AS4_PATH: optional transitive, type 17, the same on any session.
let as4_path = path.as4_path().expect("one AS number needs four octets");
#[rustfmt::skip]
let expected = [0xc0, 0x11, 0x0a, 0x02, 0x02, 0x00, 0x00, 0xfd, 0xe8, 0xfa, 0x56, 0xea, 0x01];
for ctx in [&two_octet, &four_octet] {
let bytes = wire(&as4_path, ctx);
assert_eq!(bytes, expected);
let (rest, parsed) = PathAttribute::from_wire(ctx, &bytes).unwrap();
assert!(rest.is_empty());
assert_eq!(parsed, as4_path);
}
// A path of numbers that all fit needs no AS4_PATH.
let narrow = AsPathAttribute {
segments: vec![AsPathSegment {
ordered: true,
path: vec![65000, 65001],
}],
};
assert_eq!(narrow.as4_path(), None);
}
#[test]
fn test_update_rejects_trailing_garbage() {
let ctx = default_v6_context();
// Well-formed withdrawn_len=0, pa_len=0, followed by a single byte that
// declares a prefix length of 8 but provides no prefix bytes.
let buf = &[0x00, 0x00, 0x00, 0x00, 0x08];
let result = decode_update(&ctx, buf);
assert!(
result.is_err(),
"Expected a parse error for trailing garbage, got {:?}",
result
);
}
#[test]
fn a_notification_round_trips_with_its_data() -> Result<()> {
use crate::errors::{BgpError, CeaseSubcode, FsmSubcode};
let ctx = &default_v6_context();
for (error, data, body) in [
(
BgpError::Cease(CeaseSubcode::AdminShutdown),
vec![0xde, 0xad],
&[6, 2, 0xde, 0xad][..],
),
(
BgpError::FsmError(FsmSubcode::UnexpectedEstablished),
vec![],
&[5, 3][..],
),
] {
let msg = Message::Notification(NotificationMessage { error, data });
let mut buf = BytesMut::new();
msg.to_wire(ctx, &mut buf)?;
assert_eq!(&buf[19..], body);
assert_eq!(Message::decode(ctx, &buf)?, msg);
}
Ok(())
}
/// A peer may give a reason we have no name for: a subcode of zero
/// ("unspecific"), or one assigned since. Such a NOTIFICATION used to
/// be refused as malformed, which lost the reason and answered a
/// message that must not be answered.
#[test]
fn a_notification_with_an_unnamed_reason_is_still_read() {
use crate::errors::BgpError;
let ctx = default_v6_context();
for (code, subcode) in [(6, 0), (5, 9), (2, 5), (3, 7), (4, 9), (99, 1)] {
let decoded = Message::decode(&ctx, &framed(21, 3, &[code, subcode])).unwrap();
let Message::Notification(notification) = &decoded else {
panic!("expected a NOTIFICATION, got {decoded:?}");
};
assert_eq!(notification.error, BgpError::Other { code, subcode });
assert_eq!(notification.error.code_subcode(), (code, subcode));
}
// A state machine error without a subcode has a name (RFC 6608);
// FRR sends it.
let decoded = Message::decode(&ctx, &framed(21, 3, &[5, 0])).unwrap();
assert!(matches!(
&decoded,
Message::Notification(n)
if n.error == BgpError::FsmError(crate::errors::FsmSubcode::Unspecified)
));
// Too short to hold a code and subcode.
assert!(Message::decode(&ctx, &framed(20, 3, &[6])).is_err());
}
#[test]
fn test_message_keepalive_roundtrip() -> Result<()> {
let ctx = &default_v6_context();
let mut buf = BytesMut::new();
Message::KeepAlive.to_wire(ctx, &mut buf)?;
// 16-byte marker + 2-byte length (19) + 1-byte type (4)
assert_eq!(buf.len(), 19);
assert!(buf[..16].iter().all(|&b| b == 0xFF));
assert_eq!(&buf[16..19], &[0x00, 0x13, 0x04]);
assert_eq!(Message::decode(ctx, &buf)?, Message::KeepAlive);
Ok(())
}
#[test]
fn test_message_rejects_bad_marker() {
let ctx = default_v6_context();
let mut buf = vec![0u8; 19];
buf[16] = 0x00;
buf[17] = 0x13;
buf[18] = 0x04;
assert!(Message::decode(&ctx, &buf).is_err());
}
#[test]
fn test_message_rejects_short_length() {
let ctx = default_v6_context();
let mut buf = vec![0xFF; 19];
buf[16] = 0x00;
buf[17] = 0x12; // length = 18, below minimum 19
buf[18] = 0x04;
assert!(Message::decode(&ctx, &buf).is_err());
}
/// A message that cannot be encoded leaves nothing behind in the
/// buffer, so that what is already there can still be sent.
#[test]
fn a_message_that_cannot_be_encoded_leaves_the_buffer_as_it_was() {
let ctx = default_v6_context();
let mut buf = BytesMut::new();
Message::KeepAlive.to_wire(&ctx, &mut buf).unwrap();
let too_long = Message::Notification(NotificationMessage {
error: crate::errors::BgpError::HoldTimer,
data: vec![0; 5000],
});
assert!(too_long.to_wire(&ctx, &mut buf).is_err());
assert_eq!(buf.len(), 19);
}
/// An attribute's length is one octet unless its value needs two.
#[test]
fn a_long_attribute_gets_the_extended_length() {
let ctx = default_v6_context();
for (communities, extended) in [(63, false), (64, true)] {
let attribute =
PathAttribute::Communities(CommunitiesPathAttribute(vec![(1, 2); communities]));
let mut out = BytesMut::new();
attribute.to_wire(&ctx, &mut out).unwrap();
let header = if extended { 4 } else { 3 };
assert_eq!(out.len(), header + 4 * communities);
assert_eq!(out[0] & 0x10 != 0, extended);
let (rest, parsed) = PathAttribute::from_wire(&ctx, &out).unwrap();
assert!(rest.is_empty());
assert_eq!(parsed, attribute);
}
}
/// A header, with `length` and `msg_type` as given, followed by `body`.
fn framed(length: u16, msg_type: u8, body: &[u8]) -> Vec<u8> {
let mut buf = vec![0xFF; 16];
buf.extend_from_slice(&length.to_be_bytes());
buf.push(msg_type);
buf.extend_from_slice(body);
buf
}
/// RFC 4271 §6.1 to §6.3: what is wrong decides the NOTIFICATION, and
/// for a bad length or type the Data field carries the offending value.
#[test]
fn decode_failures_name_the_notification_to_send() {
use crate::errors::{
BgpError, MsgHeaderSubcode as Header, OpenMsgSubcode, UpdateMsgSubcode,
};
let ctx = default_v6_context();
let failure = |buf: Vec<u8>| {
let e = Message::decode(&ctx, &buf).unwrap_err();
(e.notification, e.data)
};
let mut bad_marker = framed(19, 4, &[]);
bad_marker[3] = 0;
assert_eq!(
failure(bad_marker),
(BgpError::MsgHeader(Header::ConnNotSynchronized), vec![])
);
// Shorter than a header, longer than the maximum, longer than the
// bytes at hand: all a bad length, reported with that length.
for length in [18u16, 4097, 100] {
assert_eq!(
failure(framed(length, 4, &[])),
(
BgpError::MsgHeader(Header::BadMessageLength),
length.to_be_bytes().to_vec()
),
"length {length}"
);
}
assert_eq!(
failure(framed(20, 4, &[0])),
(BgpError::MsgHeader(Header::BadMessageLength), vec![0, 20]),
"a KEEPALIVE has no body"
);
assert_eq!(
failure(framed(19, 9, &[])),
(BgpError::MsgHeader(Header::BadMessageType), vec![9])
);
// An OPEN for BGP version 3: the reply says we speak version 4.
assert_eq!(
failure(framed(29, 1, &[3, 0xfd, 0xe8, 0, 90, 10, 0, 0, 1, 0])),
(
BgpError::OpenMsg(OpenMsgSubcode::UnsupportedVersion),
vec![0, 4]
)
);
// The same OPEN for version 4 is fine: no optional parameters is
// a valid OPEN.
let plain = framed(29, 1, &[4, 0xfd, 0xe8, 0, 90, 10, 0, 0, 1, 0]);
assert!(
matches!(Message::decode(&ctx, &plain), Ok(Message::Open(open)) if open.options.is_empty())
);
// An OPEN cut short.
assert_eq!(
failure(framed(21, 1, &[4, 0xfd])).0,
BgpError::OpenMsg(OpenMsgSubcode::Unspecific)
);
// An UPDATE whose attribute length points past its end.
assert_eq!(
failure(framed(23, 2, &[0, 0, 0, 40])).0,
BgpError::UpdateMsg(UpdateMsgSubcode::MalformedAttrs)
);
}
#[test]
fn decode_ignores_bytes_after_the_message() {
let ctx = default_v6_context();
let mut buf = framed(19, 4, &[]);
buf.extend_from_slice(&[1, 2, 3]);
assert_eq!(Message::decode(&ctx, &buf), Ok(Message::KeepAlive));
}
/// The optional parameters are the last thing in an OPEN.
#[test]
fn bytes_after_the_optional_parameters_of_an_open_are_refused() {
use crate::errors::{BgpError, OpenMsgSubcode};
let ctx = default_v6_context();
let open = framed(31, 1, &[4, 0xfd, 0xe8, 0, 90, 10, 0, 0, 1, 0, 0xaa, 0xbb]);
assert_eq!(
Message::decode(&ctx, &open).unwrap_err().notification,
BgpError::OpenMsg(OpenMsgSubcode::Unspecific)
);
}
}