diff --git a/CHANGELOG.md b/CHANGELOG.md index c9c152d4..c208d745 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,18 @@ 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 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 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/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/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/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"); + } + } } 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]