Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
255 changes: 255 additions & 0 deletions src/models/bgp/capabilities.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -45,6 +49,102 @@ impl BgpCapabilityType {
}
}

/// Extended Next Hop capability entry - RFC 8950, Section 3
/// Represents a single <NLRI AFI, NLRI SAFI, NextHop AFI> 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<ExtendedNextHopEntry>,
}

impl ExtendedNextHopCapability {
/// Create a new Extended Next Hop capability with the given entries
pub fn new(entries: Vec<ExtendedNextHopEntry>) -> 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<Self, ParserError> {
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::*;
Expand Down Expand Up @@ -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);
}
}
14 changes: 12 additions & 2 deletions src/models/bgp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<u8>,
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<u8>),
/// Extended Next Hop capability - RFC 8950, Section 3
ExtendedNextHop(ExtendedNextHopCapability),
}

#[derive(Debug, Clone, PartialEq, Eq, Default)]
Expand Down
31 changes: 30 additions & 1 deletion src/models/network/afi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,27 @@ impl From<IpAddr> 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)]
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)]
Expand All @@ -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]
Expand Down Expand Up @@ -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);
}
}
Loading
Loading