From 66438f953840b1e4d5485e4b1126050f86a55a51 Mon Sep 17 00:00:00 2001 From: Mingwei Zhang Date: Sun, 3 Aug 2025 15:20:27 -0700 Subject: [PATCH 1/3] feat: add RFC 8950 support for IPv4 NLRI with IPv6 next-hops Implements RFC 8950 "Advertising IPv4 Network Layer Reachability Information (NLRI) with an IPv6 Next Hop" to enable parsing of VPN-IPv4 routes with VPN-IPv6 next-hops. Changes include: - Extended Safi enum with MplsVpn (128) and MulticastVpn (129) for VPN address families - Added RouteDistinguisher type for 8-byte VPN route distinguisher parsing - Extended NextHopAddress enum with VpnIpv6 and VpnIpv6LinkLocal variants - Updated parse_mp_next_hop to handle 24-byte and 48-byte VPN next-hops: * 24 bytes: 8-byte RD + 16-byte IPv6 (VPN-IPv6 next hop) * 48 bytes: 8-byte RD + 16-byte IPv6 + 8-byte RD + 16-byte IPv6 link-local - Updated all exhaustive pattern matches to handle new NextHopAddress variants - Added comprehensive tests for VPN next-hop parsing and encoding - Updated documentation to mark RFC 8950 as supported This implementation is fully backward compatible and enables parsing of previously unsupported VPN next-hop formats per RFC 8950 Section 4. --- CHANGELOG.md | 6 ++ README.md | 2 +- src/lib.rs | 2 +- src/models/network/afi.rs | 31 +++++- src/models/network/nexthop.rs | 83 ++++++++++++++++ src/parser/bgp/attributes/attr_03_next_hop.rs | 97 +++++++++++++++++++ src/parser/bgp/attributes/attr_14_15_nlri.rs | 14 +++ src/parser/mrt/mrt_elem.rs | 10 ++ src/parser/utils.rs | 7 ++ 9 files changed, 249 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9c152d4..69f8592b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,12 @@ All notable changes to this project will be documented in this file. ### Code improvements +* added RFC 8950 support for IPv4 NLRI with IPv6 next-hops + * extended `Safi` enum with `MplsVpn` (128) and `MulticastVpn` (129) for VPN address families + * added `RouteDistinguisher` type for VPN next-hop parsing + * extended `NextHopAddress` enum with `VpnIpv6` and `VpnIpv6LinkLocal` variants + * updated `parse_mp_next_hop` to handle 24-byte and 48-byte VPN next-hops + * enables parsing of VPN-IPv4 routes with VPN-IPv6 next-hops per RFC 8950 Section 4 * added RFC 9069 validation warnings for BMP Local RIB peer types * validates Local RIB peer header requirements including zero-filled peer address and 4-byte ASN encoding * validates Local RIB peer up notifications for fabricated OPEN messages and VrTableName TLV presence diff --git a/README.md b/README.md index 1250de65..61c51ae7 100644 --- a/README.md +++ b/README.md @@ -371,7 +371,7 @@ If you would like to see any specific RFC's support, please submit an issue on G - [X] [RFC 5065](https://datatracker.ietf.org/doc/html/rfc5065): Autonomous System Confederations for BGP - [X] [RFC 6793](https://datatracker.ietf.org/doc/html/rfc6793): BGP Support for Four-Octet Autonomous System (AS) Number Space - [X] [RFC 7911](https://datatracker.ietf.org/doc/html/rfc7911): Advertisement of Multiple Paths in BGP (ADD-PATH) -- [ ] [RFC 8950](https://datatracker.ietf.org/doc/html/rfc8950): Advertising IPv4 Network Layer Reachability Information (NLRI) with an IPv6 Next Hop +- [X] [RFC 8950](https://datatracker.ietf.org/doc/html/rfc8950): Advertising IPv4 Network Layer Reachability Information (NLRI) with an IPv6 Next Hop - [X] [RFC 9072](https://datatracker.ietf.org/doc/html/rfc9072): Extended Optional Parameters Length for BGP OPEN Message Updates - [X] [RFC 9234](https://datatracker.ietf.org/doc/html/rfc9234): Route Leak Prevention and Detection Using Roles in UPDATE and OPEN Messages diff --git a/src/lib.rs b/src/lib.rs index 39adb7ff..c96b1d74 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -363,7 +363,7 @@ If you would like to see any specific RFC's support, please submit an issue on G - [X] [RFC 5065](https://datatracker.ietf.org/doc/html/rfc5065): Autonomous System Confederations for BGP - [X] [RFC 6793](https://datatracker.ietf.org/doc/html/rfc6793): BGP Support for Four-Octet Autonomous System (AS) Number Space - [X] [RFC 7911](https://datatracker.ietf.org/doc/html/rfc7911): Advertisement of Multiple Paths in BGP (ADD-PATH) -- [ ] [RFC 8950](https://datatracker.ietf.org/doc/html/rfc8950): Advertising IPv4 Network Layer Reachability Information (NLRI) with an IPv6 Next Hop +- [X] [RFC 8950](https://datatracker.ietf.org/doc/html/rfc8950): Advertising IPv4 Network Layer Reachability Information (NLRI) with an IPv6 Next Hop - [X] [RFC 9072](https://datatracker.ietf.org/doc/html/rfc9072): Extended Optional Parameters Length for BGP OPEN Message Updates - [X] [RFC 9234](https://datatracker.ietf.org/doc/html/rfc9234): Route Leak Prevention and Detection Using Roles in UPDATE and OPEN Messages diff --git a/src/models/network/afi.rs b/src/models/network/afi.rs index 34991e4e..4fa1e4ea 100644 --- a/src/models/network/afi.rs +++ b/src/models/network/afi.rs @@ -24,7 +24,14 @@ impl From for Afi { /// SAFI -- Subsequent Address Family Identifier /// -/// SAFI can be: Unicast, Multicast, or both. +/// SAFI can be: Unicast, Multicast, or both, as well as MPLS VPN variants. +/// The AFI determines the IP version (IPv4/IPv6), while SAFI determines the application. +/// +/// References: +/// - RFC 4760: Multiprotocol Extensions for BGP-4 +/// - RFC 4364: BGP/MPLS IP Virtual Private Networks (VPNs) - defines SAFI 128 +/// - RFC 6514: BGP Signaling of Multicast VPNs - defines SAFI 129 +/// - RFC 8950: Advertising IPv4 Network Layer Reachability Information (NLRI) with an IPv6 Next Hop #[derive(Debug, PartialEq, TryFromPrimitive, IntoPrimitive, Clone, Copy, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(u8)] @@ -32,6 +39,12 @@ pub enum Safi { Unicast = 1, Multicast = 2, UnicastMulticast = 3, + /// MPLS-labeled VPN address - RFC 4364, used in RFC 8950 Section 4 + /// Works with both AFI 1 (VPN-IPv4) and AFI 2 (VPN-IPv6) + MplsVpn = 128, + /// Multicast for BGP/MPLS IP VPNs - RFC 6514 + /// Works with both AFI 1 (Multicast VPN-IPv4) and AFI 2 (Multicast VPN-IPv6) + MulticastVpn = 129, } #[cfg(test)] @@ -58,6 +71,9 @@ mod tests { assert_eq!(Safi::Unicast as u8, 1); assert_eq!(Safi::Multicast as u8, 2); assert_eq!(Safi::UnicastMulticast as u8, 3); + // RFC 8950 VPN SAFI values + assert_eq!(Safi::MplsVpn as u8, 128); + assert_eq!(Safi::MulticastVpn as u8, 129); } #[test] @@ -92,5 +108,18 @@ mod tests { assert_eq!(serialized, "\"UnicastMulticast\""); let deserialized: Safi = serde_json::from_str(&serialized).unwrap(); assert_eq!(deserialized, safi); + + // RFC 8950 VPN SAFI variants + let safi = Safi::MplsVpn; + let serialized = serde_json::to_string(&safi).unwrap(); + assert_eq!(serialized, "\"MplsVpn\""); + let deserialized: Safi = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, safi); + + let safi = Safi::MulticastVpn; + let serialized = serde_json::to_string(&safi).unwrap(); + assert_eq!(serialized, "\"MulticastVpn\""); + let deserialized: Safi = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, safi); } } diff --git a/src/models/network/nexthop.rs b/src/models/network/nexthop.rs index a10a2928..d4ee40f4 100644 --- a/src/models/network/nexthop.rs +++ b/src/models/network/nexthop.rs @@ -1,15 +1,48 @@ use std::fmt::{Debug, Display, Formatter}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +/// Route Distinguisher for VPN next-hops - RFC 4364, Section 4.1 +/// An 8-byte value used to distinguish VPN routes with potentially overlapping address spaces +#[derive(PartialEq, Copy, Clone, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct RouteDistinguisher(pub [u8; 8]); + +impl Debug for RouteDistinguisher { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "RD({:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x})", + self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5], self.0[6], self.0[7] + ) + } +} + +impl Display for RouteDistinguisher { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", + self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5], self.0[6], self.0[7] + ) + } +} + /// enum that represents the type of the next hop address. /// /// [NextHopAddress] is used when parsing for next hops in [Nlri](crate::models::Nlri). +/// RFC 8950 extends this to support VPN next-hops with Route Distinguishers. #[derive(PartialEq, Copy, Clone, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum NextHopAddress { Ipv4(Ipv4Addr), Ipv6(Ipv6Addr), Ipv6LinkLocal(Ipv6Addr, Ipv6Addr), + /// VPN-IPv6 next hop - RFC 8950, Section 4 + /// Contains Route Distinguisher (8 bytes) + IPv6 address (16 bytes) = 24 bytes total + VpnIpv6(RouteDistinguisher, Ipv6Addr), + /// VPN-IPv6 next hop with link-local - RFC 8950, Section 4 + /// Contains RD (8 bytes) + IPv6 (16 bytes) + RD (8 bytes) + IPv6 link-local (16 bytes) = 48 bytes total + VpnIpv6LinkLocal(RouteDistinguisher, Ipv6Addr, RouteDistinguisher, Ipv6Addr), } impl NextHopAddress { @@ -19,6 +52,8 @@ impl NextHopAddress { NextHopAddress::Ipv4(x) => x.is_link_local(), NextHopAddress::Ipv6(x) => (x.segments()[0] & 0xffc0) == 0xfe80, NextHopAddress::Ipv6LinkLocal(_, _) => true, + NextHopAddress::VpnIpv6(_, x) => (x.segments()[0] & 0xffc0) == 0xfe80, + NextHopAddress::VpnIpv6LinkLocal(_, _, _, _) => true, } } @@ -28,6 +63,8 @@ impl NextHopAddress { NextHopAddress::Ipv4(x) => IpAddr::V4(*x), NextHopAddress::Ipv6(x) => IpAddr::V6(*x), NextHopAddress::Ipv6LinkLocal(x, _) => IpAddr::V6(*x), + NextHopAddress::VpnIpv6(_, x) => IpAddr::V6(*x), + NextHopAddress::VpnIpv6LinkLocal(_, x, _, _) => IpAddr::V6(*x), } } } @@ -39,6 +76,10 @@ impl Debug for NextHopAddress { NextHopAddress::Ipv4(x) => write!(f, "{x}"), NextHopAddress::Ipv6(x) => write!(f, "{x}"), NextHopAddress::Ipv6LinkLocal(x, y) => write!(f, "Ipv6LinkLocal({x}, {y})"), + NextHopAddress::VpnIpv6(rd, x) => write!(f, "VpnIpv6({rd}, {x})"), + NextHopAddress::VpnIpv6LinkLocal(rd1, x, rd2, y) => { + write!(f, "VpnIpv6LinkLocal({rd1}, {x}, {rd2}, {y})") + } } } } @@ -49,6 +90,8 @@ impl Display for NextHopAddress { NextHopAddress::Ipv4(v) => write!(f, "{v}"), NextHopAddress::Ipv6(v) => write!(f, "{v}"), NextHopAddress::Ipv6LinkLocal(v, _) => write!(f, "{v}"), + NextHopAddress::VpnIpv6(_, v) => write!(f, "{v}"), + NextHopAddress::VpnIpv6LinkLocal(_, v, _, _) => write!(f, "{v}"), } } } @@ -160,4 +203,44 @@ mod tests { assert_eq!(format!("{next_hop_ipv6}"), "2001:db8::1"); assert_eq!(format!("{next_hop_ipv6_link_local}"), "fe80::"); } + + #[test] + fn test_route_distinguisher() { + let rd = RouteDistinguisher([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]); + + // Test Debug format + assert_eq!(format!("{rd:?}"), "RD(00:01:02:03:04:05:06:07)"); + + // Test Display format + assert_eq!(format!("{rd}"), "00:01:02:03:04:05:06:07"); + } + + #[test] + fn test_vpn_next_hop_address() { + let rd = RouteDistinguisher([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]); + let ipv6_addr = Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1); + let ipv6_link_local = Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1); + let rd2 = RouteDistinguisher([0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17]); + + // Test VpnIpv6 + let vpn_next_hop = NextHopAddress::VpnIpv6(rd, ipv6_addr); + assert_eq!(vpn_next_hop.addr(), IpAddr::V6(ipv6_addr)); + assert!(!vpn_next_hop.is_link_local()); + assert_eq!(format!("{vpn_next_hop}"), "2001:db8::1"); + assert_eq!( + format!("{vpn_next_hop:?}"), + "VpnIpv6(00:01:02:03:04:05:06:07, 2001:db8::1)" + ); + + // Test VpnIpv6LinkLocal + let vpn_ll_next_hop = NextHopAddress::VpnIpv6LinkLocal(rd, ipv6_addr, rd2, ipv6_link_local); + assert_eq!(vpn_ll_next_hop.addr(), IpAddr::V6(ipv6_addr)); + assert!(vpn_ll_next_hop.is_link_local()); // Should return true for VpnIpv6LinkLocal + assert_eq!(format!("{vpn_ll_next_hop}"), "2001:db8::1"); + assert_eq!(format!("{vpn_ll_next_hop:?}"), "VpnIpv6LinkLocal(00:01:02:03:04:05:06:07, 2001:db8::1, 10:11:12:13:14:15:16:17, fe80::1)"); + + // Test VpnIpv6 with link-local IP (not VpnIpv6LinkLocal variant) + let vpn_ll_ip = NextHopAddress::VpnIpv6(rd, ipv6_link_local); + assert!(vpn_ll_ip.is_link_local()); // Should detect link-local from IPv6 address + } } diff --git a/src/parser/bgp/attributes/attr_03_next_hop.rs b/src/parser/bgp/attributes/attr_03_next_hop.rs index 81224397..b3167011 100644 --- a/src/parser/bgp/attributes/attr_03_next_hop.rs +++ b/src/parser/bgp/attributes/attr_03_next_hop.rs @@ -19,10 +19,40 @@ pub fn parse_mp_next_hop(mut input: Bytes) -> Result, Par 0 => None, 4 => Some(input.read_ipv4_address().map(NextHopAddress::Ipv4)?), 16 => Some(input.read_ipv6_address().map(NextHopAddress::Ipv6)?), + // RFC 8950: VPN-IPv6 next hop (8-byte RD + 16-byte IPv6) + 24 => { + let rd_bytes = input.read_n_bytes(8)?; + let mut rd_array = [0u8; 8]; + rd_array.copy_from_slice(&rd_bytes); + let rd = RouteDistinguisher(rd_array); + let ipv6 = input.read_ipv6_address()?; + Some(NextHopAddress::VpnIpv6(rd, ipv6)) + } 32 => Some(NextHopAddress::Ipv6LinkLocal( input.read_ipv6_address()?, input.read_ipv6_address()?, )), + // RFC 8950: VPN-IPv6 next hop with link-local (8-byte RD + 16-byte IPv6 + 8-byte RD + 16-byte IPv6 link-local) + 48 => { + let rd1_bytes = input.read_n_bytes(8)?; + let mut rd1_array = [0u8; 8]; + rd1_array.copy_from_slice(&rd1_bytes); + let rd1 = RouteDistinguisher(rd1_array); + let ipv6 = input.read_ipv6_address()?; + + let rd2_bytes = input.read_n_bytes(8)?; + let mut rd2_array = [0u8; 8]; + rd2_array.copy_from_slice(&rd2_bytes); + let rd2 = RouteDistinguisher(rd2_array); + let ipv6_link_local = input.read_ipv6_address()?; + + Some(NextHopAddress::VpnIpv6LinkLocal( + rd1, + ipv6, + rd2, + ipv6_link_local, + )) + } v => { return Err(ParserError::ParseError(format!( "Invalid next hop length found: {v}" @@ -50,6 +80,22 @@ pub fn encode_mp_next_hop(n: &NextHopAddress) -> Bytes { output.extend(n2.octets().to_vec()); output.freeze() } + // RFC 8950: VPN-IPv6 next hop (24 bytes) + NextHopAddress::VpnIpv6(rd, ipv6) => { + let mut output = BytesMut::with_capacity(24); + output.extend(rd.0.to_vec()); // 8 bytes RD + output.extend(ipv6.octets().to_vec()); // 16 bytes IPv6 + output.freeze() + } + // RFC 8950: VPN-IPv6 next hop with link-local (48 bytes) + NextHopAddress::VpnIpv6LinkLocal(rd1, ipv6, rd2, ipv6_link_local) => { + let mut output = BytesMut::with_capacity(48); + output.extend(rd1.0.to_vec()); // 8 bytes RD1 + output.extend(ipv6.octets().to_vec()); // 16 bytes IPv6 + output.extend(rd2.0.to_vec()); // 8 bytes RD2 + output.extend(ipv6_link_local.octets().to_vec()); // 16 bytes IPv6 link-local + output.freeze() + } } } @@ -147,4 +193,55 @@ mod tests { let next_hop = parse_mp_next_hop(Bytes::from(vec![1])); assert!(next_hop.is_err()); } + + #[test] + fn test_parse_vpn_next_hop() { + // Test 24-byte VPN-IPv6 next hop (RFC 8950) + let vpn_ipv6_bytes = vec![ + // 8-byte Route Distinguisher + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + // 16-byte IPv6 address (2001:db8::1) + 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, + ]; + + let next_hop = parse_mp_next_hop(Bytes::from(vpn_ipv6_bytes.clone())).unwrap(); + if let Some(NextHopAddress::VpnIpv6(rd, ipv6)) = next_hop { + assert_eq!(rd.0, [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]); + assert_eq!(ipv6.to_string(), "2001:db8::1"); + } else { + panic!("Expected VpnIpv6 next hop"); + } + + // Test encoding round-trip + let encoded = encode_mp_next_hop(&next_hop.unwrap()); + assert_eq!(encoded.to_vec(), vpn_ipv6_bytes); + + // Test 48-byte VPN-IPv6 next hop with link-local (RFC 8950) + let vpn_ipv6_link_local_bytes = vec![ + // 8-byte Route Distinguisher 1 + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + // 16-byte IPv6 address (2001:db8::1) + 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, // 8-byte Route Distinguisher 2 + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + // 16-byte IPv6 link-local address (fe80::1) + 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, + ]; + + let next_hop = parse_mp_next_hop(Bytes::from(vpn_ipv6_link_local_bytes.clone())).unwrap(); + if let Some(NextHopAddress::VpnIpv6LinkLocal(rd1, ipv6, rd2, ipv6_ll)) = next_hop { + assert_eq!(rd1.0, [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]); + assert_eq!(ipv6.to_string(), "2001:db8::1"); + assert_eq!(rd2.0, [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17]); + assert_eq!(ipv6_ll.to_string(), "fe80::1"); + } else { + panic!("Expected VpnIpv6LinkLocal next hop"); + } + + // Test encoding round-trip + let encoded = encode_mp_next_hop(&next_hop.unwrap()); + assert_eq!(encoded.to_vec(), vpn_ipv6_link_local_bytes); + } } diff --git a/src/parser/bgp/attributes/attr_14_15_nlri.rs b/src/parser/bgp/attributes/attr_14_15_nlri.rs index 85a3e7ad..553b10bc 100644 --- a/src/parser/bgp/attributes/attr_14_15_nlri.rs +++ b/src/parser/bgp/attributes/attr_14_15_nlri.rs @@ -126,6 +126,20 @@ pub fn encode_nlri(nlri: &Nlri, reachable: bool) -> Bytes { ip_bytes.extend_from_slice(&ip2.octets()); ip_bytes } + // RFC 8950: VPN-IPv6 next hop (24 bytes) + NextHopAddress::VpnIpv6(rd, ip) => { + let mut ip_bytes = rd.0.to_vec(); // 8 bytes RD + ip_bytes.extend_from_slice(&ip.octets()); // 16 bytes IPv6 + ip_bytes + } + // RFC 8950: VPN-IPv6 next hop with link-local (48 bytes) + NextHopAddress::VpnIpv6LinkLocal(rd1, ip1, rd2, ip2) => { + let mut ip_bytes = rd1.0.to_vec(); // 8 bytes RD1 + ip_bytes.extend_from_slice(&ip1.octets()); // 16 bytes IPv6 + ip_bytes.extend_from_slice(&rd2.0); // 8 bytes RD2 + ip_bytes.extend_from_slice(&ip2.octets()); // 16 bytes IPv6 link-local + ip_bytes + } }; bytes.put_u8(next_hop_bytes.len() as u8); bytes.put_slice(&next_hop_bytes); diff --git a/src/parser/mrt/mrt_elem.rs b/src/parser/mrt/mrt_elem.rs index 779b5b81..e8890214 100644 --- a/src/parser/mrt/mrt_elem.rs +++ b/src/parser/mrt/mrt_elem.rs @@ -125,6 +125,9 @@ fn get_relevant_attributes( NextHopAddress::Ipv4(v) => IpAddr::from(*v), NextHopAddress::Ipv6(v) => IpAddr::from(*v), NextHopAddress::Ipv6LinkLocal(v, _) => IpAddr::from(*v), + // RFC 8950: VPN next hops - return the IPv6 address part + NextHopAddress::VpnIpv6(_, v) => IpAddr::from(*v), + NextHopAddress::VpnIpv6LinkLocal(_, v, _, _) => IpAddr::from(*v), }) }) }); @@ -420,6 +423,13 @@ impl Elementor { NextHopAddress::Ipv6LinkLocal(v, _) => { Some(IpAddr::from(v)) } + // RFC 8950: VPN next hops - return the IPv6 address part + NextHopAddress::VpnIpv6(_, v) => { + Some(IpAddr::from(v)) + } + NextHopAddress::VpnIpv6LinkLocal(_, v, _, _) => { + Some(IpAddr::from(v)) + } } } else { None diff --git a/src/parser/utils.rs b/src/parser/utils.rs index 1d4d5f38..5a153922 100644 --- a/src/parser/utils.rs +++ b/src/parser/utils.rs @@ -496,6 +496,13 @@ mod tests { let mut buf = Bytes::from_static(&[0x02]); assert_eq!(buf.read_safi().unwrap(), Safi::Multicast); + + // RFC 8950 VPN SAFI values + let mut buf = Bytes::from_static(&[0x80]); // 128 in hex + assert_eq!(buf.read_safi().unwrap(), Safi::MplsVpn); + + let mut buf = Bytes::from_static(&[0x81]); // 129 in hex + assert_eq!(buf.read_safi().unwrap(), Safi::MulticastVpn); } #[test] From d8b52652dfa24092fa2429fea524bdd134ce52ce Mon Sep 17 00:00:00 2001 From: Mingwei Zhang Date: Sun, 3 Aug 2025 15:40:04 -0700 Subject: [PATCH 2/3] feat: implement Extended Next Hop capability parsing and encoding Implements RFC 8950 Section 3 Extended Next Hop capability negotiation for BGP OPEN messages. This enables BGP speakers to advertise their ability to receive IPv4 NLRI with IPv6 next-hops and other cross-protocol combinations. Key features: - ExtendedNextHopCapability struct with parsing/encoding support - Integration with BGP OPEN message capability parsing infrastructure - Comprehensive validation and error handling with fallback to raw bytes - Helper methods: supports() and supported_nlri_for_nexthop() - Full test coverage including round-trip and integration tests Changes: - Add ExtendedNextHopCapability and ExtendedNextHopEntry structs - Extend CapabilityValue enum to support structured capability parsing - Update BGP capability parsing to handle Extended Next Hop capabilities - Add 8 comprehensive test cases covering all functionality - Maintain backward compatibility with existing capability parsing RFC 8950 compliance: - Supports arbitrary AFI/SAFI combinations per specification - Validates 6-byte entry format (AFI + SAFI + AFI) - Handles SAFI encoding differences (2 bytes in capability vs 1 byte normally) --- src/models/bgp/capabilities.rs | 255 +++++++++++++++++++++++++++++++++ src/models/bgp/mod.rs | 14 +- src/parser/bgp/messages.rs | 160 ++++++++++++++++++++- 3 files changed, 422 insertions(+), 7 deletions(-) diff --git a/src/models/bgp/capabilities.rs b/src/models/bgp/capabilities.rs index dd2ac3c4..b8a5aff2 100644 --- a/src/models/bgp/capabilities.rs +++ b/src/models/bgp/capabilities.rs @@ -1,3 +1,7 @@ +use crate::models::network::{Afi, Safi}; +use crate::parser::ReadUtils; +use crate::ParserError; +use bytes::{BufMut, Bytes, BytesMut}; use num_enum::{FromPrimitive, IntoPrimitive}; #[allow(non_camel_case_types)] @@ -45,6 +49,102 @@ impl BgpCapabilityType { } } +/// Extended Next Hop capability entry - RFC 8950, Section 3 +/// Represents a single triple +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ExtendedNextHopEntry { + /// Address Family Identifier for NLRI (typically AFI=1 for IPv4) + pub nlri_afi: Afi, + /// Subsequent Address Family Identifier for NLRI (1, 2, 4, 128, 129 per RFC 8950) + pub nlri_safi: Safi, + /// Address Family Identifier for Next Hop (typically AFI=2 for IPv6) + pub nexthop_afi: Afi, +} + +/// Extended Next Hop capability - RFC 8950, Section 3 +/// Contains a list of supported NLRI/NextHop AFI/SAFI combinations +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ExtendedNextHopCapability { + /// List of supported AFI/SAFI combinations for extended next hop encoding + pub entries: Vec, +} + +impl ExtendedNextHopCapability { + /// Create a new Extended Next Hop capability with the given entries + pub fn new(entries: Vec) -> Self { + Self { entries } + } + + /// Check if this capability supports a specific NLRI AFI/SAFI with NextHop AFI combination + pub fn supports(&self, nlri_afi: Afi, nlri_safi: Safi, nexthop_afi: Afi) -> bool { + self.entries.iter().any(|entry| { + entry.nlri_afi == nlri_afi + && entry.nlri_safi == nlri_safi + && entry.nexthop_afi == nexthop_afi + }) + } + + /// Get all supported NLRI AFI/SAFI combinations for a given NextHop AFI + pub fn supported_nlri_for_nexthop(&self, nexthop_afi: Afi) -> Vec<(Afi, Safi)> { + self.entries + .iter() + .filter(|entry| entry.nexthop_afi == nexthop_afi) + .map(|entry| (entry.nlri_afi, entry.nlri_safi)) + .collect() + } + + /// Parse Extended Next Hop capability from raw bytes - RFC 8950, Section 3 + /// + /// Format: Series of 6-byte entries, each containing: + /// - NLRI AFI (2 bytes) + /// - NLRI SAFI (2 bytes) + /// - NextHop AFI (2 bytes) + pub fn parse(mut data: Bytes) -> Result { + let mut entries = Vec::new(); + + // Each entry is 6 bytes (2 + 2 + 2) + if data.len() % 6 != 0 { + return Err(ParserError::ParseError(format!( + "Extended Next Hop capability length {} is not divisible by 6", + data.len() + ))); + } + + while data.len() >= 6 { + let nlri_afi = data.read_afi()?; + // SAFI is encoded as 2 bytes in capability, but SAFI enum is u8 + let nlri_safi_u16 = data.read_u16()?; + let nlri_safi = Safi::try_from(nlri_safi_u16 as u8).map_err(|_| { + ParserError::ParseError(format!("Unknown SAFI type: {}", nlri_safi_u16)) + })?; + let nexthop_afi = data.read_afi()?; + + entries.push(ExtendedNextHopEntry { + nlri_afi, + nlri_safi, + nexthop_afi, + }); + } + + Ok(ExtendedNextHopCapability::new(entries)) + } + + /// Encode Extended Next Hop capability to raw bytes - RFC 8950, Section 3 + pub fn encode(&self) -> Bytes { + let mut bytes = BytesMut::with_capacity(self.entries.len() * 6); + + for entry in &self.entries { + bytes.put_u16(entry.nlri_afi as u16); // NLRI AFI (2 bytes) + bytes.put_u16(entry.nlri_safi as u8 as u16); // NLRI SAFI (2 bytes in capability, but SAFI is u8) + bytes.put_u16(entry.nexthop_afi as u16); // NextHop AFI (2 bytes) + } + + bytes.freeze() + } +} + #[cfg(test)] mod tests { use super::*; @@ -155,4 +255,159 @@ mod tests { let deserialized: BgpCapabilityType = serde_json::from_str(&serialized).unwrap(); assert_eq!(ty, deserialized); } + + #[test] + fn test_extended_next_hop_capability() { + use crate::models::network::{Afi, Safi}; + + // Create capability with RFC 8950 standard combinations + let entries = vec![ + ExtendedNextHopEntry { + nlri_afi: Afi::Ipv4, + nlri_safi: Safi::Unicast, + nexthop_afi: Afi::Ipv6, + }, + ExtendedNextHopEntry { + nlri_afi: Afi::Ipv4, + nlri_safi: Safi::MplsVpn, + nexthop_afi: Afi::Ipv6, + }, + ]; + + let capability = ExtendedNextHopCapability::new(entries); + + // Test supports() method + assert!(capability.supports(Afi::Ipv4, Safi::Unicast, Afi::Ipv6)); + assert!(capability.supports(Afi::Ipv4, Safi::MplsVpn, Afi::Ipv6)); + assert!(!capability.supports(Afi::Ipv4, Safi::Multicast, Afi::Ipv6)); + assert!(!capability.supports(Afi::Ipv6, Safi::Unicast, Afi::Ipv6)); + + // Test supported_nlri_for_nexthop() method + let supported = capability.supported_nlri_for_nexthop(Afi::Ipv6); + assert_eq!(supported.len(), 2); + assert!(supported.contains(&(Afi::Ipv4, Safi::Unicast))); + assert!(supported.contains(&(Afi::Ipv4, Safi::MplsVpn))); + + let no_support = capability.supported_nlri_for_nexthop(Afi::Ipv4); + assert!(no_support.is_empty()); + } + + #[test] + fn test_extended_next_hop_capability_parsing() { + use crate::models::network::{Afi, Safi}; + + // Test parsing valid capability data + // Entry 1: IPv4 Unicast (AFI=1, SAFI=1) with IPv6 NextHop (AFI=2) + // Entry 2: IPv4 MPLS VPN (AFI=1, SAFI=128) with IPv6 NextHop (AFI=2) + let capability_bytes = Bytes::from(vec![ + 0x00, 0x01, // NLRI AFI = 1 (IPv4) + 0x00, 0x01, // NLRI SAFI = 1 (Unicast) + 0x00, 0x02, // NextHop AFI = 2 (IPv6) + 0x00, 0x01, // NLRI AFI = 1 (IPv4) + 0x00, 0x80, // NLRI SAFI = 128 (MPLS VPN) + 0x00, 0x02, // NextHop AFI = 2 (IPv6) + ]); + + let parsed = ExtendedNextHopCapability::parse(capability_bytes).unwrap(); + + assert_eq!(parsed.entries.len(), 2); + + // Check first entry + assert_eq!(parsed.entries[0].nlri_afi, Afi::Ipv4); + assert_eq!(parsed.entries[0].nlri_safi, Safi::Unicast); + assert_eq!(parsed.entries[0].nexthop_afi, Afi::Ipv6); + + // Check second entry + assert_eq!(parsed.entries[1].nlri_afi, Afi::Ipv4); + assert_eq!(parsed.entries[1].nlri_safi, Safi::MplsVpn); + assert_eq!(parsed.entries[1].nexthop_afi, Afi::Ipv6); + + // Test functionality + assert!(parsed.supports(Afi::Ipv4, Safi::Unicast, Afi::Ipv6)); + assert!(parsed.supports(Afi::Ipv4, Safi::MplsVpn, Afi::Ipv6)); + assert!(!parsed.supports(Afi::Ipv4, Safi::Multicast, Afi::Ipv6)); + } + + #[test] + fn test_extended_next_hop_capability_encoding() { + use crate::models::network::{Afi, Safi}; + + let entries = vec![ + ExtendedNextHopEntry { + nlri_afi: Afi::Ipv4, + nlri_safi: Safi::Unicast, + nexthop_afi: Afi::Ipv6, + }, + ExtendedNextHopEntry { + nlri_afi: Afi::Ipv4, + nlri_safi: Safi::MplsVpn, + nexthop_afi: Afi::Ipv6, + }, + ]; + + let capability = ExtendedNextHopCapability::new(entries); + let encoded = capability.encode(); + + let expected = vec![ + 0x00, 0x01, // NLRI AFI = 1 (IPv4) + 0x00, 0x01, // NLRI SAFI = 1 (Unicast) + 0x00, 0x02, // NextHop AFI = 2 (IPv6) + 0x00, 0x01, // NLRI AFI = 1 (IPv4) + 0x00, 0x80, // NLRI SAFI = 128 (MPLS VPN) + 0x00, 0x02, // NextHop AFI = 2 (IPv6) + ]; + + assert_eq!(encoded.to_vec(), expected); + } + + #[test] + fn test_extended_next_hop_capability_round_trip() { + use crate::models::network::{Afi, Safi}; + + let original_entries = vec![ + ExtendedNextHopEntry { + nlri_afi: Afi::Ipv4, + nlri_safi: Safi::Unicast, + nexthop_afi: Afi::Ipv6, + }, + ExtendedNextHopEntry { + nlri_afi: Afi::Ipv4, + nlri_safi: Safi::MulticastVpn, + nexthop_afi: Afi::Ipv6, + }, + ]; + + let original = ExtendedNextHopCapability::new(original_entries); + let encoded = original.encode(); + let parsed = ExtendedNextHopCapability::parse(encoded).unwrap(); + + assert_eq!(original, parsed); + } + + #[test] + fn test_extended_next_hop_capability_invalid_length() { + // Test with invalid length (not divisible by 6) + let invalid_bytes = Bytes::from(vec![0x00, 0x01, 0x00, 0x01, 0x00]); // 5 bytes + let result = ExtendedNextHopCapability::parse(invalid_bytes); + assert!(result.is_err()); + + if let Err(ParserError::ParseError(msg)) = result { + assert!(msg.contains("not divisible by 6")); + } else { + panic!("Expected ParseError with divisibility message"); + } + } + + #[test] + fn test_extended_next_hop_capability_empty() { + // Test with empty capability (valid - no entries) + let empty_bytes = Bytes::from(vec![]); + let parsed = ExtendedNextHopCapability::parse(empty_bytes).unwrap(); + assert_eq!(parsed.entries.len(), 0); + + // Test encoding empty capability + let empty_capability = ExtendedNextHopCapability::new(vec![]); + let encoded = empty_capability.encode(); + assert_eq!(encoded.len(), 0); + } } diff --git a/src/models/bgp/mod.rs b/src/models/bgp/mod.rs index f1e322e8..64712123 100644 --- a/src/models/bgp/mod.rs +++ b/src/models/bgp/mod.rs @@ -14,7 +14,7 @@ pub use error::*; pub use role::*; use crate::models::network::*; -use capabilities::BgpCapabilityType; +use capabilities::{BgpCapabilityType, ExtendedNextHopCapability}; use num_enum::{IntoPrimitive, TryFromPrimitive}; use std::net::Ipv4Addr; @@ -106,7 +106,17 @@ pub enum ParamValue { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Capability { pub ty: BgpCapabilityType, - pub value: Vec, + pub value: CapabilityValue, +} + +/// Parsed BGP capability values - RFC 8950, Section 3 +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum CapabilityValue { + /// Raw unparsed capability data + Raw(Vec), + /// Extended Next Hop capability - RFC 8950, Section 3 + ExtendedNextHop(ExtendedNextHopCapability), } #[derive(Debug, Clone, PartialEq, Eq, Default)] diff --git a/src/parser/bgp/messages.rs b/src/parser/bgp/messages.rs index b15cb07b..f3a9da59 100644 --- a/src/parser/bgp/messages.rs +++ b/src/parser/bgp/messages.rs @@ -3,7 +3,7 @@ use bytes::{Buf, BufMut, Bytes, BytesMut}; use std::convert::TryFrom; use crate::error::ParserError; -use crate::models::capabilities::BgpCapabilityType; +use crate::models::capabilities::{BgpCapabilityType, ExtendedNextHopCapability}; use crate::models::error::BgpError; use crate::parser::bgp::attributes::parse_attributes; use crate::parser::{encode_ipaddr, encode_nlri_prefixes, parse_nlri_list, ReadUtils}; @@ -197,9 +197,27 @@ pub fn parse_bgp_open_message(input: &mut Bytes) -> Result input.read_u8()? as u16, }; + let capability_data = input.read_n_bytes(len as usize)?; + let capability_type = BgpCapabilityType::from(code); + + // Parse specific capability types - RFC 8950, Section 3 + let capability_value = match capability_type { + BgpCapabilityType::EXTENDED_NEXT_HOP_ENCODING => { + match ExtendedNextHopCapability::parse(Bytes::from(capability_data.clone())) + { + Ok(parsed) => CapabilityValue::ExtendedNextHop(parsed), + Err(_) => { + // Fall back to raw bytes if parsing fails + CapabilityValue::Raw(capability_data) + } + } + } + _ => CapabilityValue::Raw(capability_data), + }; + ParamValue::Capability(Capability { - ty: BgpCapabilityType::from(code), - value: input.read_n_bytes(len as usize)?, + ty: capability_type, + value: capability_value, }) } _ => { @@ -239,8 +257,12 @@ impl BgpOpenMessage { match ¶m.param_value { ParamValue::Capability(cap) => { buf.put_u8(cap.ty.into()); - buf.put_u8(cap.value.len() as u8); - buf.extend(&cap.value); + let encoded_value = match &cap.value { + CapabilityValue::ExtendedNextHop(enh) => enh.encode(), + CapabilityValue::Raw(raw) => Bytes::from(raw.clone()), + }; + buf.put_u8(encoded_value.len() as u8); + buf.extend(&encoded_value); } ParamValue::Raw(bytes) => { buf.extend(bytes); @@ -644,4 +666,132 @@ mod tests { let msg = BgpMessage::from(BgpUpdateMessage::default()); assert!(matches!(msg, BgpMessage::Update(_))); } + + #[test] + fn test_parse_bgp_open_message_with_extended_next_hop_capability() { + use crate::models::{Afi, Safi}; + + // BGP OPEN message with Extended Next Hop capability - RFC 8950, Section 3 + // Version=4, ASN=65001, HoldTime=180, BGP-ID=192.0.2.1 + // One capability: Extended Next Hop (type=5) with two entries: + // 1) IPv4 Unicast (AFI=1, SAFI=1) can use IPv6 NextHop (AFI=2) + // 2) IPv4 MPLS VPN (AFI=1, SAFI=128) can use IPv6 NextHop (AFI=2) + let bytes = Bytes::from(vec![ + 0x04, // version + 0xfd, 0xe9, // asn = 65001 + 0x00, 0xb4, // hold time = 180 + 0xc0, 0x00, 0x02, 0x01, // sender ip = 192.0.2.1 + 0x10, // opt params length = 16 + 0x02, // param type = 2 (capability) + 0x0e, // param length = 14 + 0x05, // capability type = 5 (Extended Next Hop) + 0x0c, // capability length = 12 (2 entries * 6 bytes each) + 0x00, 0x01, // NLRI AFI = 1 (IPv4) + 0x00, 0x01, // NLRI SAFI = 1 (Unicast) + 0x00, 0x02, // NextHop AFI = 2 (IPv6) + 0x00, 0x01, // NLRI AFI = 1 (IPv4) - second entry + 0x00, 0x80, // NLRI SAFI = 128 (MPLS VPN) + 0x00, 0x02, // NextHop AFI = 2 (IPv6) + ]); + + let msg = parse_bgp_open_message(&mut bytes.clone()).unwrap(); + assert_eq!(msg.version, 4); + assert_eq!(msg.asn, Asn::new_16bit(65001)); + assert_eq!(msg.hold_time, 180); + assert_eq!(msg.sender_ip, Ipv4Addr::new(192, 0, 2, 1)); + assert!(!msg.extended_length); + assert_eq!(msg.opt_params.len(), 1); + + // Check the capability + if let ParamValue::Capability(cap) = &msg.opt_params[0].param_value { + assert_eq!(cap.ty, BgpCapabilityType::EXTENDED_NEXT_HOP_ENCODING); + + if let CapabilityValue::ExtendedNextHop(enh_cap) = &cap.value { + assert_eq!(enh_cap.entries.len(), 2); + + // Check first entry: IPv4 Unicast can use IPv6 NextHop + let entry1 = &enh_cap.entries[0]; + assert_eq!(entry1.nlri_afi, Afi::Ipv4); + assert_eq!(entry1.nlri_safi, Safi::Unicast); + assert_eq!(entry1.nexthop_afi, Afi::Ipv6); + + // Check second entry: IPv4 MPLS VPN can use IPv6 NextHop + let entry2 = &enh_cap.entries[1]; + assert_eq!(entry2.nlri_afi, Afi::Ipv4); + assert_eq!(entry2.nlri_safi, Safi::MplsVpn); + assert_eq!(entry2.nexthop_afi, Afi::Ipv6); + + // Test functionality + assert!(enh_cap.supports(Afi::Ipv4, Safi::Unicast, Afi::Ipv6)); + assert!(enh_cap.supports(Afi::Ipv4, Safi::MplsVpn, Afi::Ipv6)); + assert!(!enh_cap.supports(Afi::Ipv4, Safi::Multicast, Afi::Ipv6)); + } else { + panic!("Expected ExtendedNextHop capability value"); + } + } else { + panic!("Expected capability parameter"); + } + } + + #[test] + fn test_encode_bgp_open_message_with_extended_next_hop_capability() { + use crate::models::capabilities::{ExtendedNextHopCapability, ExtendedNextHopEntry}; + use crate::models::{Afi, Safi}; + + // Create Extended Next Hop capability + let entries = vec![ + ExtendedNextHopEntry { + nlri_afi: Afi::Ipv4, + nlri_safi: Safi::Unicast, + nexthop_afi: Afi::Ipv6, + }, + ExtendedNextHopEntry { + nlri_afi: Afi::Ipv4, + nlri_safi: Safi::MplsVpn, + nexthop_afi: Afi::Ipv6, + }, + ]; + let enh_capability = ExtendedNextHopCapability::new(entries); + + let msg = BgpOpenMessage { + version: 4, + asn: Asn::new_16bit(65001), + hold_time: 180, + sender_ip: Ipv4Addr::new(192, 0, 2, 1), + extended_length: false, + opt_params: vec![OptParam { + param_type: 2, // capability + param_len: 14, // 1 (type) + 1 (len) + 12 (2 entries * 6 bytes) + param_value: ParamValue::Capability(Capability { + ty: BgpCapabilityType::EXTENDED_NEXT_HOP_ENCODING, + value: CapabilityValue::ExtendedNextHop(enh_capability), + }), + }], + }; + + let encoded = msg.encode(); + + // Parse the encoded message back and verify it matches + let parsed = parse_bgp_open_message(&mut encoded.clone()).unwrap(); + assert_eq!(parsed.version, msg.version); + assert_eq!(parsed.asn, msg.asn); + assert_eq!(parsed.hold_time, msg.hold_time); + assert_eq!(parsed.sender_ip, msg.sender_ip); + assert_eq!(parsed.extended_length, msg.extended_length); + assert_eq!(parsed.opt_params.len(), 1); + + // Verify the capability was encoded and parsed correctly + if let ParamValue::Capability(cap) = &parsed.opt_params[0].param_value { + assert_eq!(cap.ty, BgpCapabilityType::EXTENDED_NEXT_HOP_ENCODING); + if let CapabilityValue::ExtendedNextHop(enh_cap) = &cap.value { + assert_eq!(enh_cap.entries.len(), 2); + assert!(enh_cap.supports(Afi::Ipv4, Safi::Unicast, Afi::Ipv6)); + assert!(enh_cap.supports(Afi::Ipv4, Safi::MplsVpn, Afi::Ipv6)); + } else { + panic!("Expected ExtendedNextHop capability value after round trip"); + } + } else { + panic!("Expected capability parameter after round trip"); + } + } } From 56f12d49cea88e8bb37400f6e640056ef0625af7 Mon Sep 17 00:00:00 2001 From: Mingwei Zhang Date: Sun, 3 Aug 2025 15:42:42 -0700 Subject: [PATCH 3/3] docs: update CHANGELOG.md for Extended Next Hop capability implementation Documents the RFC 8950 Extended Next Hop capability parsing and encoding feature added in the previous commit, including structured capability parsing, BGP OPEN message integration, and comprehensive validation. --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69f8592b..c208d745 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,12 @@ All notable changes to this project will be documented in this file. * extended `NextHopAddress` enum with `VpnIpv6` and `VpnIpv6LinkLocal` variants * updated `parse_mp_next_hop` to handle 24-byte and 48-byte VPN next-hops * enables parsing of VPN-IPv4 routes with VPN-IPv6 next-hops per RFC 8950 Section 4 +* added RFC 8950 Extended Next Hop capability parsing and encoding + * implemented `ExtendedNextHopCapability` for BGP OPEN message capability negotiation + * supports arbitrary AFI/SAFI combinations per RFC 8950 Section 3 + * added structured capability parsing with fallback to raw bytes for compatibility + * enables BGP speakers to advertise ability to receive cross-protocol next-hops + * includes comprehensive validation and helper methods for capability querying * added RFC 9069 validation warnings for BMP Local RIB peer types * validates Local RIB peer header requirements including zero-filled peer address and 4-byte ASN encoding * validates Local RIB peer up notifications for fabricated OPEN messages and VrTableName TLV presence