- Implement passive-side BGP FSM (OpenSent → OpenConfirm → Established) with hold timer, keepalive timer, and NOTIFICATION on expiry - Add bgp-server crate: config validation, TCP listener accept loop (BgpServerBuilder → BgpServer), connection dispatch - Add bgp-rib crate: Adj-RIB-In actor owning route state with event broadcast for streaming consumers and snapshot for introspection - Wire Adj-RIB-In into FSM: announce/withdraw on UPDATE for both legacy IPv4 NLRI fields and MP_REACH/MP_UNREACH path attributes - Add bgp-integration-tests crate with 15 end-to-end tests covering session establishment, timers, UPDATE handling, and RIB introspection - Fix require_empty! macro bug in open.rs (checked wrong buffer) - Fix FourByteAsn capability parser returning consumed buffer as remainder - Fix tracing subscriber not being installed in TracingArgs::install - Expand docs/RFCs.md with full implementation status across 15 RFCs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1989 lines
68 KiB
Rust
1989 lines
68 KiB
Rust
use std::net::{Ipv4Addr, Ipv6Addr};
|
|
use std::ops::Deref;
|
|
|
|
use bitfield::bitfield;
|
|
use bytes::BufMut;
|
|
use bytes::BytesMut;
|
|
use eyre::Context;
|
|
use eyre::Result;
|
|
use eyre::bail;
|
|
use eyre::eyre;
|
|
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::{Deserialize, Serialize};
|
|
use serde_repr::{Deserialize_repr, Serialize_repr};
|
|
use strum::EnumDiscriminants;
|
|
|
|
use bgp_primitives::constants::AddressFamilyId;
|
|
use bgp_primitives::constants::SubsequentAfi;
|
|
use bgp_primitives::ip_prefix::IpPrefix;
|
|
use bgp_primitives::parser::BgpParserError;
|
|
use bgp_primitives::parser::ParserContext;
|
|
|
|
use crate::errors::BgpError;
|
|
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];
|
|
|
|
/// Message represents the top-level messages in the BGP protocol.
|
|
#[derive(Debug, Serialize, Deserialize, EnumDiscriminants)]
|
|
#[repr(u8)]
|
|
pub enum Message {
|
|
Open(OpenMessage) = 1,
|
|
Update(UpdateMessage) = 2,
|
|
Notification(NotificationMessage) = 3,
|
|
KeepAlive = 4,
|
|
/// BGP Route Refresh message (RFC 2918).
|
|
RouteRefresh = 5,
|
|
}
|
|
|
|
impl Message {
|
|
/// Parses a complete BGP message including the 19-byte header from `buf`.
|
|
/// Returns the unparsed remainder and the decoded `Message`.
|
|
pub fn from_wire<'a>(
|
|
ctx: &ParserContext,
|
|
buf: &'a [u8],
|
|
) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> {
|
|
let (buf, marker) = nom::bytes::take(16usize).parse(buf)?;
|
|
if marker != BGP_MARKER {
|
|
return Err(Failure(BgpParserError::Eyre(eyre!(
|
|
"Invalid BGP marker: expected 16 x 0xFF"
|
|
))));
|
|
}
|
|
|
|
let (buf, length) = be_u16(buf)?;
|
|
if length < BGP_HEADER_LEN {
|
|
return Err(Failure(BgpParserError::Eyre(eyre!(
|
|
"BGP message length {length} is below minimum {BGP_HEADER_LEN}"
|
|
))));
|
|
}
|
|
if length > BGP_MAX_MSG_LEN {
|
|
return Err(Failure(BgpParserError::Eyre(eyre!(
|
|
"BGP message length {length} exceeds maximum {BGP_MAX_MSG_LEN}"
|
|
))));
|
|
}
|
|
|
|
let (buf, msg_type) = be_u8(buf)?;
|
|
let body_len = length - BGP_HEADER_LEN;
|
|
let (buf, body) = nom::bytes::take(body_len as usize).parse(buf)?;
|
|
|
|
let msg = match msg_type {
|
|
1 => {
|
|
let (_, open) = OpenMessage::from_wire(ctx, body)?;
|
|
Self::Open(open)
|
|
}
|
|
2 => {
|
|
let (_, update) = UpdateMessage::from_wire(ctx, body)?;
|
|
Self::Update(update)
|
|
}
|
|
3 => {
|
|
let (_, notif) = NotificationMessage::from_wire(ctx, body)?;
|
|
Self::Notification(notif)
|
|
}
|
|
4 => {
|
|
if !body.is_empty() {
|
|
return Err(Failure(BgpParserError::Eyre(eyre!(
|
|
"KEEPALIVE body must be empty, got {} bytes",
|
|
body.len()
|
|
))));
|
|
}
|
|
Self::KeepAlive
|
|
}
|
|
5 => Self::RouteRefresh,
|
|
t => {
|
|
return Err(Failure(BgpParserError::Eyre(eyre!(
|
|
"Unknown BGP message type {t}"
|
|
))));
|
|
}
|
|
};
|
|
|
|
Ok((buf, msg))
|
|
}
|
|
|
|
/// Serializes the message (header + body) into `out`.
|
|
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<()> {
|
|
// Serialize the body first so we can compute the total length.
|
|
let mut body = BytesMut::new();
|
|
match self {
|
|
Self::Open(m) => m.to_wire(ctx, &mut body)?,
|
|
Self::Update(m) => m.to_wire(ctx, &mut body)?,
|
|
Self::Notification(m) => m.to_wire(ctx, &mut body)?,
|
|
Self::KeepAlive | Self::RouteRefresh => {}
|
|
}
|
|
|
|
let total_len = BGP_HEADER_LEN
|
|
.checked_add(body.len() as u16)
|
|
.filter(|&l| l <= BGP_MAX_MSG_LEN)
|
|
.ok_or_else(|| eyre!("BGP message body too large to serialize"))?;
|
|
|
|
out.put_slice(&BGP_MARKER);
|
|
out.put_u16(total_len);
|
|
out.put_u8(self.type_code());
|
|
out.put(body);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn wire_len(&self, ctx: &ParserContext) -> Result<u16> {
|
|
let body_len: u16 = match self {
|
|
Self::Open(m) => m.wire_len(ctx)?,
|
|
Self::Update(m) => m.wire_len(ctx)?,
|
|
Self::Notification(m) => m.wire_len(),
|
|
Self::KeepAlive | Self::RouteRefresh => 0,
|
|
};
|
|
Ok(BGP_HEADER_LEN + body_len)
|
|
}
|
|
|
|
fn type_code(&self) -> u8 {
|
|
match self {
|
|
Self::Open(_) => 1,
|
|
Self::Update(_) => 2,
|
|
Self::Notification(_) => 3,
|
|
Self::KeepAlive => 4,
|
|
Self::RouteRefresh => 5,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents a BGP Update message.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct UpdateMessage {
|
|
pub withdrawn_routes: Vec<IpPrefix>,
|
|
pub path_attributes: Vec<PathAttribute>,
|
|
pub announced_routes: Vec<IpPrefix>,
|
|
}
|
|
|
|
impl UpdateMessage {
|
|
/// Parses a BGP UPDATE message body per RFC 4271 §4.3. The buffer passed in
|
|
/// must contain exactly the UPDATE payload (i.e. without the 19 byte BGP
|
|
/// message header). The NLRI section has no explicit length and is inferred
|
|
/// as the remainder of the buffer, so passing extra trailing bytes will
|
|
/// cause them to be (mis-)parsed as NLRI.
|
|
pub fn from_wire<'a>(
|
|
ctx: &ParserContext,
|
|
buf: &'a [u8],
|
|
) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> {
|
|
// Withdrawn routes. Per RFC 4271 these are always IPv4 prefixes; other
|
|
// address families are carried in the MP_UNREACH_NLRI path attribute.
|
|
let (buf, withdrawn_len) = be_u16(buf)?;
|
|
let (buf, withdrawn_buf) = nom::bytes::take(withdrawn_len as usize).parse(buf)?;
|
|
let (withdrawn_rem, withdrawn_routes) =
|
|
nom::multi::many0(|buf| parse_prefix(AddressFamilyId::Ipv4, buf))
|
|
.parse(withdrawn_buf)?;
|
|
if !withdrawn_rem.is_empty() {
|
|
return Err(Failure(BgpParserError::Eyre(eyre!(
|
|
"Leftover bytes {:x?} after parsing withdrawn routes",
|
|
withdrawn_rem
|
|
))));
|
|
}
|
|
|
|
// Path attributes.
|
|
let (buf, total_pa_len) = be_u16(buf)?;
|
|
let (buf, pa_buf) = nom::bytes::take(total_pa_len as usize).parse(buf)?;
|
|
let (pa_rem, path_attributes) =
|
|
nom::multi::many0(|buf| PathAttribute::from_wire(ctx, buf)).parse(pa_buf)?;
|
|
if !pa_rem.is_empty() {
|
|
return Err(Failure(BgpParserError::Eyre(eyre!(
|
|
"Leftover bytes {:x?} after parsing path attributes",
|
|
pa_rem
|
|
))));
|
|
}
|
|
|
|
// NLRI. As with withdrawn routes, these are IPv4 prefixes per RFC 4271.
|
|
// They occupy the remainder of the UPDATE body; if the caller has
|
|
// delimited `buf` correctly then `many0` will consume it entirely.
|
|
let (buf, announced_routes) =
|
|
nom::multi::many0(|buf| parse_prefix(AddressFamilyId::Ipv4, buf)).parse(buf)?;
|
|
if !buf.is_empty() {
|
|
return Err(Failure(BgpParserError::Eyre(eyre!(
|
|
"Leftover bytes {:x?} after parsing NLRI",
|
|
buf
|
|
))));
|
|
}
|
|
|
|
Ok((
|
|
buf,
|
|
Self {
|
|
withdrawn_routes,
|
|
path_attributes,
|
|
announced_routes,
|
|
},
|
|
))
|
|
}
|
|
|
|
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<()> {
|
|
// Withdrawn routes.
|
|
let withdrawn_len = prefix_list_wire_len(&self.withdrawn_routes)
|
|
.wrap_err("Withdrawn routes length exceeds u16::MAX")?;
|
|
out.put_u16(withdrawn_len);
|
|
for prefix in &self.withdrawn_routes {
|
|
out.put_u8(prefix.length);
|
|
out.put(prefix.prefix.as_slice());
|
|
}
|
|
|
|
// Path attributes.
|
|
let pa_total: u32 = self
|
|
.path_attributes
|
|
.iter()
|
|
.map(|p| p.wire_len(ctx).map(|v| v as u32))
|
|
.sum::<Result<u32>>()?;
|
|
let pa_total = u16::try_from(pa_total)
|
|
.map_err(|_| eyre!("Total path attribute length exceeds u16::MAX"))?;
|
|
out.put_u16(pa_total);
|
|
for pa in &self.path_attributes {
|
|
pa.to_wire(ctx, out)?;
|
|
}
|
|
|
|
// NLRI (consumes the remainder of the message).
|
|
for prefix in &self.announced_routes {
|
|
out.put_u8(prefix.length);
|
|
out.put(prefix.prefix.as_slice());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn wire_len(&self, ctx: &ParserContext) -> Result<u16> {
|
|
// 2 bytes for the withdrawn length field + 2 bytes for the total path
|
|
// attribute length field.
|
|
let mut total: u32 = 4;
|
|
for prefix in &self.withdrawn_routes {
|
|
total += 1 + prefix.prefix.len() as u32;
|
|
}
|
|
for pa in &self.path_attributes {
|
|
total += pa.wire_len(ctx)? as u32;
|
|
}
|
|
for prefix in &self.announced_routes {
|
|
total += 1 + prefix.prefix.len() as u32;
|
|
}
|
|
u16::try_from(total).map_err(|_| eyre!("UPDATE message length exceeds u16::MAX"))
|
|
}
|
|
}
|
|
|
|
/// Computes the total wire length of a list of IPv4/IPv6 NLRI prefixes as they
|
|
/// appear in the withdrawn or NLRI sections of an UPDATE message (each prefix
|
|
/// is encoded as a 1-byte length followed by `ceil(length/8)` prefix bytes).
|
|
fn prefix_list_wire_len(prefixes: &[IpPrefix]) -> Result<u16> {
|
|
let total: u32 = prefixes.iter().map(|p| 1 + p.prefix.len() as u32).sum();
|
|
u16::try_from(total).map_err(|_| eyre!("Prefix list length exceeds u16::MAX"))
|
|
}
|
|
|
|
bitfield! {
|
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct PathAttributeFlags(u8);
|
|
impl new;
|
|
u8;
|
|
optional, set_optional: 7;
|
|
transitive, set_transitive: 6;
|
|
partial, set_partial: 5;
|
|
extended_length, set_extended_length: 4;
|
|
}
|
|
|
|
impl Deref for PathAttributeFlags {
|
|
type Target = u8;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.0
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, EnumDiscriminants)]
|
|
#[repr(u8)]
|
|
pub enum PathAttribute {
|
|
Origin(OriginPathAttribute) = 1,
|
|
AsPath(AsPathAttribute) = 2,
|
|
NextHop(NextHopPathAttribute) = 3,
|
|
MultiExitDisc(MultiExitDiscPathAttribute) = 4,
|
|
LocalPref(LocalPrefPathAttribute) = 5,
|
|
AtomicAggregate(AtomicAggregatePathAttribute) = 6,
|
|
Aggregator(AggregatorPathAttribute) = 7,
|
|
Communitites(CommunitiesPathAttribute) = 8,
|
|
MpReachNlri(MpReachNlriPathAttribute) = 14,
|
|
MpUnreachNlri(MpUnreachNlriPathAttribute) = 15,
|
|
ExtendedCommunities(ExtendedCommunitiesPathAttribute) = 16,
|
|
LargeCommunities(LargeCommunitiesPathAttribute) = 32,
|
|
UnknownPathAttribute {
|
|
flags: PathAttributeFlags,
|
|
type_code: u8,
|
|
payload: Vec<u8>,
|
|
},
|
|
}
|
|
|
|
impl From<u8> for PathAttributeDiscriminants {
|
|
fn from(value: u8) -> Self {
|
|
match value {
|
|
v if v == Self::Origin as u8 => Self::Origin,
|
|
v if v == Self::AsPath as u8 => Self::AsPath,
|
|
v if v == Self::NextHop as u8 => Self::NextHop,
|
|
v if v == Self::MultiExitDisc as u8 => Self::MultiExitDisc,
|
|
v if v == Self::LocalPref as u8 => Self::LocalPref,
|
|
v if v == Self::AtomicAggregate as u8 => Self::AtomicAggregate,
|
|
v if v == Self::Aggregator as u8 => Self::Aggregator,
|
|
v if v == Self::Communitites as u8 => Self::Communitites,
|
|
v if v == Self::MpReachNlri as u8 => Self::MpReachNlri,
|
|
v if v == Self::MpUnreachNlri as u8 => Self::MpUnreachNlri,
|
|
v if v == Self::ExtendedCommunities as u8 => Self::ExtendedCommunities,
|
|
v if v == Self::LargeCommunities as u8 => Self::LargeCommunities,
|
|
_ => Self::UnknownPathAttribute,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PathAttribute {
|
|
fn discriminant(&self) -> u8 {
|
|
match self {
|
|
PathAttribute::Origin(_) => PathAttributeDiscriminants::Origin as u8,
|
|
PathAttribute::AsPath(_) => PathAttributeDiscriminants::AsPath as u8,
|
|
PathAttribute::NextHop(_) => PathAttributeDiscriminants::NextHop as u8,
|
|
PathAttribute::MultiExitDisc(_) => PathAttributeDiscriminants::MultiExitDisc as u8,
|
|
PathAttribute::LocalPref(_) => PathAttributeDiscriminants::LocalPref as u8,
|
|
PathAttribute::AtomicAggregate(_) => PathAttributeDiscriminants::AtomicAggregate as u8,
|
|
PathAttribute::Aggregator(_) => PathAttributeDiscriminants::Aggregator as u8,
|
|
PathAttribute::Communitites(_) => PathAttributeDiscriminants::Communitites as u8,
|
|
PathAttribute::MpReachNlri(_) => PathAttributeDiscriminants::MpReachNlri as u8,
|
|
PathAttribute::MpUnreachNlri(_) => PathAttributeDiscriminants::MpUnreachNlri as u8,
|
|
PathAttribute::ExtendedCommunities(_) => {
|
|
PathAttributeDiscriminants::ExtendedCommunities as u8
|
|
}
|
|
PathAttribute::LargeCommunities(_) => {
|
|
PathAttributeDiscriminants::LargeCommunities as u8
|
|
}
|
|
PathAttribute::UnknownPathAttribute { type_code, .. } => *type_code,
|
|
}
|
|
}
|
|
|
|
fn optional(&self) -> bool {
|
|
match self {
|
|
PathAttribute::Origin(_) => false,
|
|
PathAttribute::AsPath(_) => false,
|
|
PathAttribute::NextHop(_) => false,
|
|
PathAttribute::MultiExitDisc(_) => true,
|
|
PathAttribute::LocalPref(_) => false,
|
|
PathAttribute::AtomicAggregate(_) => true,
|
|
PathAttribute::Aggregator(_) => true,
|
|
PathAttribute::Communitites(_) => true,
|
|
PathAttribute::MpReachNlri(_) => true,
|
|
PathAttribute::MpUnreachNlri(_) => true,
|
|
PathAttribute::ExtendedCommunities(_) => true,
|
|
PathAttribute::LargeCommunities(_) => true,
|
|
PathAttribute::UnknownPathAttribute { flags, .. } => flags.optional(),
|
|
}
|
|
}
|
|
|
|
fn transitive(&self) -> bool {
|
|
match self {
|
|
PathAttribute::Origin(_) => true,
|
|
PathAttribute::AsPath(_) => true,
|
|
PathAttribute::NextHop(_) => true,
|
|
PathAttribute::MultiExitDisc(_) => false,
|
|
PathAttribute::LocalPref(_) => true,
|
|
PathAttribute::AtomicAggregate(_) => true,
|
|
PathAttribute::Aggregator(_) => false,
|
|
PathAttribute::Communitites(_) => true,
|
|
PathAttribute::MpReachNlri(_) => false,
|
|
PathAttribute::MpUnreachNlri(_) => false,
|
|
PathAttribute::ExtendedCommunities(_) => true,
|
|
PathAttribute::LargeCommunities(_) => true,
|
|
PathAttribute::UnknownPathAttribute { flags, .. } => flags.transitive(),
|
|
}
|
|
}
|
|
|
|
/// The from_wire parser for `PathAttribute` consumes type and length which it uses to
|
|
/// determine how many bytes to take and pass down to the corresponding sub-parser.
|
|
pub fn from_wire<'a>(
|
|
ctx: &ParserContext,
|
|
buf: &'a [u8],
|
|
) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> {
|
|
let (buf, attr_flags) = be_u8(buf).map(|(buf, b)| (buf, PathAttributeFlags(b)))?;
|
|
let (buf, type_code) = be_u8(buf)?;
|
|
|
|
let (buf, length): (_, u16) = if attr_flags.extended_length() {
|
|
be_u16(buf)?
|
|
} else {
|
|
be_u8(buf).map(|(buf, b)| (buf, b as u16))?
|
|
};
|
|
|
|
let discriminant = PathAttributeDiscriminants::from(type_code);
|
|
if matches!(
|
|
discriminant,
|
|
PathAttributeDiscriminants::UnknownPathAttribute
|
|
) {
|
|
let (buf, pa_buf) = nom::bytes::take(length).parse(buf)?;
|
|
return Ok((
|
|
buf,
|
|
PathAttribute::UnknownPathAttribute {
|
|
flags: attr_flags,
|
|
type_code,
|
|
payload: pa_buf.to_vec(),
|
|
},
|
|
));
|
|
}
|
|
Self::parse_known_path_attribute(ctx, buf, discriminant, length)
|
|
}
|
|
|
|
fn parse_known_path_attribute<'a>(
|
|
ctx: &ParserContext,
|
|
buf: &'a [u8],
|
|
discriminant: PathAttributeDiscriminants,
|
|
length: u16,
|
|
) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> {
|
|
let (buf, pa_buf) = nom::bytes::take(length).parse(buf)?;
|
|
|
|
let attr: PathAttribute = match discriminant {
|
|
PathAttributeDiscriminants::Origin => {
|
|
PathAttribute::Origin(OriginPathAttribute::from_wire(ctx, pa_buf)?.1)
|
|
}
|
|
PathAttributeDiscriminants::AsPath => {
|
|
PathAttribute::AsPath(AsPathAttribute::from_wire(ctx, pa_buf)?.1)
|
|
}
|
|
PathAttributeDiscriminants::NextHop => {
|
|
PathAttribute::NextHop(NextHopPathAttribute::from_wire(ctx, pa_buf)?.1)
|
|
}
|
|
PathAttributeDiscriminants::MultiExitDisc => {
|
|
PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute::from_wire(ctx, pa_buf)?.1)
|
|
}
|
|
PathAttributeDiscriminants::LocalPref => {
|
|
PathAttribute::LocalPref(LocalPrefPathAttribute::from_wire(ctx, pa_buf)?.1)
|
|
}
|
|
PathAttributeDiscriminants::AtomicAggregate => PathAttribute::AtomicAggregate(
|
|
AtomicAggregatePathAttribute::from_wire(ctx, pa_buf)?.1,
|
|
),
|
|
PathAttributeDiscriminants::Aggregator => {
|
|
PathAttribute::Aggregator(AggregatorPathAttribute::from_wire(ctx, pa_buf)?.1)
|
|
}
|
|
PathAttributeDiscriminants::Communitites => {
|
|
PathAttribute::Communitites(CommunitiesPathAttribute::from_wire(ctx, pa_buf)?.1)
|
|
}
|
|
PathAttributeDiscriminants::MpReachNlri => {
|
|
PathAttribute::MpReachNlri(MpReachNlriPathAttribute::from_wire(ctx, pa_buf)?.1)
|
|
}
|
|
PathAttributeDiscriminants::MpUnreachNlri => {
|
|
PathAttribute::MpUnreachNlri(MpUnreachNlriPathAttribute::from_wire(ctx, pa_buf)?.1)
|
|
}
|
|
PathAttributeDiscriminants::ExtendedCommunities => PathAttribute::ExtendedCommunities(
|
|
ExtendedCommunitiesPathAttribute::from_wire(ctx, pa_buf)?.1,
|
|
),
|
|
PathAttributeDiscriminants::LargeCommunities => PathAttribute::LargeCommunities(
|
|
LargeCommunitiesPathAttribute::from_wire(ctx, pa_buf)?.1,
|
|
),
|
|
PathAttributeDiscriminants::UnknownPathAttribute => unreachable!(
|
|
"parse_known_path_attribute must never be called with an unknown attribute"
|
|
),
|
|
};
|
|
Ok((buf, attr))
|
|
}
|
|
|
|
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<()> {
|
|
macro_rules! write_path_attribute {
|
|
($out: expr, $attribute: expr) => {
|
|
let wire_len = $attribute.wire_len(ctx)?;
|
|
let attr_flags = PathAttributeFlags::new(
|
|
self.optional(),
|
|
self.transitive(),
|
|
/*partial=*/ false,
|
|
wire_len > 255,
|
|
);
|
|
out.put_u8(*attr_flags);
|
|
out.put_u8(self.discriminant() as u8);
|
|
write_wire_len!(out, wire_len);
|
|
$attribute.to_wire(ctx, out)?;
|
|
};
|
|
}
|
|
|
|
macro_rules! write_wire_len {
|
|
($out: expr, $wire_len: expr) => {
|
|
if $wire_len > 255 {
|
|
$out.put_u16($wire_len as u16);
|
|
} else {
|
|
$out.put_u8($wire_len as u8);
|
|
}
|
|
};
|
|
}
|
|
|
|
match self {
|
|
PathAttribute::Origin(origin_path_attribute) => {
|
|
write_path_attribute!(out, origin_path_attribute);
|
|
}
|
|
PathAttribute::AsPath(as_path_attribute) => {
|
|
write_path_attribute!(out, as_path_attribute);
|
|
}
|
|
PathAttribute::NextHop(next_hop_path_attribute) => {
|
|
write_path_attribute!(out, next_hop_path_attribute);
|
|
}
|
|
PathAttribute::MultiExitDisc(multi_exit_disc_path_attribute) => {
|
|
write_path_attribute!(out, multi_exit_disc_path_attribute);
|
|
}
|
|
PathAttribute::LocalPref(local_pref_path_attribute) => {
|
|
write_path_attribute!(out, local_pref_path_attribute);
|
|
}
|
|
PathAttribute::AtomicAggregate(atomic_aggregate_path_attribute) => {
|
|
write_path_attribute!(out, atomic_aggregate_path_attribute);
|
|
}
|
|
PathAttribute::Aggregator(aggregator_path_attribute) => {
|
|
write_path_attribute!(out, aggregator_path_attribute);
|
|
}
|
|
PathAttribute::Communitites(communities_path_attribute) => {
|
|
write_path_attribute!(out, communities_path_attribute);
|
|
}
|
|
PathAttribute::MpReachNlri(mp_reach_nlri_path_attribute) => {
|
|
write_path_attribute!(out, mp_reach_nlri_path_attribute);
|
|
}
|
|
PathAttribute::MpUnreachNlri(mp_unreach_nlri_path_attribute) => {
|
|
write_path_attribute!(out, mp_unreach_nlri_path_attribute);
|
|
}
|
|
PathAttribute::ExtendedCommunities(extended_communities_path_attribute) => {
|
|
write_path_attribute!(out, extended_communities_path_attribute);
|
|
}
|
|
PathAttribute::LargeCommunities(large_communities_path_attribute) => {
|
|
write_path_attribute!(out, large_communities_path_attribute);
|
|
}
|
|
PathAttribute::UnknownPathAttribute {
|
|
flags,
|
|
type_code,
|
|
payload,
|
|
} => {
|
|
out.put_u8(flags.0);
|
|
out.put_u8(*type_code);
|
|
if flags.extended_length() {
|
|
out.put_u16(payload.len() as u16);
|
|
} else {
|
|
out.put_u8(payload.len() as u8);
|
|
}
|
|
out.put(payload.as_slice());
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Returns the total on-wire length of this path attribute, including the
|
|
/// attribute header (flags + type + length) and the payload.
|
|
pub fn wire_len(&self, ctx: &ParserContext) -> Result<u16> {
|
|
let payload_len: u16 = match self {
|
|
PathAttribute::Origin(a) => a.wire_len(ctx)?,
|
|
PathAttribute::AsPath(a) => a.wire_len(ctx)?,
|
|
PathAttribute::NextHop(a) => a.wire_len(ctx)?,
|
|
PathAttribute::MultiExitDisc(a) => a.wire_len(ctx)?,
|
|
PathAttribute::LocalPref(a) => a.wire_len(ctx)?,
|
|
PathAttribute::AtomicAggregate(a) => a.wire_len(ctx)?,
|
|
PathAttribute::Aggregator(a) => a.wire_len(ctx)?,
|
|
PathAttribute::Communitites(a) => a.wire_len(ctx)?,
|
|
PathAttribute::MpReachNlri(a) => a.wire_len(ctx)?,
|
|
PathAttribute::MpUnreachNlri(a) => a.wire_len(ctx)?,
|
|
PathAttribute::ExtendedCommunities(a) => a.wire_len(ctx)?,
|
|
PathAttribute::LargeCommunities(a) => a.wire_len(ctx)?,
|
|
PathAttribute::UnknownPathAttribute { flags, payload, .. } => {
|
|
// An unknown attribute preserves its original extended_length
|
|
// flag on re-serialization, so use it verbatim here rather than
|
|
// re-deriving it from the payload length.
|
|
let payload_len = u16::try_from(payload.len())
|
|
.map_err(|_| eyre!("UnknownPathAttribute payload exceeds u16::MAX"))?;
|
|
let header_len: u16 = if flags.extended_length() { 4 } else { 3 };
|
|
return Ok(header_len + payload_len);
|
|
}
|
|
};
|
|
let header_len: u16 = if payload_len > u8::MAX as u16 { 4 } else { 3 };
|
|
Ok(header_len + payload_len)
|
|
}
|
|
}
|
|
|
|
/// Origin path attribute is a mandatory attribute defined in RFC4271.
|
|
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize_repr, Deserialize_repr)]
|
|
#[repr(u8)]
|
|
pub enum OriginPathAttribute {
|
|
IGP = 0,
|
|
EGP = 1,
|
|
INCOMPLETE = 2,
|
|
}
|
|
|
|
impl OriginPathAttribute {
|
|
pub fn from_wire<'a>(
|
|
_: &ParserContext,
|
|
buf: &'a [u8],
|
|
) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> {
|
|
let (buf, byte) = be_u8(buf)?;
|
|
let attr = match byte {
|
|
b if b == OriginPathAttribute::IGP as u8 => OriginPathAttribute::IGP,
|
|
b if b == OriginPathAttribute::EGP as u8 => OriginPathAttribute::EGP,
|
|
b if b == OriginPathAttribute::INCOMPLETE as u8 => OriginPathAttribute::INCOMPLETE,
|
|
_ => {
|
|
return IResult::Err(nom::Err::Failure(BgpParserError::Eyre(eyre!(
|
|
"Unknown Origin type {}",
|
|
byte
|
|
))));
|
|
}
|
|
};
|
|
Ok((buf, attr))
|
|
}
|
|
|
|
pub fn wire_len(&self, _ctx: &ParserContext) -> Result<u16> {
|
|
Ok(1)
|
|
}
|
|
|
|
pub fn to_wire(&self, _ctx: &ParserContext, out: &mut BytesMut) -> Result<()> {
|
|
out.put_u8(*self as u8);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl TryFrom<u8> for OriginPathAttribute {
|
|
type Error = eyre::Error;
|
|
|
|
fn try_from(value: u8) -> Result<Self> {
|
|
match value {
|
|
0 => Ok(Self::IGP),
|
|
1 => Ok(Self::EGP),
|
|
2 => Ok(Self::INCOMPLETE),
|
|
other => bail!("Unexpected origin code {}", other),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// ASPathAttribute is a well-known mandatory attribute that contains a list of TLV encoded path
|
|
/// segments. Type is either 1 for AS_SET or 2 for AS_SEQUENCE, length is a 1 octet field
|
|
/// containing the number of ASNS and the value contains the ASNs. This is defined in Section 4.3
|
|
/// of RFC4271.
|
|
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
|
pub struct AsPathAttribute {
|
|
pub segments: Vec<AsPathSegment>,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
|
pub struct AsPathSegment {
|
|
/// ordered is true when representing an AS_SEQUENCE, andd false when
|
|
/// representing an AS_SET.
|
|
pub ordered: bool,
|
|
/// Path is the list of ASNs.
|
|
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],
|
|
})
|
|
}
|
|
|
|
pub fn from_wire<'a>(
|
|
ctx: &ParserContext,
|
|
buf: &'a [u8],
|
|
) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> {
|
|
let parse_segment = |ctx: &ParserContext,
|
|
buf: &'a [u8]|
|
|
-> IResult<&'a [u8], AsPathSegment, BgpParserError<&'a [u8]>> {
|
|
let (buf, typ) = be_u8(buf)?;
|
|
let (buf, len) = be_u8(buf)?;
|
|
let (buf, asns): (_, Vec<u32>) = match ctx.four_octet_asn {
|
|
Some(true) => {
|
|
nom::multi::many_m_n(len as usize, len as usize, be_u32).parse(buf)?
|
|
}
|
|
Some(false) => nom::multi::many_m_n(len as usize, len as usize, be_u16)
|
|
.parse(buf)
|
|
.map(|(buf, asns)| (buf, asns.iter().map(|asn| *asn as u32).collect()))?,
|
|
None => {
|
|
return Err(nom::Err::Failure(BgpParserError::CustomText(
|
|
"Context must set four_octet_asn before being used",
|
|
)));
|
|
}
|
|
};
|
|
|
|
Ok((
|
|
buf,
|
|
AsPathSegment {
|
|
ordered: typ == 2,
|
|
path: asns,
|
|
},
|
|
))
|
|
};
|
|
|
|
let (buf, segments) =
|
|
nom::multi::many0(|buf: &'a [u8]| parse_segment(ctx, buf)).parse(buf)?;
|
|
|
|
Ok((buf, Self { segments }))
|
|
}
|
|
|
|
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<()> {
|
|
if ctx.four_octet_asn.is_none_or(|val| !val) {
|
|
bail!("AsPathAttribute can only be sent for four_octet_asn enabled peers");
|
|
}
|
|
|
|
for segment in &self.segments {
|
|
// Segment type.
|
|
out.put_u8(if segment.ordered { 2 } else { 1 });
|
|
// Segment AS length.
|
|
out.put_u8(
|
|
segment
|
|
.path
|
|
.len()
|
|
.try_into()
|
|
.wrap_err("AS Path length too long")?,
|
|
);
|
|
// AS numbers.
|
|
match ctx
|
|
.four_octet_asn
|
|
.ok_or(eyre!("ctx.four_octet_asn must be set"))?
|
|
{
|
|
true => {
|
|
for asn in &segment.path {
|
|
out.put_u32(*asn);
|
|
}
|
|
}
|
|
false => {
|
|
for asn in &segment.path {
|
|
out.put_u16(
|
|
u16::try_from(*asn)
|
|
.map_err(|e| eyre!("AS number did not fit into u16: {}", e))?,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn wire_len(&self, ctx: &ParserContext) -> Result<u16> {
|
|
let mut counter = 0;
|
|
for segment in &self.segments {
|
|
counter += match ctx.four_octet_asn {
|
|
Some(true) => 2 + (4 * segment.path.len()),
|
|
Some(false) => 2 + (2 * segment.path.len()),
|
|
None => bail!("ParserContext needs four_octet_asn set"),
|
|
};
|
|
}
|
|
Ok(counter as u16)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
|
pub struct NextHopPathAttribute(pub Ipv4Addr);
|
|
|
|
impl NextHopPathAttribute {
|
|
pub fn from_wire<'a>(
|
|
_: &ParserContext,
|
|
buf: &'a [u8],
|
|
) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> {
|
|
let (buf, ip_u32) = be_u32(buf)?;
|
|
Ok((buf, Self(Ipv4Addr::from(ip_u32))))
|
|
}
|
|
|
|
pub fn to_wire(&self, _ctx: &ParserContext, out: &mut BytesMut) -> Result<()> {
|
|
out.put_u32(self.0.into());
|
|
Ok(())
|
|
}
|
|
|
|
pub fn wire_len(&self, _: &ParserContext) -> Result<u16> {
|
|
Ok(4)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
|
pub struct MultiExitDiscPathAttribute(pub u32);
|
|
|
|
impl MultiExitDiscPathAttribute {
|
|
pub fn from_wire<'a>(
|
|
_: &ParserContext,
|
|
buf: &'a [u8],
|
|
) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> {
|
|
let (buf, val) = be_u32(buf)?;
|
|
Ok((buf, Self(val)))
|
|
}
|
|
|
|
pub fn to_wire(&self, _: &ParserContext, out: &mut BytesMut) -> Result<()> {
|
|
out.put_u32(self.0);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn wire_len(&self, _: &ParserContext) -> Result<u16> {
|
|
Ok(4)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
|
pub struct LocalPrefPathAttribute(pub u32);
|
|
|
|
impl LocalPrefPathAttribute {
|
|
pub fn from_wire<'a>(
|
|
_: &ParserContext,
|
|
buf: &'a [u8],
|
|
) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> {
|
|
let (buf, val) = be_u32(buf)?;
|
|
Ok((buf, Self(val)))
|
|
}
|
|
|
|
pub fn to_wire(&self, _: &ParserContext, out: &mut BytesMut) -> Result<()> {
|
|
out.put_u32(self.0);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn wire_len(&self, _: &ParserContext) -> Result<u16> {
|
|
Ok(4)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
|
pub struct AtomicAggregatePathAttribute {}
|
|
|
|
impl AtomicAggregatePathAttribute {
|
|
pub fn from_wire<'a>(
|
|
_: &ParserContext,
|
|
buf: &'a [u8],
|
|
) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> {
|
|
Ok((buf, Self {}))
|
|
}
|
|
|
|
pub fn to_wire(&self, _: &ParserContext, _: &mut BytesMut) -> Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
pub fn wire_len(&self, _: &ParserContext) -> Result<u16> {
|
|
Ok(0)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
|
pub struct AggregatorPathAttribute {
|
|
pub asn: u32,
|
|
pub ip: Ipv4Addr,
|
|
}
|
|
|
|
impl AggregatorPathAttribute {
|
|
pub fn from_wire<'a>(
|
|
ctx: &ParserContext,
|
|
buf: &'a [u8],
|
|
) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> {
|
|
if ctx.four_octet_asn.is_none_or(|val| !val) {
|
|
return Err(nom::Err::Failure(BgpParserError::CustomText(
|
|
"AggregatorPathAttribute can only be parsed for four_octet_asn enabled peers",
|
|
)));
|
|
}
|
|
let (buf, asn) = be_u32(buf)?;
|
|
let (buf, ip) = be_u32(buf)?;
|
|
Ok((
|
|
buf,
|
|
Self {
|
|
asn,
|
|
ip: Ipv4Addr::from(ip),
|
|
},
|
|
))
|
|
}
|
|
|
|
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<()> {
|
|
if ctx.four_octet_asn.is_none_or(|val| !val) {
|
|
bail!("AggregatorPathAttribute can only be sent for four_octet_asn enabled peers");
|
|
}
|
|
out.put_u32(self.asn);
|
|
out.put_u32(self.ip.into());
|
|
Ok(())
|
|
}
|
|
|
|
pub fn wire_len(&self, _: &ParserContext) -> Result<u16> {
|
|
Ok(8)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
|
pub struct CommunitiesPathAttribute(Vec<(u16, u16)>);
|
|
|
|
impl CommunitiesPathAttribute {
|
|
pub fn from_wire<'a>(
|
|
_: &ParserContext,
|
|
buf: &'a [u8],
|
|
) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> {
|
|
let (buf, values) = nom::multi::many0(|i| (be_u16, be_u16).parse(i)).parse(buf)?;
|
|
Ok((buf, CommunitiesPathAttribute(values)))
|
|
}
|
|
|
|
pub fn to_wire(&self, _: &ParserContext, out: &mut BytesMut) -> Result<()> {
|
|
for value in &self.0 {
|
|
out.put_u16(value.0);
|
|
out.put_u16(value.1);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn wire_len(&self, _: &ParserContext) -> Result<u16> {
|
|
Ok((self.0.len() * 4) as u16)
|
|
}
|
|
}
|
|
|
|
/// Extended Communities as defined in https://www.rfc-editor.org/rfc/rfc4360.html.
|
|
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
|
pub struct ExtendedCommunitiesPathAttribute {
|
|
pub extended_communities: Vec<ExtendedCommunity>,
|
|
}
|
|
|
|
impl ExtendedCommunitiesPathAttribute {
|
|
pub fn from_wire<'a>(
|
|
ctx: &ParserContext,
|
|
buf: &'a [u8],
|
|
) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> {
|
|
let (buf, extended_communities) =
|
|
nom::multi::many1(|buf| ExtendedCommunity::from_wire(ctx, buf)).parse(buf)?;
|
|
Ok((
|
|
buf,
|
|
Self {
|
|
extended_communities,
|
|
},
|
|
))
|
|
}
|
|
|
|
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<()> {
|
|
for ec in &self.extended_communities {
|
|
ec.to_wire(ctx, out)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn wire_len(&self, ctx: &ParserContext) -> Result<u16> {
|
|
Ok(self
|
|
.extended_communities
|
|
.iter()
|
|
.map(|ec| ec.wire_len(ctx))
|
|
.sum::<Result<u16>>()?)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
|
pub enum ExtendedCommunity {
|
|
/// AS Specific Extended Community as specified in Section 3.1 of RFC4360.
|
|
AsSpecific {
|
|
typ: u8,
|
|
sub_typ: u8,
|
|
global_admin: u16,
|
|
local_admin: u32,
|
|
},
|
|
/// Ipv4 Address Specific Extended Community as specified in Section 3.2 of RFC4360.
|
|
Ipv4AddrSpecific {
|
|
typ: u8,
|
|
sub_typ: u8,
|
|
global_admin: u32,
|
|
local_admin: u16,
|
|
},
|
|
/// Opaque Extended Community as specified in Section 3.3 of RFC4360.
|
|
Opaque {
|
|
typ: u8,
|
|
sub_typ: u8,
|
|
value: [u8; 5],
|
|
},
|
|
/// Route Target Community as specified in Section 4 of RFC4360.
|
|
RouteTarget {
|
|
typ: u8,
|
|
sub_typ: u8,
|
|
global_admin: u32,
|
|
local_admin: u16,
|
|
},
|
|
/// Route Origin Community as specified in Section 5 of RFC4360.
|
|
RouteOrigin {
|
|
typ: u8,
|
|
sub_typ: u8,
|
|
global_admin: u16,
|
|
local_admin: u32,
|
|
},
|
|
}
|
|
|
|
impl ExtendedCommunity {
|
|
pub fn from_wire<'a>(
|
|
_: &ParserContext,
|
|
buf: &'a [u8],
|
|
) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> {
|
|
let (buf, typ) = be_u8(buf)?;
|
|
let (buf, sub_typ) = be_u8(buf)?;
|
|
let (buf, parsed) = match (typ, sub_typ) {
|
|
// Route Target Extended Community.
|
|
(0x00 | 0x01 | 0x02, 0x02) => {
|
|
let (buf, global_admin) = be_u32(buf)?;
|
|
let (buf, local_admin) = be_u16(buf)?;
|
|
(
|
|
buf,
|
|
Self::RouteTarget {
|
|
typ,
|
|
sub_typ,
|
|
global_admin,
|
|
local_admin,
|
|
},
|
|
)
|
|
}
|
|
// Route Origin Extended Community.
|
|
(0x00 | 0x01 | 0x02, 0x03) => {
|
|
let (buf, global_admin) = be_u16(buf)?;
|
|
let (buf, local_admin) = be_u32(buf)?;
|
|
(
|
|
buf,
|
|
Self::RouteOrigin {
|
|
typ,
|
|
sub_typ,
|
|
global_admin,
|
|
local_admin,
|
|
},
|
|
)
|
|
}
|
|
// AS specific Extended Community.
|
|
(0x00 | 0x40, _) => {
|
|
let (buf, global_admin) = be_u16(buf)?;
|
|
let (buf, local_admin) = be_u32(buf)?;
|
|
(
|
|
buf,
|
|
Self::AsSpecific {
|
|
typ,
|
|
sub_typ,
|
|
global_admin,
|
|
local_admin,
|
|
},
|
|
)
|
|
}
|
|
// IPv4 Address Specific Extended Community.
|
|
(0x01 | 0x41, _) => {
|
|
let (buf, global_admin) = be_u32(buf)?;
|
|
let (buf, local_admin) = be_u16(buf)?;
|
|
(
|
|
buf,
|
|
Self::Ipv4AddrSpecific {
|
|
typ,
|
|
sub_typ,
|
|
global_admin,
|
|
local_admin,
|
|
},
|
|
)
|
|
}
|
|
_ => {
|
|
let (buf, payload) = nom::bytes::take(5_usize).parse(buf)?;
|
|
let value: [u8; 5] = payload.try_into().map_err(|_| {
|
|
Failure(BgpParserError::CustomText(
|
|
"Expected exactly 5 bytes from the parser",
|
|
))
|
|
})?;
|
|
(
|
|
buf,
|
|
Self::Opaque {
|
|
typ,
|
|
sub_typ,
|
|
value,
|
|
},
|
|
)
|
|
}
|
|
};
|
|
|
|
return Ok((buf, parsed));
|
|
}
|
|
|
|
pub fn to_wire(&self, _: &ParserContext, out: &mut BytesMut) -> Result<()> {
|
|
match self {
|
|
ExtendedCommunity::AsSpecific {
|
|
typ,
|
|
sub_typ,
|
|
global_admin,
|
|
local_admin,
|
|
} => {
|
|
out.put_u8(*typ);
|
|
out.put_u8(*sub_typ);
|
|
out.put_u16(*global_admin);
|
|
out.put_u32(*local_admin);
|
|
}
|
|
ExtendedCommunity::Ipv4AddrSpecific {
|
|
typ,
|
|
sub_typ,
|
|
global_admin,
|
|
local_admin,
|
|
} => {
|
|
out.put_u8(*typ);
|
|
out.put_u8(*sub_typ);
|
|
out.put_u32(*global_admin);
|
|
out.put_u16(*local_admin);
|
|
}
|
|
ExtendedCommunity::Opaque {
|
|
typ,
|
|
sub_typ,
|
|
value,
|
|
} => {
|
|
out.put_u8(*typ);
|
|
out.put_u8(*sub_typ);
|
|
out.put(&value[..]);
|
|
}
|
|
ExtendedCommunity::RouteTarget {
|
|
typ,
|
|
sub_typ,
|
|
global_admin,
|
|
local_admin,
|
|
} => {
|
|
out.put_u8(*typ);
|
|
out.put_u8(*sub_typ);
|
|
out.put_u32(*global_admin);
|
|
out.put_u16(*local_admin);
|
|
}
|
|
ExtendedCommunity::RouteOrigin {
|
|
typ,
|
|
sub_typ,
|
|
global_admin,
|
|
local_admin,
|
|
} => {
|
|
out.put_u8(*typ);
|
|
out.put_u8(*sub_typ);
|
|
out.put_u16(*global_admin);
|
|
out.put_u32(*local_admin);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn wire_len(&self, _: &ParserContext) -> Result<u16> {
|
|
Ok(8)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct LargeCommunitiesPathAttribute {
|
|
pub communities: Vec<LargeCommunity>,
|
|
}
|
|
|
|
impl LargeCommunitiesPathAttribute {
|
|
pub fn from_wire<'a>(
|
|
ctx: &ParserContext,
|
|
buf: &'a [u8],
|
|
) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> {
|
|
let (buf, communities) =
|
|
nom::multi::many1(|buf| LargeCommunity::from_wire(ctx, buf)).parse(buf)?;
|
|
Ok((buf, Self { communities }))
|
|
}
|
|
|
|
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<()> {
|
|
for community in &self.communities {
|
|
community.to_wire(ctx, out)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn wire_len(&self, _: &ParserContext) -> Result<u16> {
|
|
Ok(12_u16 * self.communities.len() as u16)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct LargeCommunity {
|
|
pub global_admin: u32,
|
|
pub data_1: u32,
|
|
pub data_2: u32,
|
|
}
|
|
|
|
impl LargeCommunity {
|
|
pub fn from_wire<'a>(
|
|
_: &ParserContext,
|
|
buf: &'a [u8],
|
|
) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> {
|
|
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, _: &ParserContext, out: &mut BytesMut) -> Result<()> {
|
|
out.put_u32(self.global_admin);
|
|
out.put_u32(self.data_1);
|
|
out.put_u32(self.data_2);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn wire_len(&self, _: &ParserContext) -> Result<u16> {
|
|
Ok(12)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub enum NlriNextHop {
|
|
/// Represents an IPv4 address nexthop.
|
|
Ipv4(Ipv4Addr),
|
|
/// Represents a global IPv6 nexthop.
|
|
Ipv6(Ipv6Addr),
|
|
/// Represents a IPv6 Link Local and global address pair nexthop.
|
|
Ipv6WithLl {
|
|
global: Ipv6Addr,
|
|
link_local: Ipv6Addr,
|
|
},
|
|
}
|
|
|
|
impl NlriNextHop {
|
|
pub fn to_wire(&self, _: &ParserContext, out: &mut BytesMut) -> Result<()> {
|
|
match self {
|
|
NlriNextHop::Ipv4(ipv4_addr) => out.put(&ipv4_addr.octets()[..]),
|
|
NlriNextHop::Ipv6(ipv6_addr) => out.put(&ipv6_addr.octets()[..]),
|
|
NlriNextHop::Ipv6WithLl { global, link_local } => {
|
|
out.put(&global.octets()[..]);
|
|
out.put(&link_local.octets()[..])
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn wire_len(&self) -> u8 {
|
|
match self {
|
|
NlriNextHop::Ipv4(_) => 4,
|
|
NlriNextHop::Ipv6(_) => 16,
|
|
NlriNextHop::Ipv6WithLl { .. } => 32,
|
|
}
|
|
}
|
|
}
|
|
|
|
// parse_prefix is a helper function that implements an NLRI parser for the given AFI.
|
|
fn parse_prefix<'a>(
|
|
afi: AddressFamilyId,
|
|
buf: &'a [u8],
|
|
) -> IResult<&'a [u8], IpPrefix, BgpParserError<&'a [u8]>> {
|
|
let (buf, prefix_len) = be_u8(buf)?;
|
|
let byte_len = (prefix_len + 7) / 8;
|
|
let (buf, prefix_bytes) = nom::bytes::take(byte_len as usize).parse(buf)?;
|
|
let prefix = IpPrefix::new(afi, prefix_bytes.to_vec(), prefix_len)
|
|
.map_err(|e| Failure(BgpParserError::Eyre(e)))?;
|
|
Ok((buf, prefix))
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct MpReachNlriPathAttribute {
|
|
pub afi: AddressFamilyId,
|
|
pub safi: SubsequentAfi,
|
|
/// Next hop address (either IPv4 or IPv6 for now).
|
|
pub next_hop: NlriNextHop,
|
|
pub prefixes: Vec<IpPrefix>,
|
|
}
|
|
|
|
impl MpReachNlriPathAttribute {
|
|
pub fn from_wire<'a>(
|
|
_: &ParserContext,
|
|
buf: &'a [u8],
|
|
) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> {
|
|
let (buf, raw_afi) = be_u16(buf)?;
|
|
let afi =
|
|
AddressFamilyId::try_from(raw_afi).map_err(|e| Failure(BgpParserError::Eyre(e)))?;
|
|
|
|
let (buf, raw_safi) = be_u8(buf)?;
|
|
let safi =
|
|
SubsequentAfi::try_from(raw_safi).map_err(|e| Failure(BgpParserError::Eyre(e)))?;
|
|
|
|
let (buf, nh_len) = be_u8(buf)?;
|
|
|
|
let (buf, next_hop, prefixes) = match afi {
|
|
AddressFamilyId::Ipv4 => {
|
|
// Read the length of the nexthop which should equal 4.
|
|
if nh_len != 4 {
|
|
return Err(Failure(BgpParserError::Eyre(eyre!(
|
|
"Got nexthop address length {} when expected 4 for IPv4 AFI",
|
|
nh_len
|
|
))));
|
|
}
|
|
// Read the nexthop address which should now be an IPv4 address.
|
|
let (buf, nh_bytes) = be_u32(buf)?;
|
|
let next_hop = NlriNextHop::Ipv4(Ipv4Addr::from(nh_bytes));
|
|
|
|
let (buf, prefixes) =
|
|
nom::multi::many0(|buf| parse_prefix(AddressFamilyId::Ipv4, buf)).parse(buf)?;
|
|
(buf, next_hop, prefixes)
|
|
}
|
|
AddressFamilyId::Ipv6 => {
|
|
// https://datatracker.ietf.org/doc/html/rfc2545 defines that the nexthop address may be 16 or 32 bytes long.
|
|
let (buf, nh_bytes) = nom::bytes::take(nh_len as usize).parse(buf)?;
|
|
let nexthop = match nh_bytes.len() {
|
|
16 => {
|
|
// unwrap should never fire since we have explicitly checked the length.
|
|
let slice: [u8; 16] = nh_bytes.try_into().unwrap();
|
|
NlriNextHop::Ipv6(Ipv6Addr::from(slice))
|
|
}
|
|
32 => {
|
|
// unwrap should never fire since we have explicitly checked the length.
|
|
let slice: [u8; 32] = nh_bytes.try_into().unwrap();
|
|
let global_bytes: [u8; 16] = slice[0..16].try_into().unwrap();
|
|
let global = Ipv6Addr::from(global_bytes);
|
|
let link_local_bytes: [u8; 16] = slice[16..32].try_into().unwrap();
|
|
let link_local = Ipv6Addr::from(link_local_bytes);
|
|
NlriNextHop::Ipv6WithLl { global, link_local }
|
|
}
|
|
_ => {
|
|
return Err(Failure(BgpParserError::Eyre(eyre!(
|
|
"Mismatched IPv6 nexthop length, got {}, want 16 or 32",
|
|
nh_bytes.len()
|
|
))));
|
|
}
|
|
};
|
|
// Reserved 0 byte (formerly SNPA).
|
|
let (buf, _) = be_u8(buf)?;
|
|
let (buf, prefixes) =
|
|
nom::multi::many0(|buf| parse_prefix(AddressFamilyId::Ipv6, buf)).parse(buf)?;
|
|
(buf, nexthop, prefixes)
|
|
}
|
|
};
|
|
|
|
Ok((
|
|
buf,
|
|
Self {
|
|
afi,
|
|
safi,
|
|
next_hop,
|
|
prefixes,
|
|
},
|
|
))
|
|
}
|
|
|
|
pub fn to_wire(&self, ctx: &ParserContext, out: &mut BytesMut) -> Result<()> {
|
|
out.put_u16(self.afi as u16);
|
|
out.put_u8(self.safi as u8);
|
|
out.put_u8(self.next_hop.wire_len());
|
|
self.next_hop.to_wire(ctx, out)?;
|
|
out.put_u8(0);
|
|
for prefix in &self.prefixes {
|
|
out.put_u8(prefix.length);
|
|
out.put(&prefix.prefix[..]);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn wire_len(&self, _: &ParserContext) -> Result<u16> {
|
|
Ok(4_u16
|
|
+ self.next_hop.wire_len() as u16
|
|
+ 1 // Reserved byte (SNPA).
|
|
+ self
|
|
.prefixes
|
|
.iter()
|
|
.map(|p| 1 + p.prefix.len() as u16)
|
|
.sum::<u16>())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct MpUnreachNlriPathAttribute {
|
|
pub afi: AddressFamilyId,
|
|
pub safi: SubsequentAfi,
|
|
pub prefixes: Vec<IpPrefix>,
|
|
}
|
|
|
|
impl MpUnreachNlriPathAttribute {
|
|
pub fn from_wire<'a>(
|
|
_: &ParserContext,
|
|
buf: &'a [u8],
|
|
) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> {
|
|
let (buf, raw_afi) = be_u16(buf)?;
|
|
let afi =
|
|
AddressFamilyId::try_from(raw_afi).map_err(|e| Failure(BgpParserError::Eyre(e)))?;
|
|
|
|
let (buf, raw_safi) = be_u8(buf)?;
|
|
let safi =
|
|
SubsequentAfi::try_from(raw_safi).map_err(|e| Failure(BgpParserError::Eyre(e)))?;
|
|
|
|
let (buf, prefixes) = nom::multi::many0(|buf| parse_prefix(afi, buf)).parse(buf)?;
|
|
|
|
Ok((
|
|
buf,
|
|
MpUnreachNlriPathAttribute {
|
|
afi,
|
|
safi,
|
|
prefixes,
|
|
},
|
|
))
|
|
}
|
|
|
|
pub fn to_wire(&self, _: &ParserContext, out: &mut BytesMut) -> Result<()> {
|
|
out.put_u16(self.afi as u16);
|
|
out.put_u8(self.safi as u8);
|
|
for prefix in &self.prefixes {
|
|
out.put_u8(prefix.length);
|
|
out.put(&prefix.prefix[..]);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn wire_len(&self, _: &ParserContext) -> Result<u16> {
|
|
Ok(3 + self
|
|
.prefixes
|
|
.iter()
|
|
.map(|p| 1 + p.prefix.len() as u16)
|
|
.sum::<u16>())
|
|
}
|
|
}
|
|
|
|
/// Represents a BGP NOTIFICATION message per RFC 4271 §4.5.
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct NotificationMessage {
|
|
pub error: BgpError,
|
|
/// Optional diagnostic data; may be empty.
|
|
pub data: Vec<u8>,
|
|
}
|
|
|
|
impl NotificationMessage {
|
|
/// Parses a NOTIFICATION message body (without the 19-byte BGP header).
|
|
/// Consumes the entire buffer; any remaining bytes become `data`.
|
|
pub fn from_wire<'a>(
|
|
_ctx: &ParserContext,
|
|
buf: &'a [u8],
|
|
) -> IResult<&'a [u8], Self, BgpParserError<&'a [u8]>> {
|
|
let (buf, error_code) = be_u8(buf)?;
|
|
let (buf, error_subcode) = be_u8(buf)?;
|
|
|
|
let error = BgpError::from_code_subcode(error_code, error_subcode)
|
|
.map_err(|e| Failure(BgpParserError::Eyre(e.into())))?;
|
|
|
|
Ok((
|
|
&[],
|
|
Self {
|
|
error,
|
|
data: buf.to_vec(),
|
|
},
|
|
))
|
|
}
|
|
|
|
pub fn to_wire(&self, _ctx: &ParserContext, buf: &mut BytesMut) -> Result<()> {
|
|
let (code, subcode) = self.error.code_subcode();
|
|
buf.put_u8(code);
|
|
buf.put_u8(subcode);
|
|
buf.put_slice(&self.data);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn wire_len(&self) -> u16 {
|
|
2 + self.data.len() as u16
|
|
}
|
|
}
|
|
|
|
#[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::{AddressFamilyId, SubsequentAfi},
|
|
ip_prefix::IpPrefix,
|
|
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.
|
|
/// This uses `four_octet_asn` set to true and address_family to IPv6.
|
|
fn default_v6_context() -> ParserContext {
|
|
ParserContext {
|
|
four_octet_asn: Some(true),
|
|
address_family: Some(AddressFamilyId::Ipv6),
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
&[0xc0, 0x06, 0x00],
|
|
PathAttribute::AtomicAggregate(AtomicAggregatePathAttribute {})
|
|
);
|
|
|
|
// Aggregator Path Attribute.
|
|
test_path_attribute_roundtrip!(
|
|
test_aggregator,
|
|
&[
|
|
0x80, 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::Communitites(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 {
|
|
afi: AddressFamilyId::Ipv6,
|
|
safi: SubsequentAfi::Unicast,
|
|
next_hop: NlriNextHop::Ipv6WithLl {
|
|
global: ipv6!("2001:db8::1"),
|
|
link_local: ipv6!("fe80::12"),
|
|
},
|
|
prefixes: vec![IpPrefix {
|
|
address_family: AddressFamilyId::Ipv6,
|
|
prefix: vec![0x20, 0x01, 0x0d, 0xb8],
|
|
length: 32,
|
|
}]
|
|
})
|
|
);
|
|
|
|
// 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 {
|
|
afi: AddressFamilyId::Ipv6,
|
|
safi: SubsequentAfi::Unicast,
|
|
prefixes: vec![IpPrefix {
|
|
address_family: AddressFamilyId::Ipv6,
|
|
prefix: vec![0x20, 0x01, 0x0d, 0xb8],
|
|
length: 32,
|
|
}],
|
|
})
|
|
);
|
|
|
|
// 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::RouteOrigin {
|
|
typ: 0,
|
|
sub_typ: 3,
|
|
global_admin: 65000,
|
|
local_admin: 1337
|
|
},
|
|
ExtendedCommunity::RouteOrigin {
|
|
typ: 0,
|
|
sub_typ: 3,
|
|
global_admin: 65001,
|
|
local_admin: 1337
|
|
},
|
|
ExtendedCommunity::RouteOrigin {
|
|
typ: 2,
|
|
sub_typ: 3,
|
|
global_admin: 65000,
|
|
local_admin: 1337
|
|
},
|
|
]
|
|
})
|
|
);
|
|
|
|
// 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 --
|
|
|
|
/// Asserts that an UPDATE message roundtrips byte-for-byte and that
|
|
/// wire_len agrees with the length of the encoded output.
|
|
macro_rules! test_update_roundtrip {
|
|
($name:ident, $input_bytes:expr, $expected:expr) => {
|
|
#[test]
|
|
fn $name() -> Result<()> {
|
|
let ctx = &default_v6_context();
|
|
let (rem, parsed) = UpdateMessage::from_wire(ctx, $input_bytes)?;
|
|
assert!(rem.is_empty(), "Expected no trailing bytes, got {:x?}", rem);
|
|
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);
|
|
assert_eq!(parsed.wire_len(ctx)?, $input_bytes.len() as u16);
|
|
Ok(())
|
|
}
|
|
};
|
|
}
|
|
|
|
// Wrapper to generate a IPv4 prefix used in tests below.
|
|
fn v4_prefix(length: u8, bytes: &[u8]) -> IpPrefix {
|
|
IpPrefix {
|
|
address_family: AddressFamilyId::Ipv4,
|
|
prefix: bytes.to_vec(),
|
|
length,
|
|
}
|
|
}
|
|
|
|
/// 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])],
|
|
}
|
|
);
|
|
|
|
// 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])],
|
|
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])],
|
|
path_attributes: sample_ipv4_path_attributes(),
|
|
announced_routes: vec![
|
|
v4_prefix(16, &[0xac, 0x10]),
|
|
v4_prefix(24, &[0xc0, 0xa8, 0x01]),
|
|
],
|
|
}
|
|
);
|
|
|
|
// 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).
|
|
#[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 = UpdateMessage::from_wire(&ctx, buf);
|
|
assert!(
|
|
result.is_err(),
|
|
"Expected a parse error for trailing garbage, got {:?}",
|
|
result
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_notification_roundtrip() -> Result<()> {
|
|
use crate::errors::{BgpError, CeaseSubcode};
|
|
|
|
let ctx = &default_v6_context();
|
|
let msg = NotificationMessage {
|
|
error: BgpError::Cease(CeaseSubcode::AdminShutdown),
|
|
data: vec![0xde, 0xad],
|
|
};
|
|
|
|
let mut buf = BytesMut::new();
|
|
msg.to_wire(ctx, &mut buf)?;
|
|
assert_eq!(buf.as_ref(), &[6, 2, 0xde, 0xad]);
|
|
|
|
let (rem, parsed) = NotificationMessage::from_wire(ctx, &buf).unwrap();
|
|
assert!(rem.is_empty());
|
|
assert_eq!(parsed, msg);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_notification_no_data() -> Result<()> {
|
|
use crate::errors::{BgpError, FsmSubcode};
|
|
|
|
let ctx = &default_v6_context();
|
|
let msg = NotificationMessage {
|
|
error: BgpError::FsmError(FsmSubcode::UnexpectedEstablished),
|
|
data: vec![],
|
|
};
|
|
|
|
let mut buf = BytesMut::new();
|
|
msg.to_wire(ctx, &mut buf)?;
|
|
assert_eq!(buf.as_ref(), &[5, 3]);
|
|
|
|
let (rem, parsed) = NotificationMessage::from_wire(ctx, &buf).unwrap();
|
|
assert!(rem.is_empty());
|
|
assert_eq!(parsed, msg);
|
|
Ok(())
|
|
}
|
|
|
|
#[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]);
|
|
|
|
let (rem, parsed) = Message::from_wire(ctx, &buf).unwrap();
|
|
assert!(rem.is_empty());
|
|
assert!(matches!(parsed, Message::KeepAlive));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_message_notification_roundtrip() -> Result<()> {
|
|
use crate::errors::{BgpError, CeaseSubcode};
|
|
|
|
let ctx = &default_v6_context();
|
|
let inner = NotificationMessage {
|
|
error: BgpError::Cease(CeaseSubcode::AdminShutdown),
|
|
data: vec![],
|
|
};
|
|
let msg = Message::Notification(inner);
|
|
|
|
let mut buf = BytesMut::new();
|
|
msg.to_wire(ctx, &mut buf)?;
|
|
assert_eq!(buf.len(), msg.wire_len(ctx)? as usize);
|
|
|
|
let (rem, parsed) = Message::from_wire(ctx, &buf).unwrap();
|
|
assert!(rem.is_empty());
|
|
assert!(matches!(parsed, Message::Notification(_)));
|
|
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::from_wire(&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::from_wire(&ctx, &buf).is_err());
|
|
}
|
|
}
|