diff --git a/AGENTS.md b/AGENTS.md index ffef567..8f0c51d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,6 +33,11 @@ Always run clippy with `--all-features`: cargo clippy --all-features ``` +The pre-push hook uses the stricter form below; run it before pushing if you want to match the exact CI check: +``` +cargo clippy --all-targets --all-features -- -D warnings +``` + ### Formatting `cargo fmt` does not accept `--all-features` (formatting is feature-independent): ``` @@ -43,6 +48,30 @@ To check formatting without modifying files: cargo fmt -- --check ``` +### Documentation +`README.md` is **auto-generated** from `src/lib.rs` doc comments via [`cargo-readme`](https://github.com/livioribeiro/cargo-readme). + +- Edit documentation in **`src/lib.rs`** (inside the top-level `//!` doc comment block), not in `README.md` directly. +- After editing `src/lib.rs`, regenerate `README.md`: + ``` + cargo readme > README.md + ``` +- The pre-push hook runs a `cargo readme` diff check; if `README.md` is out of sync with `src/lib.rs`, the push will be rejected. +- If you do not have `cargo-readme` installed: + ``` + cargo install cargo-readme + ``` + +## Pre-push Checks + +The repository has a `.git/hooks/pre-push` script that runs automatically on every push. It performs three checks in order: + +1. **Formatting**: `cargo fmt --check` +2. **README sync**: `cargo readme > TMP_README.md && diff -b TMP_README.md README.md` +3. **Clippy**: `cargo clippy --all-targets --all-features -- -D warnings` + +If any check fails, the push is aborted. Fix the issue, commit, and push again. + ## Notes - The `bgpkit-parser` binary requires the `cli` feature (`required-features = ["cli"]`). diff --git a/CHANGELOG.md b/CHANGELOG.md index 4533198..fbfb7c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,10 @@ All notable changes to this project will be documented in this file. * **`Attributes::attr_mask`**: Internal `[u64; 4]` bitmask field for O(1) attribute presence tracking (32 bytes vs 256 bytes for `[bool; 256]`) * **`serde` feature**: Enabled `serde/rc` for `Arc` serialization support in `BgpRouteElem` when the `serde` feature is active. +* **BMP RFC 9515, 9736, 9972 support**: + - RFC 9972: Added 26 new `StatType` variants for advanced Adj-RIB-In/Adj-RIB-Out monitoring statistics (types 18–43) + - RFC 9736: Separated Peer Up TLV namespace from Initiation TLVs; both enums now use `FromPrimitive` with catch-all `Unknown(u16)` for forward compatibility with unassigned types + - RFC 9515: Documentation-only update (registration procedures changed to "First Come First Served") * **Integration tests**: Added `tests/test_bgp_update_validation.rs` with comprehensive scenario coverage for all validation cases * **Example `examples/parse_bmp_mpls.rs`**: Demonstrates extracting MPLS-labeled NLRI from BMP Route Monitoring messages, showing how to access label stack information via `MpReachNlri` attribute diff --git a/README.md b/README.md index 300fccc..1fc95fe 100644 --- a/README.md +++ b/README.md @@ -771,6 +771,9 @@ BGPKIT Parser implements comprehensive BGP, MRT, BMP, and related protocol stand - [RFC 7854](https://datatracker.ietf.org/doc/html/rfc7854): BGP Monitoring Protocol (BMP) - [RFC 8671](https://datatracker.ietf.org/doc/html/rfc8671): Support for Adj-RIB-Out in BMP - [RFC 9069](https://datatracker.ietf.org/doc/html/rfc9069): Support for Local RIB in BMP +- [RFC 9515](https://datatracker.ietf.org/doc/html/rfc9515): Revision to Registration Procedures for Multiple BMP Registries +- [RFC 9736](https://datatracker.ietf.org/doc/html/rfc9736): The BGP Monitoring Protocol (BMP) Peer Up Message Namespace +- [RFC 9972](https://datatracker.ietf.org/doc/html/rfc9972): Advanced BGP Monitoring Protocol (BMP) Statistics Types ### BGP Communities diff --git a/src/lib.rs b/src/lib.rs index 1bb5958..3d526bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -767,6 +767,9 @@ BGPKIT Parser implements comprehensive BGP, MRT, BMP, and related protocol stand - [RFC 7854](https://datatracker.ietf.org/doc/html/rfc7854): BGP Monitoring Protocol (BMP) - [RFC 8671](https://datatracker.ietf.org/doc/html/rfc8671): Support for Adj-RIB-Out in BMP - [RFC 9069](https://datatracker.ietf.org/doc/html/rfc9069): Support for Local RIB in BMP +- [RFC 9515](https://datatracker.ietf.org/doc/html/rfc9515): Revision to Registration Procedures for Multiple BMP Registries +- [RFC 9736](https://datatracker.ietf.org/doc/html/rfc9736): The BGP Monitoring Protocol (BMP) Peer Up Message Namespace +- [RFC 9972](https://datatracker.ietf.org/doc/html/rfc9972): Advanced BGP Monitoring Protocol (BMP) Statistics Types ## BGP Communities diff --git a/src/parser/bmp/error.rs b/src/parser/bmp/error.rs index df9b4e3..3d5cd7f 100644 --- a/src/parser/bmp/error.rs +++ b/src/parser/bmp/error.rs @@ -1,7 +1,5 @@ use crate::bmp::messages::headers::BmpPeerType; -use crate::bmp::messages::initiation_message::InitiationTlvType; use crate::bmp::messages::peer_down_notification::PeerDownReason; -use crate::bmp::messages::peer_up_notification::PeerUpTlvType; use crate::bmp::messages::route_mirroring::RouteMirroringInfo; use crate::bmp::messages::termination_message::{TerminationReason, TerminationTlvType}; use crate::bmp::messages::BmpMsgType; @@ -76,18 +74,6 @@ impl From> for ParserBmpError { } } -impl From> for ParserBmpError { - fn from(_: TryFromPrimitiveError) -> Self { - ParserBmpError::UnknownTlvType - } -} - -impl From> for ParserBmpError { - fn from(_: TryFromPrimitiveError) -> Self { - ParserBmpError::UnknownTlvType - } -} - impl From> for ParserBmpError { fn from(_: TryFromPrimitiveError) -> Self { ParserBmpError::CorruptedBmpMessage @@ -166,10 +152,6 @@ mod tests { ParserBmpError::from(TryFromPrimitiveError::::new(0)), ParserBmpError::CorruptedBmpMessage ); - assert_eq!( - ParserBmpError::from(TryFromPrimitiveError::::new(0)), - ParserBmpError::UnknownTlvType - ); assert_eq!( ParserBmpError::from(TryFromPrimitiveError::::new(0)), ParserBmpError::CorruptedBmpMessage @@ -178,10 +160,6 @@ mod tests { ParserBmpError::from(TryFromPrimitiveError::::new(0)), ParserBmpError::UnknownTlvType ); - assert_eq!( - ParserBmpError::from(TryFromPrimitiveError::::new(0)), - ParserBmpError::UnknownTlvType - ); assert_eq!( ParserBmpError::from(TryFromPrimitiveError::::new(0)), ParserBmpError::UnknownTlvValue diff --git a/src/parser/bmp/messages/initiation_message.rs b/src/parser/bmp/messages/initiation_message.rs index 5761a52..832814b 100644 --- a/src/parser/bmp/messages/initiation_message.rs +++ b/src/parser/bmp/messages/initiation_message.rs @@ -1,8 +1,7 @@ use crate::parser::bmp::error::ParserBmpError; use crate::parser::ReadUtils; use bytes::{Buf, Bytes}; -use num_enum::{IntoPrimitive, TryFromPrimitive}; -use std::convert::TryFrom; +use num_enum::{FromPrimitive, IntoPrimitive}; #[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] @@ -18,18 +17,27 @@ pub struct InitiationTlv { pub info: String, } -///Type-Length-Value Type +/// BMP Initiation Information TLV Type /// -/// -#[derive(Debug, TryFromPrimitive, IntoPrimitive, PartialEq, Clone, Copy)] +/// +/// +/// Per RFC 9736, the "BMP Initiation and Peer Up Information TLVs" registry was renamed +/// to "BMP Initiation Information TLVs", and types 3, 4, and 65535 are reserved in this +/// namespace. The VrTableName and AdminLabel variants are retained for backward compatibility +/// with pre-RFC 9736 implementations. +#[derive(Debug, FromPrimitive, IntoPrimitive, PartialEq, Clone, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(u16)] pub enum InitiationTlvType { String = 0, SysDescr = 1, SysName = 2, + /// Reserved per RFC 9736, retained for backward compatibility. VrTableName = 3, + /// Reserved per RFC 9736, retained for backward compatibility. AdminLabel = 4, + #[num_enum(catch_all)] + Unknown(u16) = 65535, } /// Parse BMP initiation message @@ -39,7 +47,7 @@ pub fn parse_initiation_message(data: &mut Bytes) -> Result 4 { - let info_type: InitiationTlvType = InitiationTlvType::try_from(data.read_u16()?)?; + let info_type = InitiationTlvType::from(data.read_u16()?); let info_len = data.read_u16()?; if data.remaining() < info_len as usize { // not enough bytes to read @@ -82,6 +90,29 @@ mod tests { } } + #[test] + fn test_parse_unknown_initiation_tlv() { + let mut buffer = BytesMut::new(); + // Known TLV (SysDescr) + buffer.put_u16(1); // InitiationTlvType::SysDescr + buffer.put_u16(4); + buffer.put_slice(b"Test"); + // Unknown TLV (unassigned type 255) + buffer.put_u16(255); + buffer.put_u16(3); + buffer.put_slice(b"Foo"); + + let mut bytes = buffer.freeze(); + let result = parse_initiation_message(&mut bytes).unwrap(); + + assert_eq!(result.tlvs.len(), 2); + assert_eq!(result.tlvs[0].info_type, InitiationTlvType::SysDescr); + assert_eq!(result.tlvs[0].info, "Test"); + assert_eq!(result.tlvs[1].info_type, InitiationTlvType::Unknown(255)); + assert_eq!(result.tlvs[1].info_len, 3); + assert_eq!(result.tlvs[1].info, "Foo"); + } + #[test] fn test_debug() { let initiation_message = InitiationMessage { diff --git a/src/parser/bmp/messages/peer_up_notification.rs b/src/parser/bmp/messages/peer_up_notification.rs index 7c2934c..235df12 100644 --- a/src/parser/bmp/messages/peer_up_notification.rs +++ b/src/parser/bmp/messages/peer_up_notification.rs @@ -6,7 +6,7 @@ use crate::parser::bmp::messages::BmpPeerType; use crate::parser::ReadUtils; use bytes::{Buf, Bytes}; use log::warn; -use num_enum::{IntoPrimitive, TryFromPrimitive}; +use num_enum::{FromPrimitive, IntoPrimitive}; use std::net::IpAddr; #[derive(Debug, PartialEq, Clone)] @@ -20,18 +20,27 @@ pub struct PeerUpNotification { pub tlvs: Vec, } -///Type-Length-Value Type +/// BMP Peer Up Message TLV Type /// -/// -#[derive(Debug, TryFromPrimitive, IntoPrimitive, PartialEq, Clone, Copy)] +/// +/// +/// RFC 9736 created an independent namespace for Peer Up Information TLVs, separate from +/// the Initiation message namespace. Per the new registry, types 1, 2, and 65535 are reserved. +/// The SysDescr and SysName variants are retained for backward compatibility with pre-RFC 9736 +/// implementations. +#[derive(Debug, FromPrimitive, IntoPrimitive, PartialEq, Clone, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(u16)] pub enum PeerUpTlvType { String = 0, + /// Reserved per RFC 9736, retained for backward compatibility. SysDescr = 1, + /// Reserved per RFC 9736, retained for backward compatibility. SysName = 2, VrTableName = 3, AdminLabel = 4, + #[num_enum(catch_all)] + Unknown(u16) = 65535, } #[derive(Debug, PartialEq, Clone)] @@ -104,7 +113,7 @@ pub fn parse_peer_up_notification( let mut has_vr_table_name = false; while data.remaining() >= 4 { - let info_type = PeerUpTlvType::try_from(data.read_u16()?)?; + let info_type = PeerUpTlvType::from(data.read_u16()?); let info_len = data.read_u16()?; let info_value = data.read_n_bytes_to_string(info_len as usize)?; @@ -821,6 +830,59 @@ mod tests { // In production, a warning would be logged about oversized VrTableName } + #[test] + fn test_parse_peer_up_unknown_tlv() { + let mut data = BytesMut::new(); + + // Local address setup + data.extend_from_slice(&[ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xA8, + 0x01, 0x01, // 192.168.1.1 + 0x00, 0xB3, // local port 179 + 0x00, 0xB3, // remote port 179 + ]); + + // Add two complete BGP OPEN messages + let bgp_open = crate::models::BgpMessage::Open(BgpOpenMessage { + version: 4, + asn: crate::models::Asn::new_32bit(65001), + hold_time: 180, + bgp_identifier: Ipv4Addr::new(192, 168, 1, 1), + extended_length: false, + opt_params: vec![], + }); + let bgp_bytes = bgp_open.encode(AsnLength::Bits32); + data.extend_from_slice(&bgp_bytes); + data.extend_from_slice(&bgp_bytes); + + // Add a known TLV (String) and an unknown TLV (type 255) + data.extend_from_slice(&[0x00, 0x00]); // String TLV type + data.extend_from_slice(&[0x00, 0x04]); // length 4 + data.extend_from_slice(b"Test"); + + data.extend_from_slice(&[0x00, 0xFF]); // Unknown TLV type (255) + data.extend_from_slice(&[0x00, 0x03]); // length 3 + data.extend_from_slice(b"Foo"); + + let afi = Afi::Ipv4; + let asn_len = AsnLength::Bits32; + let result = parse_peer_up_notification(&mut data.freeze(), &afi, &asn_len, None); + + assert!( + result.is_ok(), + "Should parse unknown TLV types without error" + ); + let peer_notification = result.unwrap(); + assert_eq!(peer_notification.tlvs.len(), 2); + assert_eq!(peer_notification.tlvs[0].info_type, PeerUpTlvType::String); + assert_eq!(peer_notification.tlvs[0].info_value, "Test"); + assert_eq!( + peer_notification.tlvs[1].info_type, + PeerUpTlvType::Unknown(255) + ); + assert_eq!(peer_notification.tlvs[1].info_value, "Foo"); + } + #[test] fn test_non_local_rib_no_validation() { // This test verifies that non-LocalRib peer types don't trigger LocalRib validations diff --git a/src/parser/bmp/messages/stats_report.rs b/src/parser/bmp/messages/stats_report.rs index b8d696a..8a536da 100644 --- a/src/parser/bmp/messages/stats_report.rs +++ b/src/parser/bmp/messages/stats_report.rs @@ -53,6 +53,30 @@ pub enum StatType { RoutesInPostPolicyAdjRibOut = 15, RoutesInPerAfiSafiPrePolicyAdjRibOut = 16, RoutesInPerAfiSafiPostPolicyAdjRibOut = 17, + RoutesInPrePolicyAdjRibIn = 18, + RoutesInPerAfiSafiPrePolicyAdjRibIn = 19, + RoutesInPostPolicyAdjRibIn = 20, + RoutesInPerAfiSafiPostPolicyAdjRibIn = 21, + RoutesInPerAfiSafiPrePolicyAdjRibInRejectedByPolicy = 22, + RoutesInPerAfiSafiPostPolicyAdjRibInAcceptedByPolicy = 23, + RoutesInPerAfiSafiPostPolicyAdjRibInOrLocRibSuppressed = 26, + RoutesInPerAfiSafiPostPolicyAdjRibInOrLocRibStaleByGracefulRestart = 27, + RoutesInPerAfiSafiPostPolicyAdjRibInOrLocRibStaleByLongLivedGracefulRestart = 28, + RoutesInPostPolicyAdjRibInLeftBeforeThreshold = 29, + RoutesInPerAfiSafiPostPolicyAdjRibInLeftBeforeThreshold = 30, + RoutesInPostPolicyAdjRibInOrLocRibLeftBeforeLicenseThreshold = 31, + RoutesInPerAfiSafiPostPolicyAdjRibInOrLocRibLeftBeforeLicenseThreshold = 32, + RoutesInPrePolicyAdjRibInRejectedDueToMaxAsPathLength = 33, + RoutesInPerAfiSafiPrePolicyAdjRibInRejectedDueToMaxAsPathLength = 34, + RoutesInPerAfiSafiPostPolicyAdjRibInInvalidatedByRoa = 35, + RoutesInPerAfiSafiPostPolicyAdjRibInValidatedByRoa = 36, + RoutesInPerAfiSafiPostPolicyAdjRibInRpkiStateNotFound = 37, + RoutesInPerAfiSafiPrePolicyAdjRibOutRejectedByPolicy = 38, + RoutesInPrePolicyAdjRibOutFilteredDueToMaxAsPathLength = 39, + RoutesInPerAfiSafiPrePolicyAdjRibOutFilteredDueToMaxAsPathLength = 40, + RoutesInPerAfiSafiPostPolicyAdjRibOutInvalidatedByRoa = 41, + RoutesInPerAfiSafiPostPolicyAdjRibOutValidatedByRoa = 42, + RoutesInPerAfiSafiPostPolicyAdjRibOutRpkiStateNotFound = 43, #[num_enum(catch_all)] Other(u16) = 65535, }