From 27b1fbfc39b72a9df1930805c9bc2d45db670b8e Mon Sep 17 00:00:00 2001 From: Mingwei Zhang Date: Mon, 15 Jun 2026 18:21:08 -0700 Subject: [PATCH 1/7] feat: retain raw BGP attributes and add AIGP parser --- Cargo.toml | 2 +- src/models/bgp/attributes/mod.rs | 35 ++++- src/parser/bgp/attributes/README.md | 28 ++-- src/parser/bgp/attributes/attr_26_aigp.rs | 99 ++++++++++++ src/parser/bgp/attributes/mod.rs | 179 +++++++++++++++++----- src/parser/mrt/mrt_elem.rs | 18 ++- 6 files changed, 295 insertions(+), 66 deletions(-) create mode 100644 src/parser/bgp/attributes/attr_26_aigp.rs diff --git a/Cargo.toml b/Cargo.toml index a1f7dfd..150065c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ serde = { version = "1.0", features = ["derive"], optional = true } ####################### # Parser dependencies # ####################### -bytes = { version = "1.7", optional = true } +bytes = { version = "1.7", optional = true, features = ["serde"] } zerocopy = { version = "0.8", features = ["derive"], optional = true } hex = { version = "0.4.3", optional = true } # bmp/openbmp parsing oneio = { version = "0.23", default-features = false, features = ["http", "gz", "bz"], optional = true } diff --git a/src/models/bgp/attributes/mod.rs b/src/models/bgp/attributes/mod.rs index f2a51a3..e44859e 100644 --- a/src/models/bgp/attributes/mod.rs +++ b/src/models/bgp/attributes/mod.rs @@ -5,6 +5,7 @@ mod origin; use crate::models::network::*; use bitflags::bitflags; +use bytes::Bytes; use num_enum::{FromPrimitive, IntoPrimitive}; use std::cmp::Ordering; use std::iter::{FromIterator, Map}; @@ -75,7 +76,6 @@ pub enum AttrType { ORIGINATOR_ID = 9, CLUSTER_LIST = 10, /// - CLUSTER_ID = 13, MP_REACHABLE_NLRI = 14, MP_UNREACHABLE_NLRI = 15, /// @@ -95,6 +95,7 @@ pub enum AttrType { SFP_ATTRIBUTE = 37, BFD_DISCRIMINATOR = 38, BGP_PREFIX_SID = 40, + BIER = 41, ATTR_SET = 128, /// DEVELOPMENT = 255, @@ -168,7 +169,7 @@ impl Attributes { } pub fn add_attr(&mut self, attr: Attribute) { - let ty = u8::from(attr.value.attr_type()); + let ty = attr.value.attr_code(); self.attr_mask[(ty / 64) as usize] |= 1u64 << (ty % 64); self.inner.push(attr); } @@ -378,7 +379,7 @@ impl Iterator for MetaCommunitiesIter<'_> { fn compute_mask(inner: &[Attribute]) -> [u64; 4] { let mut attr_mask = [0; 4]; for attr in inner { - let ty = u8::from(attr.value.attr_type()); + let ty = attr.value.attr_code(); attr_mask[(ty / 64) as usize] |= 1u64 << (ty % 64); } attr_mask @@ -522,7 +523,7 @@ impl From for Attribute { pub struct AigpTlv { pub tlv_type: u8, pub length: u16, - pub value: Vec, + pub value: Bytes, } /// AIGP (Accumulated IGP Metric) Attribute - RFC 7311 @@ -608,6 +609,7 @@ pub enum AttributeValue { /// BGP Tunnel Encapsulation attribute - RFC 9012 TunnelEncapsulation(crate::models::bgp::tunnel_encap::TunnelEncapAttribute), Development(Vec), + Raw(AttrRaw), Deprecated(AttrRaw), Unknown(AttrRaw), /// AIGP (Accumulated IGP Metric) attribute - RFC 7311 @@ -645,7 +647,7 @@ pub enum AttributeCategory { } impl AttributeValue { - pub const fn attr_type(&self) -> AttrType { + pub fn attr_type(&self) -> AttrType { match self { AttributeValue::Origin(_) => AttrType::ORIGIN, AttributeValue::AsPath { is_as4: false, .. } => AttrType::AS_PATH, @@ -670,12 +672,23 @@ impl AttributeValue { AttributeValue::LinkState(_) => AttrType::BGP_LS_ATTRIBUTE, AttributeValue::TunnelEncapsulation(_) => AttrType::TUNNEL_ENCAPSULATION, AttributeValue::Development(_) => AttrType::DEVELOPMENT, - AttributeValue::Deprecated(x) | AttributeValue::Unknown(x) => x.attr_type, + AttributeValue::Raw(x) | AttributeValue::Deprecated(x) | AttributeValue::Unknown(x) => { + x.attr_type() + } AttributeValue::Aigp(_) => AttrType::AIGP, AttributeValue::AttrSet(_) => AttrType::ATTR_SET, } } + pub fn attr_code(&self) -> u8 { + match self { + AttributeValue::Raw(x) | AttributeValue::Deprecated(x) | AttributeValue::Unknown(x) => { + x.code + } + _ => self.attr_type().into(), + } + } + pub fn attr_category(&self) -> Option { use AttributeCategory::*; @@ -722,8 +735,14 @@ impl AttributeValue { #[derive(Debug, PartialEq, Clone, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct AttrRaw { - pub attr_type: AttrType, - pub bytes: Vec, + pub code: u8, + pub bytes: Bytes, +} + +impl AttrRaw { + pub fn attr_type(&self) -> AttrType { + AttrType::from(self.code) + } } #[cfg(test)] diff --git a/src/parser/bgp/attributes/README.md b/src/parser/bgp/attributes/README.md index c444079..74f0141 100644 --- a/src/parser/bgp/attributes/README.md +++ b/src/parser/bgp/attributes/README.md @@ -13,31 +13,32 @@ | Aggregate | [RFC4271][rfc4271] | 7,18 | Yes | | Community | [RFC1997][rfc1997] | 8 | Yes | | Originator ID | [RFC4456][rfc4456] | 9 | Yes | -| Cluster List | [RFC4456][rfc4456] | 10,13 | Yes | +| Cluster List | [RFC4456][rfc4456] | 10 | Yes | | MP NLRI | [RFC4760][rfc4760] | 14,15 | Yes | | Extended Community | [RFC4360][rfc4360] | 16,25 | Yes | | Large Community | [RFC8092][rfc8092] | 32 | Yes | | Only To Customer | [RFC9234][rfc9234] | 35 | Yes | +| AIGP | [RFC7311][rfc7311] | 26 | Yes | ## Known Limitations | Path Attribute | RFC | Type Code | Status | Notes | |---------------------------------|-------------------------------|-----------|-----------------------------|----------------------------------| -| AIGP (Accumulated IGP Metric) | [RFC7311][rfc7311] | 26 | Type defined, model only | Parser and encoding not yet implemented | -| ATTR_SET | [RFC6368][rfc6368] | 128 | Type defined, model only | Parser and encoding not yet implemented | -| PMSI_TUNNEL | [RFC6514][rfc6514] | 22 | Type defined in enum | Parser not implemented | -| TRAFFIC_ENGINEERING | [RFC5543][rfc5543] | 24 | Type defined in enum | Parser not implemented | +| ATTR_SET | [RFC6368][rfc6368] | 128 | Raw-retained / model only | Structured nested parser not yet implemented | +| PMSI_TUNNEL | [RFC6514][rfc6514] | 22 | Raw-retained | Structured parser not implemented | +| TRAFFIC_ENGINEERING | [RFC5543][rfc5543] | 24 | Raw-retained | Structured parser not implemented | | IPv6_EXT_COMMUNITIES | [RFC5701][rfc5701] | 25 | ✅ Implemented | Listed in main table above | -| PE_DISTINGUISHER_LABELS | [RFC6514][rfc6514] | 27 | Type defined in enum | Parser not implemented | -| BGPSEC_PATH | [RFC8205][rfc8205] | 33 | Type defined in enum | Parser not implemented | -| SFP_ATTRIBUTE | [RFC9015][rfc9015] | 37 | Type defined in enum | Parser not implemented | -| BFD_DISCRIMINATOR | [RFC9026][rfc9026] | 38 | Type defined in enum | Parser not implemented | -| BGP_PREFIX_SID | [RFC8669][rfc8669] | 40 | Type defined in enum | Parser not implemented | +| PE_DISTINGUISHER_LABELS | [RFC6514][rfc6514] | 27 | Raw-retained | Structured parser not implemented | +| BGPSEC_PATH | [RFC8205][rfc8205] | 33 | Raw-retained | Structured parser not implemented | +| SFP_ATTRIBUTE | [RFC9015][rfc9015] | 37 | Raw-retained | Structured parser not implemented | +| BFD_DISCRIMINATOR | [RFC9026][rfc9026] | 38 | Raw-retained | Structured parser not implemented | +| BGP_PREFIX_SID | [RFC8669][rfc8669] | 40 | Raw-retained | Structured parser not implemented | +| BIER | [RFC9793][rfc9793] | 41 | Raw-retained | Structured parser not implemented | **Legend:** -- **Type defined**: Attribute type code is defined in `AttrType` enum (models) -- **Model only**: Data structures exist but parser/encoder not implemented -- **Type defined in enum**: Only the type code exists, no parser or model +- **Raw-retained**: Attribute value bytes are preserved as `AttributeValue::Raw(AttrRaw)` and can be re-encoded, but no structured parser exists yet. +- **Model only**: Data structures exist but structured parser/encoder is incomplete. +- Deprecated/historic code points are intentionally handled with `AttributeValue::Deprecated(AttrRaw)` helpers instead of active `AttrType` variants. Code point status should be checked against the IANA BGP Path Attributes registry. [rfc1997]: https://datatracker.ietf.org/doc/html/rfc1997 [rfc4271]: https://datatracker.ietf.org/doc/html/rfc4271#section-4.3 @@ -55,4 +56,5 @@ [rfc9015]: https://datatracker.ietf.org/doc/html/rfc9015 [rfc9026]: https://datatracker.ietf.org/doc/html/rfc9026 [rfc9234]: https://datatracker.ietf.org/doc/html/rfc9234 +[rfc9793]: https://datatracker.ietf.org/doc/html/rfc9793 [iana-bgp]: https://www.iana.org/assignments/bgp-parameters/bgp-parameters.xhtml \ No newline at end of file diff --git a/src/parser/bgp/attributes/attr_26_aigp.rs b/src/parser/bgp/attributes/attr_26_aigp.rs new file mode 100644 index 0000000..0fd26cd --- /dev/null +++ b/src/parser/bgp/attributes/attr_26_aigp.rs @@ -0,0 +1,99 @@ +use crate::models::*; +use crate::parser::ReadUtils; +use crate::ParserError; +use bytes::{Buf, BufMut, Bytes, BytesMut}; + +/// Parse AIGP attribute TLVs (RFC 7311). +pub fn parse_aigp(mut input: Bytes) -> Result { + let mut tlvs = Vec::new(); + + while input.remaining() > 0 { + if input.remaining() < 3 { + return Err(ParserError::TruncatedMsg( + "truncated AIGP TLV header".to_string(), + )); + } + + let tlv_type = input.read_u8()?; + let length = input.read_u16()?; + if length < 3 { + return Err(ParserError::ParseError(format!( + "invalid AIGP TLV length {length} for type {tlv_type}" + ))); + } + + let value_len = (length - 3) as usize; + if input.remaining() < value_len { + return Err(ParserError::TruncatedMsg(format!( + "truncated AIGP TLV value for type {tlv_type}: need {value_len}, have {}", + input.remaining() + ))); + } + + let value = input.split_to(value_len); + tlvs.push(AigpTlv { + tlv_type, + length, + value, + }); + } + + Ok(AttributeValue::Aigp(Aigp { tlvs })) +} + +pub fn encode_aigp(aigp: &Aigp) -> Bytes { + let mut buf = BytesMut::new(); + for tlv in &aigp.tlvs { + let length = if tlv.length as usize == tlv.value.len() + 3 { + tlv.length + } else { + (tlv.value.len() + 3) as u16 + }; + buf.put_u8(tlv.tlv_type); + buf.put_u16(length); + buf.extend_from_slice(&tlv.value); + } + buf.freeze() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_aigp_metric_tlv() { + let input = Bytes::from_static(&[ + 0x01, 0x00, 0x0b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a, + ]); + let value = parse_aigp(input).unwrap(); + match value { + AttributeValue::Aigp(aigp) => { + assert_eq!(aigp.tlvs.len(), 1); + assert_eq!(aigp.tlvs[0].tlv_type, 1); + assert_eq!(aigp.tlvs[0].length, 11); + assert_eq!(aigp.accumulated_metric(), Some(42)); + } + value => panic!("expected AIGP, got {value:?}"), + } + } + + #[test] + fn test_parse_aigp_unknown_tlv_and_round_trip() { + let input = Bytes::from_static(&[0x7f, 0x00, 0x05, 0xaa, 0xbb]); + let value = parse_aigp(input.clone()).unwrap(); + match value { + AttributeValue::Aigp(aigp) => { + assert_eq!(aigp.tlvs.len(), 1); + assert_eq!(aigp.tlvs[0].tlv_type, 0x7f); + assert_eq!(aigp.tlvs[0].value, Bytes::from_static(&[0xaa, 0xbb])); + assert_eq!(encode_aigp(&aigp), input); + } + value => panic!("expected AIGP, got {value:?}"), + } + } + + #[test] + fn test_parse_aigp_rejects_short_tlv() { + assert!(parse_aigp(Bytes::from_static(&[0x01, 0x00])).is_err()); + } +} diff --git a/src/parser/bgp/attributes/mod.rs b/src/parser/bgp/attributes/mod.rs index 8bf731a..cb153fc 100644 --- a/src/parser/bgp/attributes/mod.rs +++ b/src/parser/bgp/attributes/mod.rs @@ -10,6 +10,7 @@ mod attr_10_13_cluster; mod attr_14_15_nlri; mod attr_16_25_extended_communities; mod attr_23_tunnel_encap; +mod attr_26_aigp; mod attr_29_linkstate; mod attr_32_large_communities; mod attr_35_otc; @@ -44,6 +45,7 @@ use crate::parser::bgp::attributes::attr_16_25_extended_communities::{ use crate::parser::bgp::attributes::attr_23_tunnel_encap::{ encode_tunnel_encapsulation_attribute, parse_tunnel_encapsulation_attribute, }; +use crate::parser::bgp::attributes::attr_26_aigp::{encode_aigp, parse_aigp}; use crate::parser::bgp::attributes::attr_29_linkstate::{ encode_link_state_attribute, parse_link_state_attribute, }; @@ -122,6 +124,22 @@ fn validate_attribute_flags( } } +fn is_raw_retained_attr(attr_type: AttrType) -> bool { + matches!( + attr_type, + AttrType::RESERVED + | AttrType::PMSI_TUNNEL + | AttrType::TRAFFIC_ENGINEERING + | AttrType::PE_DISTINGUISHER_LABELS + | AttrType::BGPSEC_PATH + | AttrType::SFP_ATTRIBUTE + | AttrType::BFD_DISCRIMINATOR + | AttrType::BGP_PREFIX_SID + | AttrType::BIER + | AttrType::ATTR_SET + ) +} + /// Check if an attribute type is well-known mandatory fn is_well_known_mandatory(attr_type: AttrType) -> bool { matches!( @@ -322,34 +340,9 @@ pub fn parse_attributes( &attr_type, attr_length ); - let parsed_attr_type = AttrType::from(attr_type); - - let partial = validation.observe_header(attr_type, parsed_attr_type, flag, attr_length); + let attr_type = AttrType::from(attr_type); - let attr_type = match parsed_attr_type { - attr_type @ AttrType::Unknown(unknown_type) => { - // skip pass the remaining bytes of this attribute - let bytes = data.read_n_bytes(attr_length)?; - let attr_value = match get_deprecated_attr_type(unknown_type) { - Some(t) => { - debug!("deprecated attribute type: {} - {}", unknown_type, t); - AttributeValue::Deprecated(AttrRaw { attr_type, bytes }) - } - None => { - debug!("unknown attribute type: {}", unknown_type); - AttributeValue::Unknown(AttrRaw { attr_type, bytes }) - } - }; - - assert_eq!(attr_type, attr_value.attr_type()); - attributes.push(Attribute { - value: attr_value, - flag, - }); - continue; - } - t => t, - }; + let partial = validation.observe_header(attr_type.into(), attr_type, flag, attr_length); let bytes_left = data.remaining(); @@ -365,6 +358,44 @@ pub fn parse_attributes( // we know data has enough bytes to read, so we can split the bytes into a new Bytes object data.has_n_remaining(attr_length)?; let mut attr_data = data.split_to(attr_length); + let raw_bytes = attr_data.clone(); + let raw_code = u8::from(attr_type); + + if let Some(t) = get_deprecated_attr_type(raw_code) { + debug!("deprecated attribute type: {} - {}", raw_code, t); + attributes.push(Attribute { + value: AttributeValue::Deprecated(AttrRaw { + code: raw_code, + bytes: raw_bytes, + }), + flag, + }); + continue; + } + + if matches!(attr_type, AttrType::Unknown(_)) { + debug!("unknown attribute type: {}", raw_code); + attributes.push(Attribute { + value: AttributeValue::Unknown(AttrRaw { + code: raw_code, + bytes: raw_bytes, + }), + flag, + }); + continue; + } + + if is_raw_retained_attr(attr_type) { + debug!("raw-retained attribute type: {}", raw_code); + attributes.push(Attribute { + value: AttributeValue::Raw(AttrRaw { + code: raw_code, + bytes: raw_bytes, + }), + flag, + }); + continue; + } let attr = match attr_type { AttrType::ORIGIN => parse_origin(attr_data), @@ -420,6 +451,7 @@ pub fn parse_attributes( Ok(AttributeValue::Development(value)) } AttrType::ONLY_TO_CUSTOMER => parse_only_to_customer(attr_data), + AttrType::AIGP => parse_aigp(attr_data), AttrType::TUNNEL_ENCAPSULATION => parse_tunnel_encapsulation_attribute(attr_data), AttrType::BGP_LS_ATTRIBUTE => parse_link_state_attribute(attr_data), _ => Err(ParserError::Unsupported(format!( @@ -434,6 +466,13 @@ pub fn parse_attributes( } Err(e) => { validation.observe_parse_error(attr_type, partial, &e); + attributes.push(Attribute { + value: AttributeValue::Raw(AttrRaw { + code: raw_code, + bytes: raw_bytes, + }), + flag, + }); continue; } }; @@ -452,7 +491,7 @@ impl Attribute { let mut bytes = BytesMut::new(); let flag = self.flag.bits(); - let type_code = self.value.attr_type().into(); + let type_code = self.value.attr_code(); bytes.put_u8(flag); bytes.put_u8(type_code); @@ -506,12 +545,10 @@ impl Attribute { AttributeValue::LinkState(v) => encode_link_state_attribute(v), AttributeValue::TunnelEncapsulation(v) => encode_tunnel_encapsulation_attribute(v), AttributeValue::Development(v) => Bytes::from(v.to_owned()), - AttributeValue::Deprecated(v) => Bytes::from(v.bytes.to_owned()), - AttributeValue::Unknown(v) => Bytes::from(v.bytes.to_owned()), - AttributeValue::Aigp(_v) => { - // AIGP encoding not yet implemented - return empty bytes - Bytes::new() - } + AttributeValue::Raw(v) => v.bytes.clone(), + AttributeValue::Deprecated(v) => v.bytes.clone(), + AttributeValue::Unknown(v) => v.bytes.clone(), + AttributeValue::Aigp(v) => encode_aigp(v), AttributeValue::AttrSet(_v) => { // ATTR_SET encoding not yet implemented - return empty bytes Bytes::new() @@ -746,12 +783,82 @@ mod tests { let attributes = parse_attributes(data, &asn_len, add_path, afi, safi, prefixes).unwrap(); - // There is a validation warning about the reserved attribute - assert!(attributes.validation_warnings.iter().any(|vw| { + assert!(attributes.inner.iter().any(|attr| { + matches!( + &attr.value, + AttributeValue::Raw(raw) + if raw.code == u8::from(AttrType::RESERVED) + && raw.bytes == Bytes::from_static(&[0x01]) + ) + })); + assert!(!attributes.validation_warnings.iter().any(|vw| { matches!(vw, BgpValidationWarning::OptionalAttributeError { attr_type, reason:_ } if *attr_type == AttrType::RESERVED) })); } + #[test] + fn test_raw_retention_for_known_unsupported_attribute() { + let data = Bytes::from(vec![0x80, 0x16, 0x03, 0xaa, 0xbb, 0xcc]); // PMSI_TUNNEL + let attributes = + parse_attributes(data, &AsnLength::Bits16, false, None, None, None).unwrap(); + + assert_eq!(attributes.inner.len(), 1); + match &attributes.inner[0].value { + AttributeValue::Raw(raw) => { + assert_eq!(raw.code, 22); + assert_eq!(raw.attr_type(), AttrType::PMSI_TUNNEL); + assert_eq!(raw.bytes, Bytes::from_static(&[0xaa, 0xbb, 0xcc])); + } + value => panic!("expected Raw, got {value:?}"), + } + assert_eq!( + attributes.encode(AsnLength::Bits16), + Bytes::from_static(&[0x80, 0x16, 0x03, 0xaa, 0xbb, 0xcc]) + ); + } + + #[test] + fn test_deprecated_code_13_retained_as_deprecated() { + let data = Bytes::from(vec![0x80, 0x0d, 0x04, 0x01, 0x02, 0x03, 0x04]); + let attributes = + parse_attributes(data, &AsnLength::Bits16, false, None, None, None).unwrap(); + + assert_eq!(attributes.inner.len(), 1); + match &attributes.inner[0].value { + AttributeValue::Deprecated(raw) => { + assert_eq!(raw.code, 13); + assert_eq!(raw.attr_type(), AttrType::Unknown(13)); + assert_eq!(raw.bytes, Bytes::from_static(&[0x01, 0x02, 0x03, 0x04])); + } + value => panic!("expected Deprecated, got {value:?}"), + } + assert_eq!( + attributes.encode(AsnLength::Bits16), + Bytes::from_static(&[0x80, 0x0d, 0x04, 0x01, 0x02, 0x03, 0x04]) + ); + } + + #[test] + fn test_malformed_typed_attribute_falls_back_to_raw() { + let data = Bytes::from(vec![0x40, 0x03, 0x03, 0x01, 0x02, 0x03]); // NEXT_HOP length must be 4 + let attributes = + parse_attributes(data, &AsnLength::Bits16, false, None, None, None).unwrap(); + + assert!(attributes.has_validation_warnings()); + match &attributes.inner[0].value { + AttributeValue::Raw(raw) => { + assert_eq!(raw.code, 3); + assert_eq!(raw.attr_type(), AttrType::NEXT_HOP); + assert_eq!(raw.bytes, Bytes::from_static(&[0x01, 0x02, 0x03])); + } + value => panic!("expected Raw fallback, got {value:?}"), + } + assert_eq!( + attributes.encode(AsnLength::Bits16), + Bytes::from_static(&[0x40, 0x03, 0x03, 0x01, 0x02, 0x03]) + ); + } + #[test] fn test_rfc7606_attribute_length_error() { // Create an ORIGIN attribute with wrong length (should be 1 byte, not 2) diff --git a/src/parser/mrt/mrt_elem.rs b/src/parser/mrt/mrt_elem.rs index be405cf..e298aa7 100644 --- a/src/parser/mrt/mrt_elem.rs +++ b/src/parser/mrt/mrt_elem.rs @@ -8,6 +8,7 @@ use crate::models::*; use crate::parser::bgp::messages::parse_bgp_update_message; use crate::ParserError; use crate::ParserError::ParseError; +use bytes::Bytes; use itertools::Itertools; use log::{error, warn}; use std::collections::HashMap; @@ -142,6 +143,7 @@ fn get_relevant_attributes( AttributeValue::Deprecated(t) => { deprecated.push(t); } + AttributeValue::Raw(_) => {} AttributeValue::OriginatorId(_) | AttributeValue::Clusters(_) @@ -905,12 +907,12 @@ mod tests { aggr_ip: Some(Ipv4Addr::from_str("10.2.0.0").unwrap()), only_to_customer: Some(Asn::new_32bit(65000)), unknown: Some(vec![AttrRaw { - attr_type: AttrType::RESERVED, - bytes: vec![], + code: AttrType::RESERVED.into(), + bytes: Bytes::new(), }]), deprecated: Some(vec![AttrRaw { - attr_type: AttrType::RESERVED, - bytes: vec![], + code: AttrType::RESERVED.into(), + bytes: Bytes::new(), }]), }; @@ -959,12 +961,12 @@ mod tests { )), AttributeValue::OnlyToCustomer(Asn::new_32bit(65000)), AttributeValue::Unknown(AttrRaw { - attr_type: AttrType::RESERVED, - bytes: vec![], + code: AttrType::RESERVED.into(), + bytes: Bytes::new(), }), AttributeValue::Deprecated(AttrRaw { - attr_type: AttrType::RESERVED, - bytes: vec![], + code: AttrType::RESERVED.into(), + bytes: Bytes::new(), }), ] .into_iter() From ceff8de307e97d8db9189fbafbea995c069e66a4 Mon Sep 17 00:00:00 2001 From: Mingwei Zhang Date: Mon, 15 Jun 2026 20:13:39 -0700 Subject: [PATCH 2/7] feat: add SFP/BFD/Prefix-SID/BIER parsers, examples, changelog, Cargo.toml bytes fix --- CHANGELOG.md | 42 ++++++ Cargo.toml | 4 +- README.md | 12 ++ examples/raw_attributes.rs | 119 ++++++++++++++++ examples/scan_path_attributes.rs | 131 ++++++++++++++++++ src/lib.rs | 12 ++ src/models/bgp/attributes/mod.rs | 70 ++++++++++ src/parser/bgp/attributes/README.md | 8 +- src/parser/bgp/attributes/attr_37_sfp.rs | 93 +++++++++++++ .../attributes/attr_38_bfd_discriminator.rs | 103 ++++++++++++++ .../bgp/attributes/attr_40_bgp_prefix_sid.rs | 95 +++++++++++++ src/parser/bgp/attributes/attr_41_bier.rs | 94 +++++++++++++ src/parser/bgp/attributes/mod.rs | 116 +++++++++++++++- src/parser/mrt/mrt_elem.rs | 4 + 14 files changed, 893 insertions(+), 10 deletions(-) create mode 100644 examples/raw_attributes.rs create mode 100644 examples/scan_path_attributes.rs create mode 100644 src/parser/bgp/attributes/attr_37_sfp.rs create mode 100644 src/parser/bgp/attributes/attr_38_bfd_discriminator.rs create mode 100644 src/parser/bgp/attributes/attr_40_bgp_prefix_sid.rs create mode 100644 src/parser/bgp/attributes/attr_41_bier.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4958490..3bf9926 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,48 @@ All notable changes to this project will be documented in this file. +## Unreleased + +### Breaking changes + +* **`AttrRaw` fields changed**: `attr_type: AttrType` replaced with `code: u8`. Use `.attr_type()` to get the `AttrType` back. `bytes` changed from `Vec` to `Bytes`. +* **`AttrType::CLUSTER_ID` removed**: Code 13 is the deprecated `RCID_PATH / CLUSTER_ID` per IANA registry and is now retained as `AttributeValue::Deprecated`. +* **`AttributeValue` gains new variants**: `Raw(AttrRaw)`, `BfdDiscriminator(...)`, `BgpPrefixSid(...)`, `Bier(...)`, `Sfp(...)`. Downstream exhaustive matches must handle these. +* **`AigpTlv.value` changed**: From `Vec` to `Bytes`. +* **No more silent attribute loss**: Known-but-unsupported attributes that were previously dropped are now retained as `AttributeValue::Raw(AttrRaw)`. If your code expected these to be absent, it may now see them. + +### New features + +* **Raw attribute retention**: Known BGP path attributes without semantic parsing are no longer silently dropped. They are retained as `AttributeValue::Raw(AttrRaw)` with their original wire code and bytes, enabling faithful re-encoding. +* **Deprecated code point handling**: Removed `AttrType::CLUSTER_ID = 13` (deprecated per IANA); code 13 is now retained as `Deprecated(AttrRaw)`. Added `is_deprecated_attr_type()` helper. +* **`AttrType::BIER` added**: Code 41 (RFC 9793) added to the enum. +* **Typed parser failure fallback**: If a typed attribute parser fails, the original bytes are retained as `AttributeValue::Raw` instead of being dropped. A validation warning is still recorded. +* **Serialization**: Added `serde` feature to `bytes` dependency so `AttrRaw` (with `Bytes`) can be serialized. + +### New attribute parsers + +* **RFC 7311 — AIGP (code 26)**: Parses AIGP TLVs, preserves unknown TLVs, supports round-trip encoding. Exposes `accumulated_metric()` helper. +* **RFC 9015 — SFP attribute (code 37)**: Parses SFP TLVs with 1-octet type and 2-octet length. Preserves unknown TLVs. +* **RFC 9026 — BFD Discriminator (code 38)**: Parses mode, discriminator, and optional 1-octet TLVs. Preserves unknown optional TLVs. +* **RFC 8669 — BGP Prefix-SID (code 40)**: Parses Prefix-SID TLVs with 1-octet type and 2-octet length. Preserves unknown TLVs. +* **RFC 9793 — BIER (code 41)**: Parses BIER TLVs with 2-octet type and 2-octet length. Preserves unknown TLVs. + +### Added + +* `AttributeValue::Raw(AttrRaw)` — for known but undecoded attributes. +* `AttrRaw { code: u8, bytes: Bytes }` — stores wire code and raw value bytes. +* `AttributeValue::attr_code()` — returns the raw wire attribute type code as `u8`. +* `AttrRaw::attr_type()` — convenience conversion to `AttrType`. +* Model structs: `BfdDiscriminatorAttribute`, `BgpPrefixSidAttribute`, `BierAttribute`, `SfpAttribute`. +* Raw TLV helper types: `RawTlv8`, `RawTlv8Ext`, `RawTlv16`. +* `examples/raw_attributes.rs` — demonstrates inspecting raw/deprecated/unknown attributes. + +### Fixed + +* Code 13 (`RCID_PATH / CLUSTER_ID`) no longer maps to an active `AttrType` variant; correctly handled as deprecated. +* Known but unsupported attributes (codes 0, 22, 24, 27, 33, 128) are now retained as `Raw` instead of dropped. +* Malformed typed attributes retain their original bytes as `Raw` after recording a validation warning. + ## v0.17.0 - 2026-05-24 ### New features diff --git a/Cargo.toml b/Cargo.toml index 150065c..8c060cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ serde = { version = "1.0", features = ["derive"], optional = true } ####################### # Parser dependencies # ####################### -bytes = { version = "1.7", optional = true, features = ["serde"] } +bytes = { version = "1.7" } zerocopy = { version = "0.8", features = ["derive"], optional = true } hex = { version = "0.4.3", optional = true } # bmp/openbmp parsing oneio = { version = "0.23", default-features = false, features = ["http", "gz", "bz"], optional = true } @@ -67,7 +67,6 @@ default = ["parser", "rustls"] local = ["parser", "oneio"] parser = [ - "bytes", "chrono", "regex", "zerocopy", @@ -96,6 +95,7 @@ wasm = [ ] serde = [ "dep:serde", + "bytes/serde", "ipnet/serde", "serde/rc", ] diff --git a/README.md b/README.md index 1fc95fe..5c07543 100644 --- a/README.md +++ b/README.md @@ -791,6 +791,18 @@ Full support for standard, extended, and large communities: - [RFC 6810](https://datatracker.ietf.org/doc/html/rfc6810): The Resource Public Key Infrastructure (RPKI) to Router Protocol - [RFC 8210](https://datatracker.ietf.org/doc/html/rfc8210): The Resource Public Key Infrastructure (RPKI) to Router Protocol, Version 1 +### BGP Path Attributes + +Typed parsing for these RFC-defined BGP path attributes: + +- [RFC 7311](https://datatracker.ietf.org/doc/html/rfc7311): Accumulated IGP Metric (AIGP) Attribute +- [RFC 9015](https://datatracker.ietf.org/doc/html/rfc9015): BGP SFP Attribute +- [RFC 9026](https://datatracker.ietf.org/doc/html/rfc9026): BFD Discriminator Attribute +- [RFC 8669](https://datatracker.ietf.org/doc/html/rfc8669): BGP Prefix-SID Attribute +- [RFC 9793](https://datatracker.ietf.org/doc/html/rfc9793): BGP Extensions for BIER + +Additional known attribute type codes are raw-retained (`AttributeValue::Raw`) and re-encoded faithfully. Deprecated and unassigned codes are also preserved. + ### Advanced Features **FlowSpec**: diff --git a/examples/raw_attributes.rs b/examples/raw_attributes.rs new file mode 100644 index 0000000..8f4daac --- /dev/null +++ b/examples/raw_attributes.rs @@ -0,0 +1,119 @@ +/// Example: inspecting raw, deprecated, and unknown BGP path attributes. +/// +/// v0.18 introduces `AttributeValue::Raw` for known but undecoded attributes, +/// making it possible to inspect, count, and round-trip attributes even when +/// full semantic parsing is not yet implemented. +/// +/// Run with: +/// ```bash +/// cargo run --example raw_attributes --features cli +/// ``` +use bgpkit_parser::models::*; + +fn describe_value(value: &AttributeValue) { + match value { + AttributeValue::Raw(raw) => { + println!( + " Raw code {} ({} bytes, {:?})", + raw.code, + raw.bytes.len(), + raw.attr_type() + ); + } + AttributeValue::Deprecated(raw) => { + println!(" Deprecated code {} ({} bytes)", raw.code, raw.bytes.len()); + } + AttributeValue::Unknown(raw) => { + println!(" Unknown code {} ({} bytes)", raw.code, raw.bytes.len()); + } + AttributeValue::Aigp(aigp) => { + println!( + " AIGP ({} TLVs, metric={:?})", + aigp.tlvs.len(), + aigp.accumulated_metric() + ); + } + AttributeValue::BfdDiscriminator(bfd) => { + println!( + " BFD Discriminator (mode={}, discriminator={})", + bfd.mode, bfd.discriminator + ); + } + AttributeValue::BgpPrefixSid(psid) => { + println!(" BGP Prefix-SID ({} TLVs)", psid.tlvs.len()); + } + AttributeValue::Bier(bier) => println!(" BIER ({} TLVs)", bier.tlvs.len()), + AttributeValue::Sfp(sfp) => println!(" SFP ({} TLVs)", sfp.tlvs.len()), + other => println!(" {:?}", other.attr_type()), + } +} + +fn main() { + println!("=== Demonstrating raw/deprecated/unknown attribute handling ===\n"); + + // Build an Attributes collection from AttributeValue iterators. + let attributes = Attributes::from_iter(vec![ + // Known but unsupported: PMSI_TUNNEL (code 22) + AttributeValue::Raw(AttrRaw { + code: 22, + bytes: bytes::Bytes::from_static(&[0x01, 0x02, 0x03]), + }), + // Deprecated: code 13 (RCID_PATH / CLUSTER_ID) + AttributeValue::Deprecated(AttrRaw { + code: 13, + bytes: bytes::Bytes::from_static(&[0xaa, 0xbb, 0xcc, 0xdd]), + }), + // Unknown / unassigned: code 127 + AttributeValue::Unknown(AttrRaw { + code: 127, + bytes: bytes::Bytes::from_static(&[0xde, 0xad]), + }), + // Typed: a structured AIGP attribute + AttributeValue::Aigp(Aigp { + tlvs: vec![AigpTlv { + tlv_type: 1, + length: 11, + value: bytes::Bytes::from_static(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2a]), + }], + }), + ]); + + println!("All attributes:"); + for value in &attributes { + describe_value(value); + } + + // Check warnings + println!( + "\nValidation warnings: {}", + attributes.validation_warnings().len() + ); + + // Encode and decode round-trip + let encoded = attributes.encode(AsnLength::Bits32); + println!("\nEncoded size: {} bytes", encoded.len()); + + // Show raw access to the undecoded bytes + println!("\nRaw bytes access:"); + for value in &attributes { + match value { + AttributeValue::Raw(raw) => { + println!( + " Raw code {}: {} bytes (first bytes: {:02x?})", + raw.code, + raw.bytes.len(), + &raw.bytes[..raw.bytes.len().min(8)] + ); + } + AttributeValue::Deprecated(raw) => { + println!(" Deprecated code {}: {} bytes", raw.code, raw.bytes.len()); + } + AttributeValue::Unknown(raw) => { + println!(" Unknown code {}: {} bytes", raw.code, raw.bytes.len()); + } + _ => {} + } + } + + println!("\n=== Done ==="); +} diff --git a/examples/scan_path_attributes.rs b/examples/scan_path_attributes.rs new file mode 100644 index 0000000..f0476f9 --- /dev/null +++ b/examples/scan_path_attributes.rs @@ -0,0 +1,131 @@ +/// Scan MRT archives for interesting BGP path attributes. +/// +/// Iterates over recent RouteViews and RIPE RIS update files, scanning for +/// raw-retained, deprecated, and recently implemented attributes. +/// +/// Usage: +/// ```bash +/// cargo run --example scan_path_attributes --features cli +/// ``` +use bgpkit_parser::BgpkitParser; +use std::collections::HashMap; + +/// Scan a single MRT file, sampling up to `max_elems` elements. +fn scan_file(url: &str, max_elems: u64) -> Result<(HashMap, u64), String> { + let parser = BgpkitParser::new(url).map_err(|e| format!("parser error: {e}"))?; + let mut counts: HashMap = HashMap::new(); + let mut processed = 0u64; + + for elem in parser.into_elem_iter() { + // Check unknown attributes (includes unassigned + raw-retained known codes) + if let Some(ref unknown) = elem.unknown { + for raw in unknown { + let key = format!("unknown(code={}, type={:?})", raw.code, raw.attr_type()); + *counts.entry(key).or_default() += 1; + } + } + + // Check deprecated attributes + if let Some(ref deprecated) = elem.deprecated { + for raw in deprecated { + let key = format!("deprecated(code={})", raw.code); + *counts.entry(key).or_default() += 1; + } + } + + processed += 1; + if processed >= max_elems { + break; + } + } + Ok((counts, processed)) +} + +fn main() { + println!("=== BGP Path Attribute Scanner ==="); + println!("Looks for unsupported/raw/deprecated attributes in public archive files.\n"); + + // RouteViews collectors + let collectors = [ + "route-views4", + "route-views6", + "route-views2", + "route-views3", + "route-views.linx", + "route-views.eqix", + "route-views.amsix", + ]; + + let mut urls: Vec = Vec::new(); + + // RouteViews archive URLs — June 2026, sampling a few hours + for collector in &collectors { + for day in [1, 2] { + for hour in [0, 12] { + let url = format!( + "http://archive.routeviews.org/{}/bgpdata/2026.06/UPDATES/updates.202606{:02}.{:04}00.bz2", + collector, day, hour + ); + urls.push(url); + } + } + } + + // RIPE RIS update files + for rrc in 0..=6 { + for day in [1, 2] { + let url = format!( + "https://data.ris.ripe.net/rrc{:02}/2026.06/updates.202606{:02}.0000.gz", + rrc, day + ); + urls.push(url); + } + } + + println!( + "Candidates: {} files (sampling 50K elements each)", + urls.len() + ); + println!(); + + let mut found_files: Vec<(String, HashMap)> = Vec::new(); + + for url in &urls { + print!(" {:65}", url); + std::io::Write::flush(&mut std::io::stdout()).ok(); + + match scan_file(url, 50_000) { + Ok((counts, processed)) => { + if counts.is_empty() { + println!(" ({}K elems, nothing)", processed / 1000); + } else { + println!(" ({}K elems, {} hits)", processed / 1000, counts.len()); + found_files.push((url.clone(), counts)); + } + } + Err(e) => { + eprintln!(" error: {}", e); + } + } + } + + println!(); + if found_files.is_empty() { + println!("No interesting attributes found in the scanned window."); + println!( + "This is expected: deprecated and specialized attributes are rare in public data." + ); + } else { + println!("=== Files with interesting attributes ==="); + println!(); + for (url, counts) in &found_files { + println!("File: {}", url); + let mut entries: Vec<_> = counts.iter().collect(); + entries.sort_by_key(|(k, _)| String::clone(k)); + for (key, count) in &entries { + println!(" {}: {}", key, count); + } + println!(); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 3d526bd..e7dcb01 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -787,6 +787,18 @@ Full support for standard, extended, and large communities: - [RFC 6810](https://datatracker.ietf.org/doc/html/rfc6810): The Resource Public Key Infrastructure (RPKI) to Router Protocol - [RFC 8210](https://datatracker.ietf.org/doc/html/rfc8210): The Resource Public Key Infrastructure (RPKI) to Router Protocol, Version 1 +## BGP Path Attributes + +Typed parsing for these RFC-defined BGP path attributes: + +- [RFC 7311](https://datatracker.ietf.org/doc/html/rfc7311): Accumulated IGP Metric (AIGP) Attribute +- [RFC 9015](https://datatracker.ietf.org/doc/html/rfc9015): BGP SFP Attribute +- [RFC 9026](https://datatracker.ietf.org/doc/html/rfc9026): BFD Discriminator Attribute +- [RFC 8669](https://datatracker.ietf.org/doc/html/rfc8669): BGP Prefix-SID Attribute +- [RFC 9793](https://datatracker.ietf.org/doc/html/rfc9793): BGP Extensions for BIER + +Additional known attribute type codes are raw-retained (`AttributeValue::Raw`) and re-encoded faithfully. Deprecated and unassigned codes are also preserved. + ## Advanced Features **FlowSpec**: diff --git a/src/models/bgp/attributes/mod.rs b/src/models/bgp/attributes/mod.rs index e44859e..a61eabc 100644 --- a/src/models/bgp/attributes/mod.rs +++ b/src/models/bgp/attributes/mod.rs @@ -561,6 +561,60 @@ impl Aigp { } } +/// Raw TLV with 1-octet type and 1-octet value length. +#[derive(Debug, PartialEq, Clone, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct RawTlv8 { + pub tlv_type: u8, + pub value: Bytes, +} + +/// Raw TLV with 1-octet type and 2-octet value length. +#[derive(Debug, PartialEq, Clone, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct RawTlv8Ext { + pub tlv_type: u8, + pub value: Bytes, +} + +/// Raw TLV with 2-octet type and 2-octet value length. +#[derive(Debug, PartialEq, Clone, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct RawTlv16 { + pub tlv_type: u16, + pub value: Bytes, +} + +/// BFD Discriminator Attribute - RFC 9026 +#[derive(Debug, PartialEq, Clone, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct BfdDiscriminatorAttribute { + pub mode: u8, + pub discriminator: u32, + pub tlvs: Vec, +} + +/// BGP Prefix-SID Attribute - RFC 8669 +#[derive(Debug, PartialEq, Clone, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct BgpPrefixSidAttribute { + pub tlvs: Vec, +} + +/// BIER Attribute - RFC 9793 +#[derive(Debug, PartialEq, Clone, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct BierAttribute { + pub tlvs: Vec, +} + +/// SFP Attribute - RFC 9015 +#[derive(Debug, PartialEq, Clone, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct SfpAttribute { + pub tlvs: Vec, +} + /// ATTR_SET Attribute - RFC 6368 /// /// Used in BGP/MPLS IP VPNs to transparently carry customer BGP path attributes @@ -608,6 +662,14 @@ pub enum AttributeValue { LinkState(crate::models::bgp::linkstate::LinkStateAttribute), /// BGP Tunnel Encapsulation attribute - RFC 9012 TunnelEncapsulation(crate::models::bgp::tunnel_encap::TunnelEncapAttribute), + /// BFD Discriminator attribute - RFC 9026 + BfdDiscriminator(BfdDiscriminatorAttribute), + /// BGP Prefix-SID attribute - RFC 8669 + BgpPrefixSid(BgpPrefixSidAttribute), + /// BIER attribute - RFC 9793 + Bier(BierAttribute), + /// SFP attribute - RFC 9015 + Sfp(SfpAttribute), Development(Vec), Raw(AttrRaw), Deprecated(AttrRaw), @@ -671,6 +733,10 @@ impl AttributeValue { AttributeValue::MpUnreachNlri(_) => AttrType::MP_UNREACHABLE_NLRI, AttributeValue::LinkState(_) => AttrType::BGP_LS_ATTRIBUTE, AttributeValue::TunnelEncapsulation(_) => AttrType::TUNNEL_ENCAPSULATION, + AttributeValue::BfdDiscriminator(_) => AttrType::BFD_DISCRIMINATOR, + AttributeValue::BgpPrefixSid(_) => AttrType::BGP_PREFIX_SID, + AttributeValue::Bier(_) => AttrType::BIER, + AttributeValue::Sfp(_) => AttrType::SFP_ATTRIBUTE, AttributeValue::Development(_) => AttrType::DEVELOPMENT, AttributeValue::Raw(x) | AttributeValue::Deprecated(x) | AttributeValue::Unknown(x) => { x.attr_type() @@ -712,6 +778,10 @@ impl AttributeValue { AttributeValue::MpUnreachNlri(_) => Some(OptionalNonTransitive), AttributeValue::LinkState(_) => Some(OptionalNonTransitive), AttributeValue::Aigp(_) => Some(OptionalNonTransitive), + AttributeValue::BfdDiscriminator(_) => Some(OptionalTransitive), + AttributeValue::BgpPrefixSid(_) => Some(OptionalTransitive), + AttributeValue::Bier(_) => Some(OptionalTransitive), + AttributeValue::Sfp(_) => Some(OptionalTransitive), AttributeValue::AttrSet(_) => Some(OptionalTransitive), _ => None, } diff --git a/src/parser/bgp/attributes/README.md b/src/parser/bgp/attributes/README.md index 74f0141..6cbafc8 100644 --- a/src/parser/bgp/attributes/README.md +++ b/src/parser/bgp/attributes/README.md @@ -19,6 +19,10 @@ | Large Community | [RFC8092][rfc8092] | 32 | Yes | | Only To Customer | [RFC9234][rfc9234] | 35 | Yes | | AIGP | [RFC7311][rfc7311] | 26 | Yes | +| BFD Discriminator | [RFC9026][rfc9026] | 38 | Yes | +| BGP Prefix-SID | [RFC8669][rfc8669] | 40 | Yes | +| SFP Attribute | [RFC9015][rfc9015] | 37 | Yes | +| BIER | [RFC9793][rfc9793] | 41 | Yes | ## Known Limitations @@ -30,10 +34,6 @@ | IPv6_EXT_COMMUNITIES | [RFC5701][rfc5701] | 25 | ✅ Implemented | Listed in main table above | | PE_DISTINGUISHER_LABELS | [RFC6514][rfc6514] | 27 | Raw-retained | Structured parser not implemented | | BGPSEC_PATH | [RFC8205][rfc8205] | 33 | Raw-retained | Structured parser not implemented | -| SFP_ATTRIBUTE | [RFC9015][rfc9015] | 37 | Raw-retained | Structured parser not implemented | -| BFD_DISCRIMINATOR | [RFC9026][rfc9026] | 38 | Raw-retained | Structured parser not implemented | -| BGP_PREFIX_SID | [RFC8669][rfc8669] | 40 | Raw-retained | Structured parser not implemented | -| BIER | [RFC9793][rfc9793] | 41 | Raw-retained | Structured parser not implemented | **Legend:** - **Raw-retained**: Attribute value bytes are preserved as `AttributeValue::Raw(AttrRaw)` and can be re-encoded, but no structured parser exists yet. diff --git a/src/parser/bgp/attributes/attr_37_sfp.rs b/src/parser/bgp/attributes/attr_37_sfp.rs new file mode 100644 index 0000000..e792b0d --- /dev/null +++ b/src/parser/bgp/attributes/attr_37_sfp.rs @@ -0,0 +1,93 @@ +use crate::models::*; +use crate::parser::ReadUtils; +use crate::ParserError; +use bytes::{Buf, BufMut, Bytes, BytesMut}; + +pub fn parse_sfp(mut input: Bytes) -> Result { + let mut tlvs = Vec::new(); + + while input.remaining() > 0 { + if input.remaining() < 3 { + return Err(ParserError::TruncatedMsg( + "truncated SFP TLV header".to_string(), + )); + } + let tlv_type = input.read_u8()?; + let length = input.read_u16()? as usize; + if input.remaining() < length { + return Err(ParserError::TruncatedMsg(format!( + "truncated SFP TLV value for type {tlv_type}: need {length}, have {}", + input.remaining() + ))); + } + let value = input.split_to(length); + tlvs.push(RawTlv8Ext { tlv_type, value }); + } + + Ok(AttributeValue::Sfp(SfpAttribute { tlvs })) +} + +pub fn encode_sfp(attr: &SfpAttribute) -> Bytes { + let mut buf = BytesMut::new(); + for tlv in &attr.tlvs { + buf.put_u8(tlv.tlv_type); + buf.put_u16(tlv.value.len() as u16); + buf.extend_from_slice(&tlv.value); + } + buf.freeze() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_sfp_association_tlv_round_trip() { + let input = Bytes::from_static(&[0x01, 0x00, 0x04, 0xaa, 0xbb, 0xcc, 0xdd]); + let value = parse_sfp(input.clone()).unwrap(); + match value { + AttributeValue::Sfp(attr) => { + assert_eq!(attr.tlvs.len(), 1); + assert_eq!(attr.tlvs[0].tlv_type, 1); + assert_eq!( + attr.tlvs[0].value, + Bytes::from_static(&[0xaa, 0xbb, 0xcc, 0xdd]) + ); + assert_eq!(encode_sfp(&attr), input); + } + value => panic!("expected SFP, got {value:?}"), + } + } + + #[test] + fn test_parse_sfp_unknown_tlv_round_trip() { + let input = Bytes::from_static(&[0x7f, 0x00, 0x02, 0xde, 0xad]); + let value = parse_sfp(input.clone()).unwrap(); + match value { + AttributeValue::Sfp(attr) => { + assert_eq!(attr.tlvs[0].tlv_type, 0x7f); + assert_eq!(attr.tlvs[0].value, Bytes::from_static(&[0xde, 0xad])); + assert_eq!(encode_sfp(&attr), input); + } + value => panic!("expected SFP, got {value:?}"), + } + } + + #[test] + fn test_parse_sfp_empty_attribute_round_trip() { + let value = parse_sfp(Bytes::new()).unwrap(); + match value { + AttributeValue::Sfp(attr) => { + assert!(attr.tlvs.is_empty()); + assert_eq!(encode_sfp(&attr), Bytes::new()); + } + value => panic!("expected SFP, got {value:?}"), + } + } + + #[test] + fn test_parse_sfp_rejects_truncated_tlv() { + assert!(parse_sfp(Bytes::from_static(&[0x01, 0x00])).is_err()); + assert!(parse_sfp(Bytes::from_static(&[0x01, 0x00, 0x02, 0xaa])).is_err()); + } +} diff --git a/src/parser/bgp/attributes/attr_38_bfd_discriminator.rs b/src/parser/bgp/attributes/attr_38_bfd_discriminator.rs new file mode 100644 index 0000000..89d81b2 --- /dev/null +++ b/src/parser/bgp/attributes/attr_38_bfd_discriminator.rs @@ -0,0 +1,103 @@ +use crate::models::*; +use crate::parser::ReadUtils; +use crate::ParserError; +use bytes::{Buf, BufMut, Bytes, BytesMut}; + +pub fn parse_bfd_discriminator(mut input: Bytes) -> Result { + if input.remaining() < 5 { + return Err(ParserError::TruncatedMsg( + "truncated BFD Discriminator attribute".to_string(), + )); + } + + let mode = input.read_u8()?; + let discriminator = input.read_u32()?; + let mut tlvs = Vec::new(); + + while input.remaining() > 0 { + if input.remaining() < 2 { + return Err(ParserError::TruncatedMsg( + "truncated BFD Discriminator optional TLV header".to_string(), + )); + } + let tlv_type = input.read_u8()?; + let length = input.read_u8()? as usize; + if input.remaining() < length { + return Err(ParserError::TruncatedMsg(format!( + "truncated BFD Discriminator optional TLV value for type {tlv_type}: need {length}, have {}", + input.remaining() + ))); + } + let value = input.split_to(length); + tlvs.push(RawTlv8 { tlv_type, value }); + } + + Ok(AttributeValue::BfdDiscriminator( + BfdDiscriminatorAttribute { + mode, + discriminator, + tlvs, + }, + )) +} + +pub fn encode_bfd_discriminator(attr: &BfdDiscriminatorAttribute) -> Bytes { + let mut buf = BytesMut::new(); + buf.put_u8(attr.mode); + buf.put_u32(attr.discriminator); + for tlv in &attr.tlvs { + buf.put_u8(tlv.tlv_type); + buf.put_u8(tlv.value.len() as u8); + buf.extend_from_slice(&tlv.value); + } + buf.freeze() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_bfd_discriminator_with_source_ipv4_tlv() { + let input = Bytes::from_static(&[ + 0x01, 0x01, 0x02, 0x03, 0x04, // mode + discriminator + 0x01, 0x04, 192, 0, 2, 1, // source IP TLV + ]); + let value = parse_bfd_discriminator(input.clone()).unwrap(); + match value { + AttributeValue::BfdDiscriminator(attr) => { + assert_eq!(attr.mode, 1); + assert_eq!(attr.discriminator, 0x01020304); + assert_eq!(attr.tlvs.len(), 1); + assert_eq!(attr.tlvs[0].tlv_type, 1); + assert_eq!(attr.tlvs[0].value, Bytes::from_static(&[192, 0, 2, 1])); + assert_eq!(encode_bfd_discriminator(&attr), input); + } + value => panic!("expected BFD Discriminator, got {value:?}"), + } + } + + #[test] + fn test_parse_bfd_discriminator_without_optional_tlvs_round_trip() { + let input = Bytes::from_static(&[0x01, 0x01, 0x02, 0x03, 0x04]); + let value = parse_bfd_discriminator(input.clone()).unwrap(); + match value { + AttributeValue::BfdDiscriminator(attr) => { + assert_eq!(attr.mode, 1); + assert_eq!(attr.discriminator, 0x01020304); + assert!(attr.tlvs.is_empty()); + assert_eq!(encode_bfd_discriminator(&attr), input); + } + value => panic!("expected BFD Discriminator, got {value:?}"), + } + } + + #[test] + fn test_parse_bfd_discriminator_rejects_short_value() { + assert!(parse_bfd_discriminator(Bytes::from_static(&[0x01, 0x02])).is_err()); + assert!(parse_bfd_discriminator(Bytes::from_static(&[ + 0x01, 0x01, 0x02, 0x03, 0x04, 0x01, 0x04, 192 + ])) + .is_err()); + } +} diff --git a/src/parser/bgp/attributes/attr_40_bgp_prefix_sid.rs b/src/parser/bgp/attributes/attr_40_bgp_prefix_sid.rs new file mode 100644 index 0000000..b65b6a9 --- /dev/null +++ b/src/parser/bgp/attributes/attr_40_bgp_prefix_sid.rs @@ -0,0 +1,95 @@ +use crate::models::*; +use crate::parser::ReadUtils; +use crate::ParserError; +use bytes::{Buf, BufMut, Bytes, BytesMut}; + +pub fn parse_bgp_prefix_sid(mut input: Bytes) -> Result { + let mut tlvs = Vec::new(); + + while input.remaining() > 0 { + if input.remaining() < 3 { + return Err(ParserError::TruncatedMsg( + "truncated BGP Prefix-SID TLV header".to_string(), + )); + } + let tlv_type = input.read_u8()?; + let length = input.read_u16()? as usize; + if input.remaining() < length { + return Err(ParserError::TruncatedMsg(format!( + "truncated BGP Prefix-SID TLV value for type {tlv_type}: need {length}, have {}", + input.remaining() + ))); + } + let value = input.split_to(length); + tlvs.push(RawTlv8Ext { tlv_type, value }); + } + + Ok(AttributeValue::BgpPrefixSid(BgpPrefixSidAttribute { tlvs })) +} + +pub fn encode_bgp_prefix_sid(attr: &BgpPrefixSidAttribute) -> Bytes { + let mut buf = BytesMut::new(); + for tlv in &attr.tlvs { + buf.put_u8(tlv.tlv_type); + buf.put_u16(tlv.value.len() as u16); + buf.extend_from_slice(&tlv.value); + } + buf.freeze() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_prefix_sid_label_index_tlv_round_trip() { + let input = Bytes::from_static(&[ + 0x01, 0x00, 0x07, // Label-Index TLV type + value length + 0x00, // reserved + 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x2a, // label index + ]); + let value = parse_bgp_prefix_sid(input.clone()).unwrap(); + match value { + AttributeValue::BgpPrefixSid(attr) => { + assert_eq!(attr.tlvs.len(), 1); + assert_eq!(attr.tlvs[0].tlv_type, 1); + assert_eq!(attr.tlvs[0].value.len(), 7); + assert_eq!(encode_bgp_prefix_sid(&attr), input); + } + value => panic!("expected Prefix-SID, got {value:?}"), + } + } + + #[test] + fn test_parse_prefix_sid_unknown_tlv_round_trip() { + let input = Bytes::from_static(&[0x7f, 0x00, 0x02, 0xaa, 0xbb]); + let value = parse_bgp_prefix_sid(input.clone()).unwrap(); + match value { + AttributeValue::BgpPrefixSid(attr) => { + assert_eq!(attr.tlvs[0].tlv_type, 0x7f); + assert_eq!(attr.tlvs[0].value, Bytes::from_static(&[0xaa, 0xbb])); + assert_eq!(encode_bgp_prefix_sid(&attr), input); + } + value => panic!("expected Prefix-SID, got {value:?}"), + } + } + + #[test] + fn test_parse_prefix_sid_empty_attribute_round_trip() { + let value = parse_bgp_prefix_sid(Bytes::new()).unwrap(); + match value { + AttributeValue::BgpPrefixSid(attr) => { + assert!(attr.tlvs.is_empty()); + assert_eq!(encode_bgp_prefix_sid(&attr), Bytes::new()); + } + value => panic!("expected Prefix-SID, got {value:?}"), + } + } + + #[test] + fn test_parse_prefix_sid_rejects_truncated_tlv() { + assert!(parse_bgp_prefix_sid(Bytes::from_static(&[0x01, 0x00])).is_err()); + assert!(parse_bgp_prefix_sid(Bytes::from_static(&[0x01, 0x00, 0x02, 0xaa])).is_err()); + } +} diff --git a/src/parser/bgp/attributes/attr_41_bier.rs b/src/parser/bgp/attributes/attr_41_bier.rs new file mode 100644 index 0000000..d00d565 --- /dev/null +++ b/src/parser/bgp/attributes/attr_41_bier.rs @@ -0,0 +1,94 @@ +use crate::models::*; +use crate::parser::ReadUtils; +use crate::ParserError; +use bytes::{Buf, BufMut, Bytes, BytesMut}; + +pub fn parse_bier(mut input: Bytes) -> Result { + let mut tlvs = Vec::new(); + + while input.remaining() > 0 { + if input.remaining() < 4 { + return Err(ParserError::TruncatedMsg( + "truncated BIER TLV header".to_string(), + )); + } + let tlv_type = input.read_u16()?; + let length = input.read_u16()? as usize; + if input.remaining() < length { + return Err(ParserError::TruncatedMsg(format!( + "truncated BIER TLV value for type {tlv_type}: need {length}, have {}", + input.remaining() + ))); + } + let value = input.split_to(length); + tlvs.push(RawTlv16 { tlv_type, value }); + } + + Ok(AttributeValue::Bier(BierAttribute { tlvs })) +} + +pub fn encode_bier(attr: &BierAttribute) -> Bytes { + let mut buf = BytesMut::new(); + for tlv in &attr.tlvs { + buf.put_u16(tlv.tlv_type); + buf.put_u16(tlv.value.len() as u16); + buf.extend_from_slice(&tlv.value); + } + buf.freeze() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_bier_tlv_round_trip() { + let input = Bytes::from_static(&[ + 0x00, 0x01, // BIER TLV type 1 + 0x00, 0x03, // value length + 0xaa, 0xbb, 0xcc, + ]); + let value = parse_bier(input.clone()).unwrap(); + match value { + AttributeValue::Bier(attr) => { + assert_eq!(attr.tlvs.len(), 1); + assert_eq!(attr.tlvs[0].tlv_type, 1); + assert_eq!(attr.tlvs[0].value, Bytes::from_static(&[0xaa, 0xbb, 0xcc])); + assert_eq!(encode_bier(&attr), input); + } + value => panic!("expected BIER, got {value:?}"), + } + } + + #[test] + fn test_parse_bier_unknown_tlv_round_trip() { + let input = Bytes::from_static(&[0x12, 0x34, 0x00, 0x02, 0xde, 0xad]); + let value = parse_bier(input.clone()).unwrap(); + match value { + AttributeValue::Bier(attr) => { + assert_eq!(attr.tlvs[0].tlv_type, 0x1234); + assert_eq!(attr.tlvs[0].value, Bytes::from_static(&[0xde, 0xad])); + assert_eq!(encode_bier(&attr), input); + } + value => panic!("expected BIER, got {value:?}"), + } + } + + #[test] + fn test_parse_bier_empty_attribute_round_trip() { + let value = parse_bier(Bytes::new()).unwrap(); + match value { + AttributeValue::Bier(attr) => { + assert!(attr.tlvs.is_empty()); + assert_eq!(encode_bier(&attr), Bytes::new()); + } + value => panic!("expected BIER, got {value:?}"), + } + } + + #[test] + fn test_parse_bier_rejects_truncated_tlv() { + assert!(parse_bier(Bytes::from_static(&[0x00, 0x01, 0x00])).is_err()); + assert!(parse_bier(Bytes::from_static(&[0x00, 0x01, 0x00, 0x02, 0xaa])).is_err()); + } +} diff --git a/src/parser/bgp/attributes/mod.rs b/src/parser/bgp/attributes/mod.rs index cb153fc..2c4432a 100644 --- a/src/parser/bgp/attributes/mod.rs +++ b/src/parser/bgp/attributes/mod.rs @@ -14,6 +14,10 @@ mod attr_26_aigp; mod attr_29_linkstate; mod attr_32_large_communities; mod attr_35_otc; +mod attr_37_sfp; +mod attr_38_bfd_discriminator; +mod attr_40_bgp_prefix_sid; +mod attr_41_bier; use bytes::{Buf, BufMut, Bytes, BytesMut}; use log::{debug, warn}; @@ -55,6 +59,14 @@ use crate::parser::bgp::attributes::attr_32_large_communities::{ use crate::parser::bgp::attributes::attr_35_otc::{ encode_only_to_customer, parse_only_to_customer, }; +use crate::parser::bgp::attributes::attr_37_sfp::{encode_sfp, parse_sfp}; +use crate::parser::bgp::attributes::attr_38_bfd_discriminator::{ + encode_bfd_discriminator, parse_bfd_discriminator, +}; +use crate::parser::bgp::attributes::attr_40_bgp_prefix_sid::{ + encode_bgp_prefix_sid, parse_bgp_prefix_sid, +}; +use crate::parser::bgp::attributes::attr_41_bier::{encode_bier, parse_bier}; use crate::parser::ReadUtils; /// Validate attribute flags according to RFC 4271 and RFC 7606 @@ -132,10 +144,6 @@ fn is_raw_retained_attr(attr_type: AttrType) -> bool { | AttrType::TRAFFIC_ENGINEERING | AttrType::PE_DISTINGUISHER_LABELS | AttrType::BGPSEC_PATH - | AttrType::SFP_ATTRIBUTE - | AttrType::BFD_DISCRIMINATOR - | AttrType::BGP_PREFIX_SID - | AttrType::BIER | AttrType::ATTR_SET ) } @@ -454,6 +462,10 @@ pub fn parse_attributes( AttrType::AIGP => parse_aigp(attr_data), AttrType::TUNNEL_ENCAPSULATION => parse_tunnel_encapsulation_attribute(attr_data), AttrType::BGP_LS_ATTRIBUTE => parse_link_state_attribute(attr_data), + AttrType::SFP_ATTRIBUTE => parse_sfp(attr_data), + AttrType::BFD_DISCRIMINATOR => parse_bfd_discriminator(attr_data), + AttrType::BGP_PREFIX_SID => parse_bgp_prefix_sid(attr_data), + AttrType::BIER => parse_bier(attr_data), _ => Err(ParserError::Unsupported(format!( "unsupported attribute type: {attr_type:?}" ))), @@ -544,6 +556,10 @@ impl Attribute { } AttributeValue::LinkState(v) => encode_link_state_attribute(v), AttributeValue::TunnelEncapsulation(v) => encode_tunnel_encapsulation_attribute(v), + AttributeValue::BfdDiscriminator(v) => encode_bfd_discriminator(v), + AttributeValue::BgpPrefixSid(v) => encode_bgp_prefix_sid(v), + AttributeValue::Bier(v) => encode_bier(v), + AttributeValue::Sfp(v) => encode_sfp(v), AttributeValue::Development(v) => Bytes::from(v.to_owned()), AttributeValue::Raw(v) => v.bytes.clone(), AttributeValue::Deprecated(v) => v.bytes.clone(), @@ -859,6 +875,98 @@ mod tests { ); } + #[test] + fn test_all_raw_retained_attribute_codes_parse_and_round_trip() { + let raw_codes = [0, 22, 24, 27, 33, 128]; + + for code in raw_codes { + let wire = vec![0xc0, code, 0x02, 0xaa, 0xbb]; + let attributes = parse_attributes( + Bytes::from(wire.clone()), + &AsnLength::Bits16, + false, + None, + None, + None, + ) + .unwrap(); + assert_eq!(attributes.inner.len(), 1, "code {code}"); + match &attributes.inner[0].value { + AttributeValue::Raw(raw) => { + assert_eq!(raw.code, code); + assert_eq!(raw.bytes, Bytes::from_static(&[0xaa, 0xbb])); + } + value => panic!("expected Raw for code {code}, got {value:?}"), + } + assert!(attributes.has_attr(AttrType::from(code)), "code {code}"); + assert_eq!(attributes.encode(AsnLength::Bits16), Bytes::from(wire)); + } + } + + #[test] + fn test_unassigned_attribute_code_retained_as_unknown() { + let wire = vec![0xc0, 0x7f, 0x02, 0xaa, 0xbb]; + let attributes = parse_attributes( + Bytes::from(wire.clone()), + &AsnLength::Bits16, + false, + None, + None, + None, + ) + .unwrap(); + + assert_eq!(attributes.inner.len(), 1); + match &attributes.inner[0].value { + AttributeValue::Unknown(raw) => { + assert_eq!(raw.code, 0x7f); + assert_eq!(raw.attr_type(), AttrType::Unknown(0x7f)); + assert_eq!(raw.bytes, Bytes::from_static(&[0xaa, 0xbb])); + } + value => panic!("expected Unknown, got {value:?}"), + } + assert!(attributes.has_attr(AttrType::Unknown(0x7f))); + assert_eq!(attributes.encode(AsnLength::Bits16), Bytes::from(wire)); + } + + #[test] + fn test_structured_tlv_attributes_parse_and_round_trip() { + let cases = [ + ( + vec![0xc0, 0x26, 0x05, 0x01, 0x01, 0x02, 0x03, 0x04], + "BFD Discriminator", + ), + ( + vec![0xc0, 0x28, 0x05, 0x7f, 0x00, 0x02, 0xaa, 0xbb], + "BGP Prefix-SID", + ), + ( + vec![0xc0, 0x29, 0x06, 0x12, 0x34, 0x00, 0x02, 0xde, 0xad], + "BIER", + ), + (vec![0xc0, 0x25, 0x05, 0x7f, 0x00, 0x02, 0xde, 0xad], "SFP"), + ]; + + for (wire, name) in cases { + let data = Bytes::from(wire.clone()); + let attributes = + parse_attributes(data, &AsnLength::Bits16, false, None, None, None).unwrap(); + assert_eq!(attributes.inner.len(), 1, "{name}"); + match (name, &attributes.inner[0].value) { + ("BFD Discriminator", AttributeValue::BfdDiscriminator(_)) + | ("BGP Prefix-SID", AttributeValue::BgpPrefixSid(_)) + | ("BIER", AttributeValue::Bier(_)) + | ("SFP", AttributeValue::Sfp(_)) => {} + (_, value) => panic!("unexpected value for {name}: {value:?}"), + } + assert_eq!( + attributes.encode(AsnLength::Bits16), + Bytes::from(wire), + "{name}" + ); + } + } + #[test] fn test_rfc7606_attribute_length_error() { // Create an ORIGIN attribute with wrong length (should be 1 byte, not 2) diff --git a/src/parser/mrt/mrt_elem.rs b/src/parser/mrt/mrt_elem.rs index e298aa7..f9d4fb2 100644 --- a/src/parser/mrt/mrt_elem.rs +++ b/src/parser/mrt/mrt_elem.rs @@ -151,6 +151,10 @@ fn get_relevant_attributes( | AttributeValue::LinkState(_) | AttributeValue::TunnelEncapsulation(_) | AttributeValue::Aigp(_) + | AttributeValue::BfdDiscriminator(_) + | AttributeValue::BgpPrefixSid(_) + | AttributeValue::Bier(_) + | AttributeValue::Sfp(_) | AttributeValue::AttrSet(_) => {} }; } From 66227123c3a6aceadbc2e5839d23287b62c44255 Mon Sep 17 00:00:00 2001 From: Mingwei Zhang Date: Mon, 15 Jun 2026 20:14:20 -0700 Subject: [PATCH 3/7] chore: fix unused Afi import warning on no-default-features builds --- src/models/network/mpls.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/models/network/mpls.rs b/src/models/network/mpls.rs index 53b0e8c..2f3bcd3 100644 --- a/src/models/network/mpls.rs +++ b/src/models/network/mpls.rs @@ -15,7 +15,9 @@ //! - **MultiLabel** (§2.3): Used when Multiple Labels Capability (Code 8) is negotiated. //! Multiple labels can be encoded with the BoS bit delimiting the stack. -use crate::models::network::{Afi, NetworkPrefix}; +#[cfg(feature = "parser")] +use crate::models::network::Afi; +use crate::models::network::NetworkPrefix; #[cfg(feature = "parser")] use bytes::{Buf, Bytes}; use ipnet::IpNet; From 7edd2e202c6fb73f8a1d8ac173f38c8e5f023b69 Mon Sep 17 00:00:00 2001 From: Mingwei Zhang Date: Mon, 15 Jun 2026 20:16:11 -0700 Subject: [PATCH 4/7] chore: suppress unused Afi import warning with scoped allow attr --- src/models/network/mpls.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/models/network/mpls.rs b/src/models/network/mpls.rs index 2f3bcd3..0d612e3 100644 --- a/src/models/network/mpls.rs +++ b/src/models/network/mpls.rs @@ -15,9 +15,8 @@ //! - **MultiLabel** (§2.3): Used when Multiple Labels Capability (Code 8) is negotiated. //! Multiple labels can be encoded with the BoS bit delimiting the stack. -#[cfg(feature = "parser")] -use crate::models::network::Afi; -use crate::models::network::NetworkPrefix; +#[allow(unused_imports)] +use crate::models::network::{Afi, NetworkPrefix}; #[cfg(feature = "parser")] use bytes::{Buf, Bytes}; use ipnet::IpNet; From 6f385bebebc4bb5d885d6e3041845a7805bec034 Mon Sep 17 00:00:00 2001 From: Mingwei Zhang Date: Mon, 15 Jun 2026 20:31:30 -0700 Subject: [PATCH 5/7] fix: surface Raw attributes in MrtElem, fix imports, and improve docs - Merge AttributeValue::Raw into Unknown arm in get_relevant_attributes so raw-retained codes (0,22,24,27,33,128) appear in elem.unknown - Fix scan_path_attributes.rs comments and add date update note - Add doc comment to AttrRaw::attr_type() explaining Raw vs Unknown - Replace Bytes::from(v.to_owned()) with Bytes::copy_from_slice(v) - Replace blanket #[allow(unused_imports)] with precise #[cfg] gating --- examples/scan_path_attributes.rs | 10 +++++++--- src/models/bgp/attributes/mod.rs | 5 +++++ src/models/network/mpls.rs | 5 +++-- src/parser/bgp/attributes/mod.rs | 2 +- src/parser/mrt/mrt_elem.rs | 3 +-- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/examples/scan_path_attributes.rs b/examples/scan_path_attributes.rs index f0476f9..c810318 100644 --- a/examples/scan_path_attributes.rs +++ b/examples/scan_path_attributes.rs @@ -1,7 +1,10 @@ /// Scan MRT archives for interesting BGP path attributes. /// /// Iterates over recent RouteViews and RIPE RIS update files, scanning for -/// raw-retained, deprecated, and recently implemented attributes. +/// deprecated, unknown/unassigned, and raw-retained attributes. +/// +/// NOTE: The date range (year, month, days) is hardcoded below. +/// Update it to a recent window before running. /// /// Usage: /// ```bash @@ -17,7 +20,7 @@ fn scan_file(url: &str, max_elems: u64) -> Result<(HashMap, u64), S let mut processed = 0u64; for elem in parser.into_elem_iter() { - // Check unknown attributes (includes unassigned + raw-retained known codes) + // Check unknown/unassigned and raw-retained known attributes if let Some(ref unknown) = elem.unknown { for raw in unknown { let key = format!("unknown(code={}, type={:?})", raw.code, raw.attr_type()); @@ -58,7 +61,8 @@ fn main() { let mut urls: Vec = Vec::new(); - // RouteViews archive URLs — June 2026, sampling a few hours + // NOTE: Update year/month/days below to a recent date window before running. + // RouteViews update files — last 2 days, sampling hours 0 and 12 for collector in &collectors { for day in [1, 2] { for hour in [0, 12] { diff --git a/src/models/bgp/attributes/mod.rs b/src/models/bgp/attributes/mod.rs index a61eabc..d0348c0 100644 --- a/src/models/bgp/attributes/mod.rs +++ b/src/models/bgp/attributes/mod.rs @@ -810,6 +810,11 @@ pub struct AttrRaw { } impl AttrRaw { + /// Map the raw wire code back to an `AttrType`. + /// + /// For `Raw` variants (known-but-unparsed codes like `PMSI_TUNNEL`), + /// this returns the concrete `AttrType` variant (e.g. `AttrType::PMSI_TUNNEL`). + /// For `Deprecated` and `Unknown` variants, this returns `AttrType::Unknown(code)`. pub fn attr_type(&self) -> AttrType { AttrType::from(self.code) } diff --git a/src/models/network/mpls.rs b/src/models/network/mpls.rs index 0d612e3..2f3bcd3 100644 --- a/src/models/network/mpls.rs +++ b/src/models/network/mpls.rs @@ -15,8 +15,9 @@ //! - **MultiLabel** (§2.3): Used when Multiple Labels Capability (Code 8) is negotiated. //! Multiple labels can be encoded with the BoS bit delimiting the stack. -#[allow(unused_imports)] -use crate::models::network::{Afi, NetworkPrefix}; +#[cfg(feature = "parser")] +use crate::models::network::Afi; +use crate::models::network::NetworkPrefix; #[cfg(feature = "parser")] use bytes::{Buf, Bytes}; use ipnet::IpNet; diff --git a/src/parser/bgp/attributes/mod.rs b/src/parser/bgp/attributes/mod.rs index 2c4432a..761327c 100644 --- a/src/parser/bgp/attributes/mod.rs +++ b/src/parser/bgp/attributes/mod.rs @@ -560,7 +560,7 @@ impl Attribute { AttributeValue::BgpPrefixSid(v) => encode_bgp_prefix_sid(v), AttributeValue::Bier(v) => encode_bier(v), AttributeValue::Sfp(v) => encode_sfp(v), - AttributeValue::Development(v) => Bytes::from(v.to_owned()), + AttributeValue::Development(v) => Bytes::copy_from_slice(v), AttributeValue::Raw(v) => v.bytes.clone(), AttributeValue::Deprecated(v) => v.bytes.clone(), AttributeValue::Unknown(v) => v.bytes.clone(), diff --git a/src/parser/mrt/mrt_elem.rs b/src/parser/mrt/mrt_elem.rs index f9d4fb2..5f47022 100644 --- a/src/parser/mrt/mrt_elem.rs +++ b/src/parser/mrt/mrt_elem.rs @@ -137,13 +137,12 @@ fn get_relevant_attributes( AttributeValue::MpUnreachNlri(nlri) => withdrawn = Some(nlri), AttributeValue::OnlyToCustomer(o) => otc = Some(o), - AttributeValue::Unknown(t) => { + AttributeValue::Unknown(t) | AttributeValue::Raw(t) => { unknown.push(t); } AttributeValue::Deprecated(t) => { deprecated.push(t); } - AttributeValue::Raw(_) => {} AttributeValue::OriginatorId(_) | AttributeValue::Clusters(_) From 5949c9c1023821baf28c9dc5068ecee157f71d38 Mon Sep 17 00:00:00 2001 From: Mingwei Zhang Date: Mon, 15 Jun 2026 20:31:34 -0700 Subject: [PATCH 6/7] docs: add codecov coverage inspection instructions and helper script --- AGENTS.md | 34 +++++++++++++++ scripts/coverage-misses.sh | 88 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100755 scripts/coverage-misses.sh diff --git a/AGENTS.md b/AGENTS.md index 67bea6e..c9f2955 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,6 +72,40 @@ The repository has a `.git/hooks/pre-push` script that runs automatically on eve If any check fails, the push is aborted. Fix the issue, commit, and push again. +## Code Coverage (Codecov) + +CI uploads coverage to Codecov on every push and PR. The `codecov.yml` at the repo root configures ignored paths, thresholds, and PR comment layout. + +### Finding uncovered lines in a PR + +Use `scripts/coverage-misses.sh` to get exact line numbers for uncovered patch lines via the public Codecov API v2 (no token needed): + +```bash +./scripts/coverage-misses.sh +``` + +Example output: + +``` +📄 src/parser/bgp/attributes/attr_26_aigp.rs + Coverage: 85.29% | Patch misses: 10 lines + Uncovered lines: 20, 21, 22, 27, 28, 29, 30, 50, 76, 91 +``` + +This is more actionable than the Codecov PR comment, which only shows miss counts with links to codecov.io. After running the script, use `read` on the file to inspect the uncovered lines. + +### How it works + +The script calls: +1. `GET /compare/impacted_files?pullid=N` — lists files with patch misses +2. `GET /compare/segments/{file_path}?pullid=N` — returns line-by-line coverage (`head_coverage: 1` = uncovered, `0` = covered, `null` = non-code) + +These endpoints are public and documented at https://docs.codecov.com/reference. + +### When to check coverage + +Before pushing a new feature, run the script on your PR to identify lines that need tests. The pre-push hook does NOT gate on coverage — coverage checks in CI are informational (non-blocking, 10% threshold). + ## Notes - The `bgpkit-parser` binary requires the `cli` feature (`required-features = ["cli"]`). diff --git a/scripts/coverage-misses.sh b/scripts/coverage-misses.sh new file mode 100755 index 0000000..3b67566 --- /dev/null +++ b/scripts/coverage-misses.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# Query Codecov API v2 for per-line missing coverage in a PR. +# Usage: ./scripts/coverage-misses.sh [PR_NUMBER] +# PR_NUMBER defaults to the value of GITHUB_PR_NUMBER env var. +set -euo pipefail + +PR_NUMBER="${1:-${GITHUB_PR_NUMBER:-}}" +if [ -z "$PR_NUMBER" ]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +OWNER="bgpkit" +REPO="bgpkit-parser" +API_BASE="https://api.codecov.io/api/v2/github/${OWNER}/repos/${REPO}" + +# ------------------------------------------------------------------- +# 1. Fetch impacted files for this PR +# ------------------------------------------------------------------- +echo "▶ Fetching impacted files for PR #${PR_NUMBER}..." +IMPACTED=$(curl -sf "${API_BASE}/compare/impacted_files?pullid=${PR_NUMBER}") +STATE=$(echo "$IMPACTED" | jq -r '.state') + +if [ "$STATE" != "processed" ]; then + echo "⚠️ Codecov comparison state is '$STATE' (not yet processed). Retry in ~30s." + exit 2 +fi + +# ------------------------------------------------------------------- +# 2. Collect files with misses +# ------------------------------------------------------------------- +echo "▶ Extracting files with uncovered lines..." +FILES_WITH_MISSES=$(echo "$IMPACTED" | jq -r ' + .files[] + | select(.patch_coverage != null and .misses_count > 0) + | [.head_name, (.patch_coverage.misses|tostring), (.patch_coverage.coverage*100|floor/100|tostring)] + | @tsv +') + +if [ -z "$FILES_WITH_MISSES" ]; then + echo "✅ No files with uncovered lines in this PR." + exit 0 +fi + +# ------------------------------------------------------------------- +# 3. For each file, get line-level segments from Codecov API +# ------------------------------------------------------------------- +echo "▶ Fetching line-level detail for each file..." +echo "" + +FIRST=1 +while IFS=$'\t' read -r file_name misses coverage_pct; do + # URL-encode the path + ENCODED_PATH=$(echo -n "$file_name" | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read()))") + + SEGMENTS_JSON=$(curl -sf "${API_BASE}/compare/segments/${ENCODED_PATH}?pullid=${PR_NUMBER}") + # Only count NEW lines that are uncovered (base_coverage is null → new line) + MISS_LINES=$(echo "$SEGMENTS_JSON" | jq -r ' + [.segments[].lines[] + | select(.head_coverage == 1 and .base_coverage == null) + | .head_number] + | sort | unique | join(", ") + ') + LINE_COUNT=$(echo "$SEGMENTS_JSON" | jq -r '[.segments[].lines[] | select(.head_coverage == 1 and .base_coverage == null)] | length') + ALL_UNCOVERED=$(echo "$SEGMENTS_JSON" | jq -r '[.segments[].lines[] | select(.head_coverage == 1)] | length') + + # Spacer between files + if [ "$FIRST" -eq 1 ]; then + FIRST=0 + else + echo "" + fi + + echo "📄 $file_name" + if [ "$LINE_COUNT" -ne "$ALL_UNCOVERED" ]; then + EXTRA=" ($((ALL_UNCOVERED - LINE_COUNT)) pre-existing uncovered)" + else + EXTRA="" + fi + echo " Coverage: ${coverage_pct}% | Patch misses: ${LINE_COUNT} lines${EXTRA}" + echo " Uncovered lines: ${MISS_LINES}" +done <<< "$FILES_WITH_MISSES" + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +TOTAL_MISSES=$(echo "$IMPACTED" | jq -r '.totals.patch.misses') +TOTAL_HITS=$(echo "$IMPACTED" | jq -r '.totals.patch.hits') +echo "Total patch: ${TOTAL_HITS} hit / ${TOTAL_MISSES} miss ($(echo "scale=1; ${TOTAL_HITS}*100/(${TOTAL_HITS}+${TOTAL_MISSES})" | bc)% coverage)" From a29f9bfd0be4a925f270cf7b221029e530e71897 Mon Sep 17 00:00:00 2001 From: Mingwei Zhang Date: Mon, 15 Jun 2026 20:35:10 -0700 Subject: [PATCH 7/7] test: add tests for coverage gaps in AIGP/BFD parsers and attr_category - AIGP: test length<3 rejection, truncated value rejection, encode length correction - BFD Discriminator: test truncated optional TLV header error path - models: test attr_category() for BfdDiscriminator, BgpPrefixSid, Bier, Sfp --- src/models/bgp/attributes/mod.rs | 29 +++++++++++++++++++ src/parser/bgp/attributes/attr_26_aigp.rs | 29 +++++++++++++++++++ .../attributes/attr_38_bfd_discriminator.rs | 7 +++++ 3 files changed, 65 insertions(+) diff --git a/src/models/bgp/attributes/mod.rs b/src/models/bgp/attributes/mod.rs index d0348c0..ee85ba4 100644 --- a/src/models/bgp/attributes/mod.rs +++ b/src/models/bgp/attributes/mod.rs @@ -1081,6 +1081,35 @@ mod tests { ); } + #[test] + fn test_new_attribute_attr_categories() { + // BFD Discriminator (RFC 9026): Optional Transitive + assert_eq!( + AttributeValue::BfdDiscriminator(BfdDiscriminatorAttribute { + mode: 1, + discriminator: 0, + tlvs: vec![], + }) + .attr_category(), + Some(AttributeCategory::OptionalTransitive) + ); + // BGP Prefix-SID (RFC 8669): Optional Transitive + assert_eq!( + AttributeValue::BgpPrefixSid(BgpPrefixSidAttribute { tlvs: vec![] }).attr_category(), + Some(AttributeCategory::OptionalTransitive) + ); + // BIER (RFC 9793): Optional Transitive + assert_eq!( + AttributeValue::Bier(BierAttribute { tlvs: vec![] }).attr_category(), + Some(AttributeCategory::OptionalTransitive) + ); + // SFP (RFC 9015): Optional Transitive + assert_eq!( + AttributeValue::Sfp(SfpAttribute { tlvs: vec![] }).attr_category(), + Some(AttributeCategory::OptionalTransitive) + ); + } + #[test] fn test_optional_non_transitive_attrs() { let multi_exit_discriminator_attr = AttributeValue::MultiExitDiscriminator(1); diff --git a/src/parser/bgp/attributes/attr_26_aigp.rs b/src/parser/bgp/attributes/attr_26_aigp.rs index 0fd26cd..f86a545 100644 --- a/src/parser/bgp/attributes/attr_26_aigp.rs +++ b/src/parser/bgp/attributes/attr_26_aigp.rs @@ -96,4 +96,33 @@ mod tests { fn test_parse_aigp_rejects_short_tlv() { assert!(parse_aigp(Bytes::from_static(&[0x01, 0x00])).is_err()); } + + #[test] + fn test_parse_aigp_rejects_invalid_length() { + // TLV with length < 3 (type=1, length=2) + let input = Bytes::from_static(&[0x01, 0x00, 0x02, 0x00]); + assert!(parse_aigp(input).is_err()); + } + + #[test] + fn test_parse_aigp_rejects_truncated_value() { + // TLV header claims 8 bytes total (value_len=5), but only 3 bytes after header + let input = Bytes::from_static(&[0x01, 0x00, 0x08, 0x00, 0x00, 0x00]); + assert!(parse_aigp(input).is_err()); + } + + #[test] + fn test_encode_aigp_corrects_mismatched_length() { + // stored length (3) does not match actual value len (0) + 3 + let aigp = Aigp { + tlvs: vec![AigpTlv { + tlv_type: 1, + length: 3, + value: Bytes::new(), + }], + }; + let encoded = encode_aigp(&aigp); + // corrected length: 0 value bytes + 3 header = 3 + assert_eq!(encoded, Bytes::from_static(&[0x01, 0x00, 0x03])); + } } diff --git a/src/parser/bgp/attributes/attr_38_bfd_discriminator.rs b/src/parser/bgp/attributes/attr_38_bfd_discriminator.rs index 89d81b2..7ccf748 100644 --- a/src/parser/bgp/attributes/attr_38_bfd_discriminator.rs +++ b/src/parser/bgp/attributes/attr_38_bfd_discriminator.rs @@ -100,4 +100,11 @@ mod tests { ])) .is_err()); } + + #[test] + fn test_parse_bfd_discriminator_rejects_truncated_optional_tlv_header() { + // Valid BFD (5 bytes) + only 1 byte for optional TLV header (need 2) + let input = Bytes::from_static(&[0x01, 0x01, 0x02, 0x03, 0x04, 0x01]); + assert!(parse_bfd_discriminator(input).is_err()); + } }