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>
2190 lines
78 KiB
Rust
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)
|
|
);
|
|
}
|
|
}
|