From ab201600142625ca8b8baf2eb6a70e4df935ce54 Mon Sep 17 00:00:00 2001 From: Rayhaan Jaufeerally Date: Sun, 20 Jul 2025 15:25:41 +0200 Subject: [PATCH] feature: Implement more path attributes --- Cargo.lock | 35 ++ Cargo.toml | 33 +- bin/bgp/README.md | 7 + crates/packet/Cargo.toml | 1 + crates/packet/src/message.rs | 599 +++++++++++++++++++++++++++++++++-- 5 files changed, 635 insertions(+), 40 deletions(-) create mode 100644 bin/bgp/README.md diff --git a/Cargo.lock b/Cargo.lock index 6b56452..d4d062f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,7 @@ dependencies = [ "nom", "serde", "serde_repr", + "strum", "thiserror", ] @@ -113,6 +114,12 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "indenter" version = "0.3.3" @@ -247,6 +254,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + [[package]] name = "scopeguard" version = "1.2.0" @@ -309,6 +322,28 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "strum" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" version = "2.0.100" diff --git a/Cargo.toml b/Cargo.toml index 7743a42..d4c4f78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,29 +1,30 @@ [workspace] default-members = ["bin/bgp"] -members = ["bin/*", "crates/*"] -resolver = "3" +members = ["bin/*", "crates/*"] +resolver = "3" [workspace.package] -authors = ["Rayhaan Jaufeerally "] +authors = ["Rayhaan Jaufeerally "] description = "A Border Gateway Protocol implementation" -edition = "2024" -homepage = "https://rayhaan.ch" -license = "Apache-2.0" -name = "bgp" -repository = "https://github.com/net-control-plane/bgp" -version = "0.0.1" +edition = "2024" +homepage = "https://rayhaan.ch" +license = "Apache-2.0" +name = "bgp" +repository = "https://github.com/net-control-plane/bgp" +version = "0.0.1" [workspace.dependencies] # --- Our crates --- bgp-packet = { path = "crates/packet" } # --- General --- -bitfield = "0.19.0" -bytes = "1.10.1" -eyre = "0.6.12" -nom = "8.0.0" -serde = { version = "1.0.219", features = ["derive"] } +bitfield = "0.19.0" +bytes = "1.10.1" +eyre = "0.6.12" +nom = "8.0.0" +serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" serde_repr = "0.1.20" -thiserror = "2.0.12" -tokio = { version = "1.44.2", features = ["full"] } +thiserror = "2.0.12" +tokio = { version = "1.44.2", features = ["full"] } +strum = { version = "0.27.1", features = ["derive"] } diff --git a/bin/bgp/README.md b/bin/bgp/README.md new file mode 100644 index 0000000..cc4a129 --- /dev/null +++ b/bin/bgp/README.md @@ -0,0 +1,7 @@ +# BGP + +This crate provides a binary which runs the Border Gateway Protocol (BGP). + +Please refer to the other implementation crates part of this repository where most of the actual implemntation lives. + +Warning: This is still very much a work in progress and is not yet ready for use. \ No newline at end of file diff --git a/crates/packet/Cargo.toml b/crates/packet/Cargo.toml index 3cbb42c..f069a6f 100644 --- a/crates/packet/Cargo.toml +++ b/crates/packet/Cargo.toml @@ -16,3 +16,4 @@ nom.workspace = true serde.workspace = true serde_repr.workspace = true thiserror.workspace = true +strum.workspace = true diff --git a/crates/packet/src/message.rs b/crates/packet/src/message.rs index ee39510..720458e 100644 --- a/crates/packet/src/message.rs +++ b/crates/packet/src/message.rs @@ -1,4 +1,5 @@ use std::net::{Ipv4Addr, Ipv6Addr}; +use std::ops::Deref; use bitfield::bitfield; use bytes::BufMut; @@ -15,6 +16,7 @@ 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 crate::constants::AddressFamilyId; use crate::constants::SubsequentAfi; @@ -108,19 +110,29 @@ pub enum Capability { pub struct UpdateMessage {} bitfield! { + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct PathAttributeFlags(u8); - impl Debug; + impl new; u8; - optional, set_optional: 0; - transitive, set_transitive: 1; - partial, set_partial: 2; - extended_length, set_extended_length: 3; + 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, + AsPath(AsPathAttribute) = 2, NextHop(NextHopPathAttribute) = 3, MultiExitDisc(MultiExitDiscPathAttribute) = 4, LocalPref(LocalPrefPathAttribute) = 5, @@ -138,7 +150,85 @@ pub enum PathAttribute { }, } +impl From 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>( @@ -154,7 +244,143 @@ impl PathAttribute { be_u8(buf).map(|(buf, b)| (buf, b as u16))? }; - todo!(); + let discriminant = PathAttributeDiscriminants::from(type_code); + 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); + if flags.extended_length() { + out.put_u16(payload.len() as u16); + } else { + out.put_u8(payload.len() as u8); + } + out.put_u8(*type_code); + out.put(payload.as_slice()); + } + } + + Ok(()) } } @@ -167,6 +393,36 @@ pub enum OriginPathAttribute { 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 { + Ok(1) + } + + pub fn to_wire(&self, _ctx: &ParserContext, out: &mut BytesMut) -> Result<()> { + out.put_u8(*self as u8); + Ok(()) + } +} + impl TryFrom for OriginPathAttribute { type Error = eyre::Error; @@ -184,12 +440,12 @@ impl TryFrom for OriginPathAttribute { /// 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)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct AsPathAttribute { pub segments: Vec, } -#[derive(Debug, PartialEq, Eq, Clone, Serialize)] +#[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. @@ -204,7 +460,7 @@ impl AsPathAttribute { ordered: true, path: asns, }; - PathAttribute::ASPath(AsPathAttribute { + PathAttribute::AsPath(AsPathAttribute { segments: vec![segment], }) } @@ -256,7 +512,7 @@ impl AsPathAttribute { // Segment type. out.put_u8(if segment.ordered { 2 } else { 1 }); // Segment AS length. - out.put_u16( + out.put_u8( segment .path .len() @@ -264,8 +520,23 @@ impl AsPathAttribute { .wrap_err("AS Path length too long")?, ); // AS numbers. - for asn in &segment.path { - out.put_u32(*asn); + 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))?, + ); + } + } } } @@ -280,7 +551,6 @@ impl AsPathAttribute { Some(false) => 2 + (2 * segment.path.len()), None => bail!("ParserContext needs four_octet_asn set"), }; - counter += 2 + (4 * segment.path.len()); } Ok(counter as u16) } @@ -298,7 +568,7 @@ impl NextHopPathAttribute { Ok((buf, Self(Ipv4Addr::from(ip_u32)))) } - pub fn to_wire(&self, out: &mut BytesMut) -> Result<()> { + pub fn to_wire(&self, _ctx: &ParserContext, out: &mut BytesMut) -> Result<()> { out.put_u32(self.0.into()); Ok(()) } @@ -444,6 +714,37 @@ pub struct ExtendedCommunitiesPathAttribute { pub extended_communities: Vec, } +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 { + Ok(self + .extended_communities + .iter() + .map(|ec| ec.wire_len(ctx)) + .sum::>()?) + } +} + #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub enum ExtendedCommunity { /// AS Specific Extended Community as specified in Section 3.1 of RFC4360. @@ -631,7 +932,7 @@ impl ExtendedCommunity { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct LargeCommunitiesPathAttribute { pub communities: Vec, } @@ -658,7 +959,7 @@ impl LargeCommunitiesPathAttribute { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct LargeCommunity { pub global_admin: u32, pub data_1: u32, @@ -695,7 +996,7 @@ impl LargeCommunity { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum NlriNextHop { /// Represents an IPv4 address nexthop. Ipv4(Ipv4Addr), @@ -739,12 +1040,12 @@ fn parse_prefix<'a>( 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(), byte_len) + 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)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct MpReachNlriPathAttribute { pub afi: AddressFamilyId, pub safi: SubsequentAfi, @@ -797,10 +1098,10 @@ impl MpReachNlriPathAttribute { 32 => { // unwrap should never fire since we have explicitly checked the length. let slice: [u8; 32] = nh_bytes.try_into().unwrap(); - let link_local_bytes: [u8; 16] = slice[0..16].try_into().unwrap(); - let link_local = Ipv6Addr::from(link_local_bytes); - let global_bytes: [u8; 16] = slice[16..32].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 } } _ => { @@ -810,6 +1111,8 @@ impl MpReachNlriPathAttribute { )))); } }; + // 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) @@ -832,6 +1135,7 @@ impl MpReachNlriPathAttribute { 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[..]); @@ -842,6 +1146,7 @@ impl MpReachNlriPathAttribute { pub fn wire_len(&self, _: &ParserContext) -> Result { Ok(4_u16 + self.next_hop.wire_len() as u16 + + 1 // Reserved byte (SNPA). + self .prefixes .iter() @@ -850,7 +1155,7 @@ impl MpReachNlriPathAttribute { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct MpUnreachNlriPathAttribute { pub afi: AddressFamilyId, pub safi: SubsequentAfi, @@ -903,3 +1208,249 @@ impl MpUnreachNlriPathAttribute { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NotificationMessage {} + +#[cfg(test)] +mod tests { + use std::{ + net::{Ipv4Addr, Ipv6Addr}, + str::FromStr, + }; + + use bytes::BytesMut; + use eyre::{Result, eyre}; + + use crate::{ + constants::{AddressFamilyId, SubsequentAfi}, + ip_prefix::IpPrefix, + message::{ + AggregatorPathAttribute, AsPathAttribute, AsPathSegment, AtomicAggregatePathAttribute, + CommunitiesPathAttribute, ExtendedCommunitiesPathAttribute, ExtendedCommunity, + LargeCommunitiesPathAttribute, LargeCommunity, LocalPrefPathAttribute, + MpReachNlriPathAttribute, MpUnreachNlriPathAttribute, MultiExitDiscPathAttribute, + NextHopPathAttribute, NlriNextHop, OriginPathAttribute, PathAttribute, + }, + 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, + }] + }) + ); +}