From 5ba29c3587cbb4590ce5f3d0a3d1697c1bd96cc0 Mon Sep 17 00:00:00 2001 From: Mingwei Zhang Date: Mon, 4 Aug 2025 21:49:57 -0700 Subject: [PATCH 1/4] feat: add RFC 8955/8956 BGP Flow Specification parsing support This commit implements Flow-Spec NLRI parsing and extended communities support following RFC 8955 (IPv4 Flow-Spec), RFC 8956 (IPv6 Flow-Spec), and RFC 9117 (Flow-Spec validation procedures). Key additions: - FlowSpecNlri data structure with 13 component types - Numeric and bitmask operators for traffic matching rules - Flow-Spec Extended Communities (rate limiting, marking, redirect) - IPv6-specific Flow-Spec components with prefix offset support - SAFI 133 (Flow-Spec) and SAFI 134 (L3VPN Flow-Spec) definitions - Custom Eq implementation for f32-containing types using bit comparison - Integration with existing NLRI and BGP attribute parsing The implementation focuses on data structure representation and parsing without packet evaluation logic, making it suitable for BGP message processing and analysis tools. All 22 Flow-Spec tests pass and BGP attributes maintain HashMap/HashSet compatibility through proper Eq trait implementation. --- CHANGELOG.md | 9 + FLOWSPEC_IMPLEMENTATION.md | 440 ++++++++++++++++ src/models/bgp/attributes/mod.rs | 2 +- src/models/bgp/attributes/nlri.rs | 134 ++++- src/models/bgp/community.rs | 169 ++++++ src/models/bgp/flowspec/mod.rs | 195 +++++++ src/models/bgp/flowspec/nlri.rs | 487 ++++++++++++++++++ src/models/bgp/flowspec/operators.rs | 309 +++++++++++ src/models/bgp/flowspec/tests.rs | 374 ++++++++++++++ src/models/bgp/mod.rs | 4 +- src/models/mrt/mod.rs | 4 +- src/models/mrt/table_dump_v2.rs | 30 +- src/models/network/afi.rs | 22 + src/parser/bgp/attributes/attr_14_15_nlri.rs | 5 + .../attr_16_25_extended_communities.rs | 212 +++++++- src/parser/bgp/messages.rs | 5 + 16 files changed, 2381 insertions(+), 20 deletions(-) create mode 100644 FLOWSPEC_IMPLEMENTATION.md create mode 100644 src/models/bgp/flowspec/mod.rs create mode 100644 src/models/bgp/flowspec/nlri.rs create mode 100644 src/models/bgp/flowspec/operators.rs create mode 100644 src/models/bgp/flowspec/tests.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index bd42155a..8bae7eee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,15 @@ All notable changes to this project will be documented in this file. ### Code improvements +* added BGP Flow-Spec parsing support following RFC 8955 and RFC 8956 + * implemented complete Flow-Spec NLRI parsing for IPv4 and IPv6 Flow Specification rules + * added support for all Flow-Spec component types (destination/source prefix, protocol, ports, ICMP, TCP flags, packet length, DSCP, fragment, flow label) + * implemented Flow-Spec operator parsing for numeric and bitmask operations with proper semantics + * added Flow-Spec Extended Communities for Traffic Rate, Traffic Action, Redirect, and Traffic Marking (RFC 8955) + * created structured data model with `FlowSpecNlri`, `FlowSpecComponent`, and operator types + * added support for IPv6-specific components including Flow Label and prefix offset encoding + * includes test coverage with RFC example validation and round-trip encoding/decoding + * maintains backward compatibility with existing NLRI and Extended Community parsing infrastructure * added BGP Tunnel Encapsulation attribute parsing support following RFC 9012, RFC 5640, and RFC 8365 * implemented complete TLV-based parsing system for BGP Tunnel Encapsulation attribute (type 23) * added support for all IANA-defined tunnel types (VXLAN, NVGRE, GRE, SR Policy, Geneve, etc.) diff --git a/FLOWSPEC_IMPLEMENTATION.md b/FLOWSPEC_IMPLEMENTATION.md new file mode 100644 index 00000000..6972cf3d --- /dev/null +++ b/FLOWSPEC_IMPLEMENTATION.md @@ -0,0 +1,440 @@ +# Flow Specification Implementation Logic + +This document details all logic and bitwise operations involved in implementing Flow Specification (Flow-Spec) parsing according to RFC 8955, RFC 8956, and RFC 9117. + +## 1. NLRI Encoding Format + +### 1.1 Length Field Encoding + +Flow-Spec NLRI starts with a length field indicating the size of the NLRI value: + +``` +Length < 240 octets: + [1 octet length] + +Length ≥ 240 octets (Extended Length): + [0xF0 + high nibble][low byte] + + Calculation: + - actual_length = ((first_byte & 0x0F) << 8) | second_byte + - Maximum value: 4095 octets +``` + +**Implementation Logic:** +```rust +fn parse_length(data: &[u8], offset: &mut usize) -> Result { + if *offset >= data.len() { + return Err(Error::InsufficientData); + } + + let first_byte = data[*offset]; + *offset += 1; + + if first_byte < 240 { + Ok(first_byte as u16) + } else { + if *offset >= data.len() { + return Err(Error::InsufficientData); + } + let second_byte = data[*offset]; + *offset += 1; + Ok(((first_byte & 0x0F) as u16) << 8 | second_byte as u16) + } +} +``` + +### 1.2 NLRI Value Structure + +After the length field, the NLRI value consists of ordered components: + +``` +NLRI Value = [Component 1][Component 2]...[Component N] + +Component = [Type (1 octet)][Component-specific data] +``` + +**Ordering Rule:** Components MUST appear in increasing numerical order by type. Each type can appear at most once. + +## 2. Component Types and Encoding + +### 2.1 IPv4 Flow-Spec Components (RFC 8955) + +| Type | Name | Encoding | Operator Type | +|------|-------------------|------------------------------------|---------------| +| 1 | Destination Prefix| `` | N/A | +| 2 | Source Prefix | `` | N/A | +| 3 | IP Protocol | `[numeric_op, value]+` | Numeric | +| 4 | Port | `[numeric_op, value]+` | Numeric | +| 5 | Destination Port | `[numeric_op, value]+` | Numeric | +| 6 | Source Port | `[numeric_op, value]+` | Numeric | +| 7 | ICMP Type | `[numeric_op, value]+` | Numeric | +| 8 | ICMP Code | `[numeric_op, value]+` | Numeric | +| 9 | TCP Flags | `[bitmask_op, bitmask]+` | Bitmask | +| 10 | Packet Length | `[numeric_op, value]+` | Numeric | +| 11 | DSCP | `[numeric_op, value]+` | Numeric | +| 12 | Fragment | `[bitmask_op, bitmask]+` | Bitmask | + +### 2.2 IPv6 Flow-Spec Extensions (RFC 8956) + +| Type | Name | Encoding | Notes | +|------|---------------------|---------------------------------------|--------------------------| +| 1 | Destination IPv6 Prefix | `` | Supports offset matching | +| 2 | Source IPv6 Prefix | `` | Supports offset matching | +| 7 | ICMPv6 Type | `[numeric_op, value]+` | IPv6-specific | +| 8 | ICMPv6 Code | `[numeric_op, value]+` | IPv6-specific | +| 13 | Flow Label | `[numeric_op, value]+` | 20-bit IPv6 Flow Label | + +### 2.3 Prefix Encoding (Types 1 & 2) + +**IPv4 Prefix:** +``` +[length octet][prefix bits padded to octet boundary] + +Example: 192.0.2.0/24 + Length: 24 (0x18) + Prefix: 0xC0 0x00 0x02 +``` + +**IPv6 Prefix with Offset:** +``` +[length octet][offset octet][prefix bits][optional padding] + +Example: Match bits 32-63 of 2001:db8::/64 + Length: 32 (match 32 bits) + Offset: 32 (skip first 32 bits) + Prefix: 0x00 0x00 0x0D 0xB8 +``` + +## 3. Operator Encoding and Bitwise Operations + +### 3.1 Numeric Operator Format + +``` + 0 1 2 3 4 5 6 7 ++---+---+---+---+---+---+---+---+ +| e | a | len | 0 |lt |gt |eq | ++---+---+---+---+---+---+---+---+ +``` + +**Bit Definitions:** +- `e` (bit 7): End-of-list flag (1 = last operator in sequence) +- `a` (bit 6): AND flag (0 = OR with next, 1 = AND with next) +- `len` (bits 5-4): Value length encoding + - `00` = 1 octet + - `01` = 2 octets + - `10` = 4 octets + - `11` = 8 octets +- `0` (bit 3): Reserved, must be 0 +- `lt` (bit 2): Less-than comparison +- `gt` (bit 1): Greater-than comparison +- `eq` (bit 0): Equal comparison + +**Comparison Logic:** +```rust +fn evaluate_numeric(operator: u8, packet_value: u64, rule_value: u64) -> bool { + let lt = (operator & 0x04) != 0; + let gt = (operator & 0x02) != 0; + let eq = (operator & 0x01) != 0; + + match (lt, gt, eq) { + (false, false, false) => false, // 000: always false + (false, false, true) => packet_value == rule_value, // 001: == + (false, true, false) => packet_value > rule_value, // 010: > + (false, true, true) => packet_value >= rule_value, // 011: >= + (true, false, false) => packet_value < rule_value, // 100: < + (true, false, true) => packet_value <= rule_value, // 101: <= + (true, true, false) => packet_value != rule_value, // 110: != + (true, true, true) => true, // 111: always true + } +} +``` + +### 3.2 Bitmask Operator Format + +``` + 0 1 2 3 4 5 6 7 ++---+---+---+---+---+---+---+---+ +| e | a | len | 0 | 0 |not| m | ++---+---+---+---+---+---+---+---+ +``` + +**Additional Bits:** +- `not` (bit 1): Logical negation (1 = negate result) +- `m` (bit 0): Match type (0 = exact match, 1 = partial match) + +**Bitmask Logic:** +```rust +fn evaluate_bitmask(operator: u8, packet_value: u64, rule_mask: u64) -> bool { + let not_flag = (operator & 0x02) != 0; + let match_flag = (operator & 0x01) != 0; + + let result = if match_flag { + // Partial match: any bits in mask must match + (packet_value & rule_mask) != 0 + } else { + // Exact match: all bits in mask must match exactly + (packet_value & rule_mask) == rule_mask + }; + + if not_flag { !result } else { result } +} +``` + +### 3.3 Operator Sequence Evaluation + +Components with multiple operators use logical operations: + +```rust +fn evaluate_component_operators(operators: &[(u8, u64)], packet_value: u64) -> bool { + let mut result = false; + let mut current_and_chain = true; + + for (i, &(operator, rule_value)) in operators.iter().enumerate() { + let is_end = (operator & 0x80) != 0; + let is_and = (operator & 0x40) != 0; + + let match_result = if is_numeric_component { + evaluate_numeric(operator, packet_value, rule_value) + } else { + evaluate_bitmask(operator, packet_value, rule_value) + }; + + if i == 0 { + current_and_chain = match_result; + } else if is_and { + current_and_chain = current_and_chain && match_result; + } else { + result = result || current_and_chain; + current_and_chain = match_result; + } + + if is_end { + result = result || current_and_chain; + break; + } + } + + result +} +``` + +## 4. Traffic Action Extended Communities + +### 4.1 Community Type Encoding + +All Flow-Spec traffic actions use non-transitive extended communities: + +**Base Type:** `0x80` (Non-transitive, two-octet AS-specific) + +| Subtype | Name | Format | Semantic | +|---------|------------------|---------------------------|-------------------------| +| 0x06 | Traffic Rate | `AS(2) + Float(4)` | Rate limit in bytes/sec | +| 0x07 | Traffic Action | `AS(2) + Flags(4)` | Action flags | +| 0x08 | RT Redirect | `AS(2) + Target(4)` | VRF redirect target | +| 0x09 | Traffic Marking | `AS(2) + DSCP(1) + 0(3)` | DSCP marking | + +### 4.2 Traffic Rate Community + +```rust +struct TrafficRateAction { + as_number: u16, + rate_bytes_per_sec: f32, // IEEE 754 single precision +} + +// Rate of 0.0 means "discard all traffic" +// Positive rate means "limit to this rate" +``` + +### 4.3 Traffic Action Community + +```rust +struct TrafficActionFlags { + terminal: bool, // Stop processing additional flow-specs + sample: bool, // Enable traffic sampling + // Other bits reserved +} + +impl TrafficActionFlags { + fn from_bytes(bytes: [u8; 4]) -> Self { + Self { + terminal: (bytes[3] & 0x01) != 0, + sample: (bytes[3] & 0x02) != 0, + } + } +} +``` + +## 5. Parsing Implementation Logic + +### 5.1 Complete NLRI Parser + +```rust +fn parse_flowspec_nlri(data: &[u8]) -> Result { + let mut offset = 0; + let length = parse_length(data, &mut offset)?; + + let end_offset = offset + length as usize; + let mut components = Vec::new(); + let mut last_type = 0u8; + + while offset < end_offset { + let component_type = data[offset]; + offset += 1; + + // Verify ordering + if component_type <= last_type { + return Err(Error::InvalidComponentOrder); + } + last_type = component_type; + + let component = match component_type { + 1 | 2 => parse_prefix_component(data, &mut offset, component_type)?, + 3..=8 | 10..=11 => parse_numeric_component(data, &mut offset, component_type)?, + 9 | 12 => parse_bitmask_component(data, &mut offset, component_type)?, + 13 => parse_flow_label_component(data, &mut offset)?, // IPv6 only + _ => return Err(Error::UnknownComponentType(component_type)), + }; + + components.push(component); + } + + Ok(FlowSpecNlri { components }) +} +``` + +### 5.2 Component-Specific Parsers + +```rust +fn parse_numeric_component(data: &[u8], offset: &mut usize, type_val: u8) -> Result { + let mut operators = Vec::new(); + + loop { + if *offset >= data.len() { + return Err(Error::InsufficientData); + } + + let operator = data[*offset]; + *offset += 1; + + let value_len = match (operator >> 4) & 0x03 { + 0 => 1, + 1 => 2, + 2 => 4, + 3 => 8, + _ => unreachable!(), + }; + + if *offset + value_len > data.len() { + return Err(Error::InsufficientData); + } + + let value = read_value(&data[*offset..*offset + value_len]); + *offset += value_len; + + let is_end = (operator & 0x80) != 0; + operators.push((operator, value)); + + if is_end { + break; + } + } + + Ok(Component::Numeric { type_val, operators }) +} +``` + +## 6. Validation Rules (RFC 9117) + +### 6.1 Flow-Spec Route Validation + +A Flow Specification route is valid if ONE of these conditions is met: + +1. **Originator Match:** The originator of the Flow-Spec route matches the originator of the best-match unicast route for the destination prefix. + +2. **AS_PATH Validation:** The AS_PATH attribute is either: + - Empty (local route), OR + - Contains only AS_CONFED_SEQUENCE segments + +### 6.2 Implementation Logic + +```rust +fn validate_flowspec_route( + flowspec_route: &FlowSpecRoute, + unicast_rib: &UnicastRib, +) -> ValidationResult { + // Extract destination prefix from Flow-Spec NLRI + let dest_prefix = extract_destination_prefix(&flowspec_route.nlri)?; + + // Find best-match unicast route + let best_unicast = unicast_rib.longest_prefix_match(&dest_prefix)?; + + // Check originator match + if flowspec_route.originator == best_unicast.originator { + return ValidationResult::Valid; + } + + // Check AS_PATH validation + match &flowspec_route.as_path { + AsPath::Empty => ValidationResult::Valid, + AsPath::Segments(segments) => { + if segments.iter().all(|seg| matches!(seg, AsPathSegment::ConfedSequence(_))) { + ValidationResult::Valid + } else { + ValidationResult::Invalid + } + } + } +} +``` + +## 7. Data Structures + +### 7.1 Core Flow-Spec Types + +```rust +#[derive(Debug, Clone, PartialEq)] +pub struct FlowSpecNlri { + pub components: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum FlowSpecComponent { + DestinationPrefix(NetworkPrefix), + SourcePrefix(NetworkPrefix), + IpProtocol(Vec), + Port(Vec), + DestinationPort(Vec), + SourcePort(Vec), + IcmpType(Vec), + IcmpCode(Vec), + TcpFlags(Vec), + PacketLength(Vec), + Dscp(Vec), + Fragment(Vec), + // IPv6-specific + FlowLabel(Vec), +} + +#[derive(Debug, Clone, PartialEq)] +pub struct NumericOperator { + pub end_of_list: bool, + pub and_with_next: bool, + pub value_length: u8, + pub less_than: bool, + pub greater_than: bool, + pub equal: bool, + pub value: u64, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct BitmaskOperator { + pub end_of_list: bool, + pub and_with_next: bool, + pub value_length: u8, + pub not: bool, + pub match_flag: bool, + pub bitmask: u64, +} +``` + +This implementation logic covers all aspects of Flow-Spec parsing including bitwise operations, component encoding, operator evaluation, and validation procedures as specified in the RFCs. \ No newline at end of file diff --git a/src/models/bgp/attributes/mod.rs b/src/models/bgp/attributes/mod.rs index 6cfd58e4..3a7a266f 100644 --- a/src/models/bgp/attributes/mod.rs +++ b/src/models/bgp/attributes/mod.rs @@ -134,7 +134,7 @@ pub fn get_deprecated_attr_type(attr_type: u8) -> Option<&'static str> { } /// Convenience wrapper for a list of attributes -#[derive(Debug, PartialEq, Eq, Clone, Default)] +#[derive(Debug, PartialEq, Clone, Default, Eq)] pub struct Attributes { // Black box type to allow for later changes/optimizations. The most common attributes could be // added as fields to allow for easier lookup. diff --git a/src/models/bgp/attributes/nlri.rs b/src/models/bgp/attributes/nlri.rs index 26d50220..500eaa5f 100644 --- a/src/models/bgp/attributes/nlri.rs +++ b/src/models/bgp/attributes/nlri.rs @@ -14,6 +14,8 @@ pub struct Nlri { pub prefixes: Vec, /// Link-State NLRI data - RFC 7752 pub link_state_nlris: Option>, + /// Flow-Spec NLRI data - RFC 8955/8956 + pub flowspec_nlris: Option>, } impl Nlri { @@ -32,6 +34,11 @@ impl Nlri { matches!(self.afi, Afi::LinkState) } + /// Returns true if this NLRI refers to Flow-Spec information. + pub const fn is_flowspec(&self) -> bool { + matches!(self.safi, Safi::FlowSpec | Safi::FlowSpecL3Vpn) + } + /// Returns true if this NLRI refers to reachable prefixes pub const fn is_reachable(&self) -> bool { self.next_hop.is_some() @@ -60,6 +67,7 @@ impl Nlri { next_hop, prefixes: vec![prefix], link_state_nlris: None, + flowspec_nlris: None, } } @@ -75,6 +83,7 @@ impl Nlri { next_hop: None, prefixes: vec![prefix], link_state_nlris: None, + flowspec_nlris: None, } } @@ -90,6 +99,7 @@ impl Nlri { next_hop, prefixes: Vec::new(), link_state_nlris: Some(nlri_list), + flowspec_nlris: None, } } @@ -103,6 +113,41 @@ impl Nlri { next_hop: None, prefixes: Vec::new(), link_state_nlris: Some(nlri_list), + flowspec_nlris: None, + } + } + + /// Create a new Flow-Spec reachable NLRI + pub fn new_flowspec_reachable( + afi: Afi, + safi: Safi, + next_hop: Option, + flowspec_nlris: Vec, + ) -> Nlri { + let next_hop = next_hop.map(NextHopAddress::from); + Nlri { + afi, + safi, + next_hop, + prefixes: Vec::new(), + link_state_nlris: None, + flowspec_nlris: Some(flowspec_nlris), + } + } + + /// Create a new Flow-Spec unreachable NLRI + pub fn new_flowspec_unreachable( + afi: Afi, + safi: Safi, + flowspec_nlris: Vec, + ) -> Nlri { + Nlri { + afi, + safi, + next_hop: None, + prefixes: Vec::new(), + link_state_nlris: None, + flowspec_nlris: Some(flowspec_nlris), } } } @@ -133,7 +178,7 @@ impl<'a> IntoIterator for &'a Nlri { } } -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct MpReachableNlri { afi: Afi, @@ -158,7 +203,7 @@ impl MpReachableNlri { } } -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct MpUnreachableNlri { afi: Afi, @@ -292,4 +337,89 @@ mod tests { let _ = nlri.next_hop_addr(); } + + #[test] + fn nlri_flowspec_creation() { + use crate::models::bgp::flowspec::{FlowSpecComponent, FlowSpecNlri}; + use std::str::FromStr; + + let component = + FlowSpecComponent::DestinationPrefix(NetworkPrefix::from_str("192.0.2.0/24").unwrap()); + let flowspec_nlri = FlowSpecNlri::new(vec![component]); + + let nlri = Nlri::new_flowspec_reachable( + Afi::Ipv4, + Safi::FlowSpec, + Some("192.0.2.1".parse().unwrap()), + vec![flowspec_nlri], + ); + + assert!(nlri.is_flowspec()); + assert!(nlri.is_reachable()); + assert!(nlri.is_ipv4()); + assert_eq!(nlri.afi, Afi::Ipv4); + assert_eq!(nlri.safi, Safi::FlowSpec); + assert_eq!(nlri.prefixes.len(), 0); // Flow-Spec doesn't use traditional prefixes + assert!(nlri.flowspec_nlris.is_some()); + assert_eq!(nlri.flowspec_nlris.as_ref().unwrap().len(), 1); + } + + #[test] + fn nlri_flowspec_unreachable() { + use crate::models::bgp::flowspec::{FlowSpecComponent, FlowSpecNlri}; + use std::str::FromStr; + + let component = + FlowSpecComponent::DestinationPrefix(NetworkPrefix::from_str("2001:db8::/32").unwrap()); + let flowspec_nlri = FlowSpecNlri::new(vec![component]); + + let nlri = Nlri::new_flowspec_unreachable(Afi::Ipv6, Safi::FlowSpec, vec![flowspec_nlri]); + + assert!(nlri.is_flowspec()); + assert!(!nlri.is_reachable()); + assert!(nlri.is_ipv6()); + assert_eq!(nlri.afi, Afi::Ipv6); + assert_eq!(nlri.safi, Safi::FlowSpec); + assert!(nlri.flowspec_nlris.is_some()); + assert_eq!(nlri.flowspec_nlris.as_ref().unwrap().len(), 1); + } + + #[test] + fn nlri_flowspec_l3vpn() { + use crate::models::bgp::flowspec::{FlowSpecComponent, FlowSpecNlri, NumericOperator}; + use std::str::FromStr; + + let components = vec![ + FlowSpecComponent::DestinationPrefix(NetworkPrefix::from_str("10.0.0.0/8").unwrap()), + FlowSpecComponent::IpProtocol(vec![NumericOperator::equal_to(6)]), // TCP + ]; + let flowspec_nlri = FlowSpecNlri::new(components); + + let nlri = Nlri::new_flowspec_reachable( + Afi::Ipv4, + Safi::FlowSpecL3Vpn, + None, // Flow-Spec often doesn't have next hop + vec![flowspec_nlri], + ); + + assert!(nlri.is_flowspec()); + assert!(!nlri.is_reachable()); + assert!(nlri.is_ipv4()); + assert_eq!(nlri.safi, Safi::FlowSpecL3Vpn); + } + + #[test] + #[should_panic] + fn nlri_flowspec_next_hop_addr_unreachable() { + use crate::models::bgp::flowspec::{FlowSpecComponent, FlowSpecNlri}; + use std::str::FromStr; + + let component = + FlowSpecComponent::DestinationPrefix(NetworkPrefix::from_str("192.0.2.0/24").unwrap()); + let flowspec_nlri = FlowSpecNlri::new(vec![component]); + + let nlri = Nlri::new_flowspec_unreachable(Afi::Ipv4, Safi::FlowSpec, vec![flowspec_nlri]); + + let _ = nlri.next_hop_addr(); + } } diff --git a/src/models/bgp/community.rs b/src/models/bgp/community.rs index c2142bc3..461511cf 100644 --- a/src/models/bgp/community.rs +++ b/src/models/bgp/community.rs @@ -102,6 +102,14 @@ pub enum ExtendedCommunity { NonTransitiveIpv4Addr(Ipv4AddrExtCommunity), NonTransitiveFourOctetAs(FourOctetAsExtCommunity), NonTransitiveOpaque(OpaqueExtCommunity), + /// Flow-Spec Traffic Rate - RFC 8955 + FlowSpecTrafficRate(FlowSpecTrafficRate), + /// Flow-Spec Traffic Action - RFC 8955 + FlowSpecTrafficAction(FlowSpecTrafficAction), + /// Flow-Spec Redirect - RFC 8955 + FlowSpecRedirect(TwoOctetAsExtCommunity), + /// Flow-Spec Traffic Marking - RFC 8955 + FlowSpecTrafficMarking(FlowSpecTrafficMarking), Raw([u8; 8]), } @@ -117,6 +125,10 @@ impl ExtendedCommunity { ExtendedCommunity::NonTransitiveIpv4Addr(_) => NonTransitiveIpv4Addr, ExtendedCommunity::NonTransitiveFourOctetAs(_) => NonTransitiveFourOctetAs, ExtendedCommunity::NonTransitiveOpaque(_) => NonTransitiveOpaque, + ExtendedCommunity::FlowSpecTrafficRate(_) => NonTransitiveTwoOctetAs, + ExtendedCommunity::FlowSpecTrafficAction(_) => NonTransitiveTwoOctetAs, + ExtendedCommunity::FlowSpecRedirect(_) => NonTransitiveTwoOctetAs, + ExtendedCommunity::FlowSpecTrafficMarking(_) => NonTransitiveTwoOctetAs, ExtendedCommunity::Raw(buffer) => Unknown(buffer[0]), } } @@ -183,6 +195,92 @@ pub struct OpaqueExtCommunity { pub value: [u8; 6], } +/// Flow-Spec Traffic Rate Extended Community +/// +/// RFC 8955 - subtype 0x06 +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct FlowSpecTrafficRate { + /// AS Number (2 octets) + pub as_number: u16, + /// Rate in bytes per second (IEEE 754 single precision float) + pub rate_bytes_per_sec: f32, +} + +impl PartialEq for FlowSpecTrafficRate { + fn eq(&self, other: &Self) -> bool { + self.as_number == other.as_number + && self.rate_bytes_per_sec.to_bits() == other.rate_bytes_per_sec.to_bits() + } +} + +impl Eq for FlowSpecTrafficRate {} + +/// Flow-Spec Traffic Action Extended Community +/// +/// RFC 8955 - subtype 0x07 +#[derive(Debug, PartialEq, Clone, Copy, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct FlowSpecTrafficAction { + /// AS Number (2 octets) + pub as_number: u16, + /// Terminal action - stop processing additional flow-specs + pub terminal: bool, + /// Sample action - enable traffic sampling + pub sample: bool, +} + +/// Flow-Spec Traffic Marking Extended Community +/// +/// RFC 8955 - subtype 0x09 +#[derive(Debug, PartialEq, Clone, Copy, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct FlowSpecTrafficMarking { + /// AS Number (2 octets) + pub as_number: u16, + /// DSCP value (6 bits) + pub dscp: u8, +} + +impl FlowSpecTrafficRate { + /// Create a new traffic rate community + pub fn new(as_number: u16, rate_bytes_per_sec: f32) -> Self { + Self { + as_number, + rate_bytes_per_sec, + } + } + + /// Create a "discard all traffic" rate (rate = 0.0) + pub fn discard(as_number: u16) -> Self { + Self { + as_number, + rate_bytes_per_sec: 0.0, + } + } +} + +impl FlowSpecTrafficAction { + /// Create a new traffic action community + pub fn new(as_number: u16, terminal: bool, sample: bool) -> Self { + Self { + as_number, + terminal, + sample, + } + } +} + +impl FlowSpecTrafficMarking { + /// Create a new traffic marking community + pub fn new(as_number: u16, dscp: u8) -> Self { + Self { + as_number, + dscp: dscp & 0x3F, + } // Mask to 6 bits + } +} + ///////////// // DISPLAY // ///////////// @@ -260,6 +358,34 @@ impl Display for ExtendedCommunity { | ExtendedCommunity::NonTransitiveOpaque(ec) => { write!(f, "{}:{}:{}", ec_type, ec.subtype, ToHexString(&ec.value)) } + ExtendedCommunity::FlowSpecTrafficRate(rate) => { + write!( + f, + "rate:{} bytes/sec (AS {})", + rate.rate_bytes_per_sec, rate.as_number + ) + } + ExtendedCommunity::FlowSpecTrafficAction(action) => { + let mut flags = Vec::new(); + if action.terminal { + flags.push("terminal"); + } + if action.sample { + flags.push("sample"); + } + write!(f, "action:{} (AS {})", flags.join(","), action.as_number) + } + ExtendedCommunity::FlowSpecRedirect(redirect) => { + write!( + f, + "redirect:AS{}:{}", + redirect.global_admin, + ToHexString(&redirect.local_admin) + ) + } + ExtendedCommunity::FlowSpecTrafficMarking(marking) => { + write!(f, "mark:DSCP{} (AS {})", marking.dscp, marking.as_number) + } ExtendedCommunity::Raw(ec) => { write!(f, "{}", ToHexString(ec)) } @@ -487,4 +613,47 @@ mod tests { let deserialized: MetaCommunity = serde_json::from_str(&serialized).unwrap(); assert_eq!(meta_community, deserialized); } + + #[test] + fn test_flowspec_traffic_rate() { + let rate = FlowSpecTrafficRate::new(64512, 1000.0); + assert_eq!(rate.as_number, 64512); + assert_eq!(rate.rate_bytes_per_sec, 1000.0); + + let discard = FlowSpecTrafficRate::discard(64512); + assert_eq!(discard.rate_bytes_per_sec, 0.0); + } + + #[test] + fn test_flowspec_traffic_action() { + let action = FlowSpecTrafficAction::new(64512, true, false); + assert_eq!(action.as_number, 64512); + assert!(action.terminal); + assert!(!action.sample); + } + + #[test] + fn test_flowspec_traffic_marking() { + let marking = FlowSpecTrafficMarking::new(64512, 46); // EF DSCP + assert_eq!(marking.as_number, 64512); + assert_eq!(marking.dscp, 46); + + // Test DSCP masking + let masked = FlowSpecTrafficMarking::new(64512, 255); + assert_eq!(masked.dscp, 63); // Should be masked to 6 bits + } + + #[test] + fn test_flowspec_community_display() { + let rate = ExtendedCommunity::FlowSpecTrafficRate(FlowSpecTrafficRate::new(64512, 1000.0)); + assert_eq!(format!("{}", rate), "rate:1000 bytes/sec (AS 64512)"); + + let action = + ExtendedCommunity::FlowSpecTrafficAction(FlowSpecTrafficAction::new(64512, true, true)); + assert_eq!(format!("{}", action), "action:terminal,sample (AS 64512)"); + + let marking = + ExtendedCommunity::FlowSpecTrafficMarking(FlowSpecTrafficMarking::new(64512, 46)); + assert_eq!(format!("{}", marking), "mark:DSCP46 (AS 64512)"); + } } diff --git a/src/models/bgp/flowspec/mod.rs b/src/models/bgp/flowspec/mod.rs new file mode 100644 index 00000000..b445f730 --- /dev/null +++ b/src/models/bgp/flowspec/mod.rs @@ -0,0 +1,195 @@ +use crate::models::*; + +pub mod nlri; +pub mod operators; + +#[cfg(test)] +mod tests; + +pub use nlri::*; +pub use operators::*; + +/// Flow Specification NLRI containing an ordered list of components +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct FlowSpecNlri { + pub components: Vec, +} + +/// Individual Flow-Spec component types as defined in RFC 8955/8956 +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum FlowSpecComponent { + /// Type 1: Destination Prefix + DestinationPrefix(NetworkPrefix), + /// Type 2: Source Prefix + SourcePrefix(NetworkPrefix), + /// Type 3: IP Protocol + IpProtocol(Vec), + /// Type 4: Port (source OR destination) + Port(Vec), + /// Type 5: Destination Port + DestinationPort(Vec), + /// Type 6: Source Port + SourcePort(Vec), + /// Type 7: ICMP Type (IPv4) / ICMPv6 Type (IPv6) + IcmpType(Vec), + /// Type 8: ICMP Code (IPv4) / ICMPv6 Code (IPv6) + IcmpCode(Vec), + /// Type 9: TCP Flags + TcpFlags(Vec), + /// Type 10: Packet Length + PacketLength(Vec), + /// Type 11: DSCP + Dscp(Vec), + /// Type 12: Fragment + Fragment(Vec), + /// Type 13: Flow Label (IPv6 only) + FlowLabel(Vec), + /// IPv6 Destination Prefix with offset + DestinationIpv6Prefix { offset: u8, prefix: NetworkPrefix }, + /// IPv6 Source Prefix with offset + SourceIpv6Prefix { offset: u8, prefix: NetworkPrefix }, +} + +impl FlowSpecComponent { + /// Get the numeric type identifier for this component + pub const fn component_type(&self) -> u8 { + match self { + FlowSpecComponent::DestinationPrefix(_) + | FlowSpecComponent::DestinationIpv6Prefix { .. } => 1, + FlowSpecComponent::SourcePrefix(_) | FlowSpecComponent::SourceIpv6Prefix { .. } => 2, + FlowSpecComponent::IpProtocol(_) => 3, + FlowSpecComponent::Port(_) => 4, + FlowSpecComponent::DestinationPort(_) => 5, + FlowSpecComponent::SourcePort(_) => 6, + FlowSpecComponent::IcmpType(_) => 7, + FlowSpecComponent::IcmpCode(_) => 8, + FlowSpecComponent::TcpFlags(_) => 9, + FlowSpecComponent::PacketLength(_) => 10, + FlowSpecComponent::Dscp(_) => 11, + FlowSpecComponent::Fragment(_) => 12, + FlowSpecComponent::FlowLabel(_) => 13, + } + } + + /// Returns true if this component uses numeric operators + pub const fn uses_numeric_operators(&self) -> bool { + matches!( + self, + FlowSpecComponent::IpProtocol(_) + | FlowSpecComponent::Port(_) + | FlowSpecComponent::DestinationPort(_) + | FlowSpecComponent::SourcePort(_) + | FlowSpecComponent::IcmpType(_) + | FlowSpecComponent::IcmpCode(_) + | FlowSpecComponent::PacketLength(_) + | FlowSpecComponent::Dscp(_) + | FlowSpecComponent::FlowLabel(_) + ) + } + + /// Returns true if this component uses bitmask operators + pub const fn uses_bitmask_operators(&self) -> bool { + matches!( + self, + FlowSpecComponent::TcpFlags(_) | FlowSpecComponent::Fragment(_) + ) + } +} + +impl FlowSpecNlri { + /// Create a new Flow-Spec NLRI with the given components + pub fn new(components: Vec) -> Self { + FlowSpecNlri { components } + } + + /// Get all components of this NLRI + pub fn components(&self) -> &[FlowSpecComponent] { + &self.components + } + + /// Check if this Flow-Spec rule matches IPv4 traffic + pub fn is_ipv4(&self) -> bool { + self.components.iter().any(|c| { + matches!(c, + FlowSpecComponent::DestinationPrefix(prefix) | + FlowSpecComponent::SourcePrefix(prefix) + if matches!(prefix.prefix, ipnet::IpNet::V4(_)) + ) + }) + } + + /// Check if this Flow-Spec rule matches IPv6 traffic + pub fn is_ipv6(&self) -> bool { + self.components.iter().any(|c| { + matches!(c, + FlowSpecComponent::DestinationPrefix(prefix) | + FlowSpecComponent::SourcePrefix(prefix) + if matches!(prefix.prefix, ipnet::IpNet::V6(_)) + ) + }) || self.components.iter().any(|c| { + matches!( + c, + FlowSpecComponent::DestinationIpv6Prefix { .. } + | FlowSpecComponent::SourceIpv6Prefix { .. } + | FlowSpecComponent::FlowLabel(_) + ) + }) + } +} + +/// Flow-Spec parsing and validation errors +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum FlowSpecError { + /// Components not in ascending type order + InvalidComponentOrder { + expected_greater_than: u8, + found: u8, + }, + /// Invalid operator encoding + InvalidOperator(u8), + /// Invalid component type + InvalidComponentType(u8), + /// Insufficient data for parsing + InsufficientData, + /// Invalid prefix encoding + InvalidPrefix, + /// Invalid value length + InvalidValueLength(u8), +} + +impl std::fmt::Display for FlowSpecError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FlowSpecError::InvalidComponentOrder { + expected_greater_than, + found, + } => { + write!( + f, + "Invalid component order: expected type > {}, but found {}", + expected_greater_than, found + ) + } + FlowSpecError::InvalidOperator(op) => { + write!(f, "Invalid operator: 0x{:02X}", op) + } + FlowSpecError::InvalidComponentType(t) => { + write!(f, "Invalid component type: {}", t) + } + FlowSpecError::InsufficientData => { + write!(f, "Insufficient data for parsing") + } + FlowSpecError::InvalidPrefix => { + write!(f, "Invalid prefix encoding") + } + FlowSpecError::InvalidValueLength(len) => { + write!(f, "Invalid value length: {}", len) + } + } + } +} + +impl std::error::Error for FlowSpecError {} diff --git a/src/models/bgp/flowspec/nlri.rs b/src/models/bgp/flowspec/nlri.rs new file mode 100644 index 00000000..97fa90ed --- /dev/null +++ b/src/models/bgp/flowspec/nlri.rs @@ -0,0 +1,487 @@ +use super::*; +use crate::models::NetworkPrefix; +use ipnet::IpNet; + +#[cfg(test)] +use std::str::FromStr; + +/// Parse Flow-Spec NLRI from byte data according to RFC 8955/8956 +pub fn parse_flowspec_nlri(data: &[u8]) -> Result { + let mut offset = 0; + let length = parse_length(data, &mut offset)?; + + if offset + length as usize > data.len() { + return Err(FlowSpecError::InsufficientData); + } + + let end_offset = offset + length as usize; + let mut components = Vec::new(); + let mut last_type = 0u8; + + while offset < end_offset { + let component_type = data[offset]; + offset += 1; + + // Verify ordering + if component_type <= last_type { + return Err(FlowSpecError::InvalidComponentOrder { + expected_greater_than: last_type, + found: component_type, + }); + } + last_type = component_type; + + let component = match component_type { + 1 => parse_destination_prefix(data, &mut offset)?, + 2 => parse_source_prefix(data, &mut offset)?, + 3 => parse_ip_protocol(data, &mut offset)?, + 4 => parse_port(data, &mut offset)?, + 5 => parse_destination_port(data, &mut offset)?, + 6 => parse_source_port(data, &mut offset)?, + 7 => parse_icmp_type(data, &mut offset)?, + 8 => parse_icmp_code(data, &mut offset)?, + 9 => parse_tcp_flags(data, &mut offset)?, + 10 => parse_packet_length(data, &mut offset)?, + 11 => parse_dscp(data, &mut offset)?, + 12 => parse_fragment(data, &mut offset)?, + 13 => parse_flow_label(data, &mut offset)?, + _ => return Err(FlowSpecError::InvalidComponentType(component_type)), + }; + + components.push(component); + } + + Ok(FlowSpecNlri { components }) +} + +/// Encode Flow-Spec NLRI to byte data +pub fn encode_flowspec_nlri(nlri: &FlowSpecNlri) -> Vec { + let mut data = Vec::new(); + + // Encode each component + for component in &nlri.components { + data.push(component.component_type()); + + match component { + FlowSpecComponent::DestinationPrefix(prefix) + | FlowSpecComponent::SourcePrefix(prefix) => { + encode_prefix(prefix, &mut data); + } + FlowSpecComponent::DestinationIpv6Prefix { offset, prefix } + | FlowSpecComponent::SourceIpv6Prefix { offset, prefix } => { + encode_ipv6_prefix(*offset, prefix, &mut data); + } + FlowSpecComponent::IpProtocol(ops) + | FlowSpecComponent::Port(ops) + | FlowSpecComponent::DestinationPort(ops) + | FlowSpecComponent::SourcePort(ops) + | FlowSpecComponent::IcmpType(ops) + | FlowSpecComponent::IcmpCode(ops) + | FlowSpecComponent::PacketLength(ops) + | FlowSpecComponent::Dscp(ops) + | FlowSpecComponent::FlowLabel(ops) => { + encode_numeric_operators(ops, &mut data); + } + FlowSpecComponent::TcpFlags(ops) | FlowSpecComponent::Fragment(ops) => { + encode_bitmask_operators(ops, &mut data); + } + } + } + + // Prepend length + let mut result = Vec::new(); + encode_length(data.len() as u16, &mut result); + result.extend(data); + result +} + +/// Parse length field (1 or 2 octets) +pub(crate) fn parse_length(data: &[u8], offset: &mut usize) -> Result { + if *offset >= data.len() { + return Err(FlowSpecError::InsufficientData); + } + + let first_byte = data[*offset]; + *offset += 1; + + if first_byte < 240 { + Ok(first_byte as u16) + } else { + if *offset >= data.len() { + return Err(FlowSpecError::InsufficientData); + } + let second_byte = data[*offset]; + *offset += 1; + Ok(((first_byte & 0x0F) as u16) << 8 | second_byte as u16) + } +} + +/// Encode length field (1 or 2 octets) +pub(crate) fn encode_length(length: u16, data: &mut Vec) { + if length < 240 { + data.push(length as u8); + } else { + data.push(0xF0 | ((length >> 8) as u8)); + data.push(length as u8); + } +} + +/// Parse prefix component (Types 1 & 2) +fn parse_prefix_component(data: &[u8], offset: &mut usize) -> Result { + if *offset >= data.len() { + return Err(FlowSpecError::InsufficientData); + } + + let prefix_len = data[*offset]; + *offset += 1; + + let prefix_bytes = prefix_len.div_ceil(8); + if *offset + prefix_bytes as usize > data.len() { + return Err(FlowSpecError::InsufficientData); + } + + let prefix_data = &data[*offset..*offset + prefix_bytes as usize]; + *offset += prefix_bytes as usize; + + // Construct prefix based on address family + let prefix = if prefix_bytes <= 4 { + // IPv4 + let mut addr_bytes = [0u8; 4]; + addr_bytes[..prefix_data.len()].copy_from_slice(prefix_data); + let addr = std::net::Ipv4Addr::from(addr_bytes); + let ipnet = IpNet::V4( + ipnet::Ipv4Net::new(addr, prefix_len).map_err(|_| FlowSpecError::InvalidPrefix)?, + ); + NetworkPrefix::new(ipnet, None) + } else { + // IPv6 + let mut addr_bytes = [0u8; 16]; + addr_bytes[..prefix_data.len()].copy_from_slice(prefix_data); + let addr = std::net::Ipv6Addr::from(addr_bytes); + let ipnet = IpNet::V6( + ipnet::Ipv6Net::new(addr, prefix_len).map_err(|_| FlowSpecError::InvalidPrefix)?, + ); + NetworkPrefix::new(ipnet, None) + }; + + Ok(prefix) +} + +/// Parse destination prefix (Type 1) +fn parse_destination_prefix( + data: &[u8], + offset: &mut usize, +) -> Result { + let prefix = parse_prefix_component(data, offset)?; + Ok(FlowSpecComponent::DestinationPrefix(prefix)) +} + +/// Parse source prefix (Type 2) +fn parse_source_prefix( + data: &[u8], + offset: &mut usize, +) -> Result { + let prefix = parse_prefix_component(data, offset)?; + Ok(FlowSpecComponent::SourcePrefix(prefix)) +} + +/// Parse numeric operators sequence +fn parse_numeric_operators( + data: &[u8], + offset: &mut usize, +) -> Result, FlowSpecError> { + let mut operators = Vec::new(); + + loop { + if *offset >= data.len() { + return Err(FlowSpecError::InsufficientData); + } + + let operator_byte = data[*offset]; + *offset += 1; + + let value_length = match (operator_byte >> 4) & 0x03 { + 0 => 1, + 1 => 2, + 2 => 4, + 3 => 8, + _ => { + return Err(FlowSpecError::InvalidValueLength( + (operator_byte >> 4) & 0x03, + )) + } + }; + + if *offset + value_length > data.len() { + return Err(FlowSpecError::InsufficientData); + } + + let value = read_value(&data[*offset..*offset + value_length]); + *offset += value_length; + + let operator = NumericOperator::from_byte_and_value(operator_byte, value)?; + let is_end = operator.end_of_list; + operators.push(operator); + + if is_end { + break; + } + } + + Ok(operators) +} + +/// Parse bitmask operators sequence +fn parse_bitmask_operators( + data: &[u8], + offset: &mut usize, +) -> Result, FlowSpecError> { + let mut operators = Vec::new(); + + loop { + if *offset >= data.len() { + return Err(FlowSpecError::InsufficientData); + } + + let operator_byte = data[*offset]; + *offset += 1; + + let value_length = match (operator_byte >> 4) & 0x03 { + 0 => 1, + 1 => 2, + 2 => 4, + 3 => 8, + _ => { + return Err(FlowSpecError::InvalidValueLength( + (operator_byte >> 4) & 0x03, + )) + } + }; + + if *offset + value_length > data.len() { + return Err(FlowSpecError::InsufficientData); + } + + let bitmask = read_value(&data[*offset..*offset + value_length]); + *offset += value_length; + + let operator = BitmaskOperator::from_byte_and_value(operator_byte, bitmask)?; + let is_end = operator.end_of_list; + operators.push(operator); + + if is_end { + break; + } + } + + Ok(operators) +} + +/// Read value from bytes (big-endian) +fn read_value(bytes: &[u8]) -> u64 { + let mut value = 0u64; + for &byte in bytes { + value = (value << 8) | byte as u64; + } + value +} + +/// Write value to bytes (big-endian) +fn write_value(value: u64, length: usize, data: &mut Vec) { + for i in (0..length).rev() { + data.push((value >> (i * 8)) as u8); + } +} + +// Component parsers +fn parse_ip_protocol(data: &[u8], offset: &mut usize) -> Result { + let operators = parse_numeric_operators(data, offset)?; + Ok(FlowSpecComponent::IpProtocol(operators)) +} + +fn parse_port(data: &[u8], offset: &mut usize) -> Result { + let operators = parse_numeric_operators(data, offset)?; + Ok(FlowSpecComponent::Port(operators)) +} + +fn parse_destination_port( + data: &[u8], + offset: &mut usize, +) -> Result { + let operators = parse_numeric_operators(data, offset)?; + Ok(FlowSpecComponent::DestinationPort(operators)) +} + +fn parse_source_port(data: &[u8], offset: &mut usize) -> Result { + let operators = parse_numeric_operators(data, offset)?; + Ok(FlowSpecComponent::SourcePort(operators)) +} + +fn parse_icmp_type(data: &[u8], offset: &mut usize) -> Result { + let operators = parse_numeric_operators(data, offset)?; + Ok(FlowSpecComponent::IcmpType(operators)) +} + +fn parse_icmp_code(data: &[u8], offset: &mut usize) -> Result { + let operators = parse_numeric_operators(data, offset)?; + Ok(FlowSpecComponent::IcmpCode(operators)) +} + +fn parse_tcp_flags(data: &[u8], offset: &mut usize) -> Result { + let operators = parse_bitmask_operators(data, offset)?; + Ok(FlowSpecComponent::TcpFlags(operators)) +} + +fn parse_packet_length( + data: &[u8], + offset: &mut usize, +) -> Result { + let operators = parse_numeric_operators(data, offset)?; + Ok(FlowSpecComponent::PacketLength(operators)) +} + +fn parse_dscp(data: &[u8], offset: &mut usize) -> Result { + let operators = parse_numeric_operators(data, offset)?; + Ok(FlowSpecComponent::Dscp(operators)) +} + +fn parse_fragment(data: &[u8], offset: &mut usize) -> Result { + let operators = parse_bitmask_operators(data, offset)?; + Ok(FlowSpecComponent::Fragment(operators)) +} + +fn parse_flow_label(data: &[u8], offset: &mut usize) -> Result { + let operators = parse_numeric_operators(data, offset)?; + Ok(FlowSpecComponent::FlowLabel(operators)) +} + +// Encoding functions +fn encode_prefix(prefix: &NetworkPrefix, data: &mut Vec) { + let prefix_len = prefix.prefix.prefix_len(); + data.push(prefix_len); + + let prefix_bytes = prefix_len.div_ceil(8); + let addr_bytes = match prefix.prefix.addr() { + std::net::IpAddr::V4(addr) => addr.octets().to_vec(), + std::net::IpAddr::V6(addr) => addr.octets().to_vec(), + }; + + data.extend(&addr_bytes[..prefix_bytes as usize]); +} + +fn encode_ipv6_prefix(offset: u8, prefix: &NetworkPrefix, data: &mut Vec) { + data.push(prefix.prefix.prefix_len()); + data.push(offset); + + let prefix_bytes = prefix.prefix.prefix_len().div_ceil(8); + if let std::net::IpAddr::V6(addr) = prefix.prefix.addr() { + let addr_bytes = addr.octets(); + data.extend(&addr_bytes[..prefix_bytes as usize]); + } +} + +fn encode_numeric_operators(operators: &[NumericOperator], data: &mut Vec) { + for operator in operators { + data.push(operator.to_byte()); + write_value(operator.value, operator.value_length as usize, data); + } +} + +fn encode_bitmask_operators(operators: &[BitmaskOperator], data: &mut Vec) { + for operator in operators { + data.push(operator.to_byte()); + write_value(operator.bitmask, operator.value_length as usize, data); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_length_encoding() { + // Test short length + let mut data = Vec::new(); + encode_length(100, &mut data); + assert_eq!(data, vec![100]); + + let mut offset = 0; + let parsed_len = parse_length(&data, &mut offset).unwrap(); + assert_eq!(parsed_len, 100); + assert_eq!(offset, 1); + + // Test extended length + let mut data = Vec::new(); + encode_length(1000, &mut data); + assert_eq!(data, vec![0xF3, 0xE8]); // 1000 = 0x3E8 + + let mut offset = 0; + let parsed_len = parse_length(&data, &mut offset).unwrap(); + assert_eq!(parsed_len, 1000); + assert_eq!(offset, 2); + } + + #[test] + fn test_read_write_value() { + // Test 1 byte + assert_eq!(read_value(&[0x25]), 0x25); + + // Test 2 bytes + assert_eq!(read_value(&[0x01, 0xBB]), 443); + + // Test 4 bytes + assert_eq!(read_value(&[0x00, 0x00, 0x00, 0x50]), 80); + + // Test writing + let mut data = Vec::new(); + write_value(443, 2, &mut data); + assert_eq!(data, vec![0x01, 0xBB]); + } + + #[test] + fn test_simple_nlri_parsing() { + // Packets to 192.0.2.0/24 and TCP (protocol 6) + let data = vec![ + 0x08, // Length: 8 bytes (the actual NLRI content length) + 0x01, // Type 1: Destination Prefix + 0x18, // /24 + 0xC0, 0x00, 0x02, // 192.0.2.0 + 0x03, // Type 3: IP Protocol + 0x81, // end=1, and=0, len=00, eq=1 + 0x06, // TCP + ]; + + let nlri = parse_flowspec_nlri(&data).unwrap(); + assert_eq!(nlri.components.len(), 2); + + match &nlri.components[0] { + FlowSpecComponent::DestinationPrefix(prefix) => { + assert_eq!(prefix.to_string(), "192.0.2.0/24"); + } + _ => panic!("Expected destination prefix"), + } + + match &nlri.components[1] { + FlowSpecComponent::IpProtocol(ops) => { + assert_eq!(ops.len(), 1); + assert_eq!(ops[0].value, 6); + assert!(ops[0].equal); + } + _ => panic!("Expected IP protocol"), + } + } + + #[test] + fn test_nlri_round_trip() { + let original_nlri = FlowSpecNlri::new(vec![ + FlowSpecComponent::DestinationPrefix(NetworkPrefix::from_str("192.0.2.0/24").unwrap()), + FlowSpecComponent::IpProtocol(vec![NumericOperator::equal_to(6)]), + FlowSpecComponent::DestinationPort(vec![NumericOperator::equal_to(80)]), + ]); + + let encoded = encode_flowspec_nlri(&original_nlri); + let parsed_nlri = parse_flowspec_nlri(&encoded).unwrap(); + + assert_eq!(original_nlri, parsed_nlri); + } +} diff --git a/src/models/bgp/flowspec/operators.rs b/src/models/bgp/flowspec/operators.rs new file mode 100644 index 00000000..8a52c2b0 --- /dev/null +++ b/src/models/bgp/flowspec/operators.rs @@ -0,0 +1,309 @@ +use super::FlowSpecError; + +/// Numeric operator for Flow-Spec components (RFC 8955 Section 4.2.1) +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct NumericOperator { + /// End-of-list flag (bit 7) + pub end_of_list: bool, + /// AND flag - true=AND with next, false=OR with next (bit 6) + pub and_with_next: bool, + /// Value length in octets (bits 5-4): 00=1, 01=2, 10=4, 11=8 + pub value_length: u8, + /// Less-than comparison (bit 2) + pub less_than: bool, + /// Greater-than comparison (bit 1) + pub greater_than: bool, + /// Equal comparison (bit 0) + pub equal: bool, + /// The comparison value + pub value: u64, +} + +/// Bitmask operator for Flow-Spec components (RFC 8955 Section 4.2.2) +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct BitmaskOperator { + /// End-of-list flag (bit 7) + pub end_of_list: bool, + /// AND flag - true=AND with next, false=OR with next (bit 6) + pub and_with_next: bool, + /// Value length in octets (bits 5-4): 00=1, 01=2, 10=4, 11=8 + pub value_length: u8, + /// NOT flag - logical negation (bit 1) + pub not: bool, + /// Match flag - true=partial match, false=exact match (bit 0) + pub match_flag: bool, + /// The bitmask value + pub bitmask: u64, +} + +impl NumericOperator { + /// Create a new numeric operator from raw byte and value + pub fn from_byte_and_value(operator_byte: u8, value: u64) -> Result { + let value_length = match (operator_byte >> 4) & 0x03 { + 0 => 1, + 1 => 2, + 2 => 4, + 3 => 8, + _ => return Err(FlowSpecError::InvalidOperator(operator_byte)), + }; + + // Bit 3 must be 0 for numeric operators + if (operator_byte & 0x08) != 0 { + return Err(FlowSpecError::InvalidOperator(operator_byte)); + } + + Ok(NumericOperator { + end_of_list: (operator_byte & 0x80) != 0, + and_with_next: (operator_byte & 0x40) != 0, + value_length, + less_than: (operator_byte & 0x04) != 0, + greater_than: (operator_byte & 0x02) != 0, + equal: (operator_byte & 0x01) != 0, + value, + }) + } + + /// Convert to byte representation + pub fn to_byte(&self) -> u8 { + let mut byte = 0u8; + + if self.end_of_list { + byte |= 0x80; + } + if self.and_with_next { + byte |= 0x40; + } + + // Encode length + let len_bits = match self.value_length { + 1 => 0x00, + 2 => 0x10, + 4 => 0x20, + 8 => 0x30, + _ => 0x00, // Default to 1 byte + }; + byte |= len_bits; + + // Bit 3 is reserved (0) + + if self.less_than { + byte |= 0x04; + } + if self.greater_than { + byte |= 0x02; + } + if self.equal { + byte |= 0x01; + } + + byte + } + + /// Create equality operator + pub fn equal_to(value: u64) -> Self { + let value_length = if value <= 0xFF { + 1 + } else if value <= 0xFFFF { + 2 + } else if value <= 0xFFFFFFFF { + 4 + } else { + 8 + }; + + NumericOperator { + end_of_list: true, + and_with_next: false, + value_length, + less_than: false, + greater_than: false, + equal: true, + value, + } + } + + /// Create range operator (greater than or equal) + pub fn greater_than_or_equal(value: u64) -> Self { + let value_length = if value <= 0xFF { + 1 + } else if value <= 0xFFFF { + 2 + } else if value <= 0xFFFFFFFF { + 4 + } else { + 8 + }; + + NumericOperator { + end_of_list: true, + and_with_next: false, + value_length, + less_than: false, + greater_than: true, + equal: true, + value, + } + } +} + +impl BitmaskOperator { + /// Create a new bitmask operator from raw byte and value + pub fn from_byte_and_value(operator_byte: u8, bitmask: u64) -> Result { + let value_length = match (operator_byte >> 4) & 0x03 { + 0 => 1, + 1 => 2, + 2 => 4, + 3 => 8, + _ => return Err(FlowSpecError::InvalidOperator(operator_byte)), + }; + + // Bits 3 and 2 must be 0 for bitmask operators + if (operator_byte & 0x0C) != 0 { + return Err(FlowSpecError::InvalidOperator(operator_byte)); + } + + Ok(BitmaskOperator { + end_of_list: (operator_byte & 0x80) != 0, + and_with_next: (operator_byte & 0x40) != 0, + value_length, + not: (operator_byte & 0x02) != 0, + match_flag: (operator_byte & 0x01) != 0, + bitmask, + }) + } + + /// Convert to byte representation + pub fn to_byte(&self) -> u8 { + let mut byte = 0u8; + + if self.end_of_list { + byte |= 0x80; + } + if self.and_with_next { + byte |= 0x40; + } + + // Encode length + let len_bits = match self.value_length { + 1 => 0x00, + 2 => 0x10, + 4 => 0x20, + 8 => 0x30, + _ => 0x00, // Default to 1 byte + }; + byte |= len_bits; + + // Bits 3 and 2 are reserved (0) + + if self.not { + byte |= 0x02; + } + if self.match_flag { + byte |= 0x01; + } + + byte + } + + /// Create exact match operator + pub fn exact_match(bitmask: u64) -> Self { + let value_length = if bitmask <= 0xFF { + 1 + } else if bitmask <= 0xFFFF { + 2 + } else if bitmask <= 0xFFFFFFFF { + 4 + } else { + 8 + }; + + BitmaskOperator { + end_of_list: true, + and_with_next: false, + value_length, + not: false, + match_flag: false, + bitmask, + } + } + + /// Create partial match operator + pub fn partial_match(bitmask: u64) -> Self { + let value_length = if bitmask <= 0xFF { + 1 + } else if bitmask <= 0xFFFF { + 2 + } else if bitmask <= 0xFFFFFFFF { + 4 + } else { + 8 + }; + + BitmaskOperator { + end_of_list: true, + and_with_next: false, + value_length, + not: false, + match_flag: true, + bitmask, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_numeric_operator_creation() { + let op = NumericOperator::equal_to(25); + assert!(op.equal); + assert!(!op.less_than); + assert!(!op.greater_than); + assert_eq!(op.value, 25); + assert_eq!(op.value_length, 1); + } + + #[test] + fn test_numeric_operator_byte_conversion() { + let op = NumericOperator { + end_of_list: true, + and_with_next: false, + value_length: 1, + less_than: false, + greater_than: false, + equal: true, + value: 25, + }; + + let byte = op.to_byte(); + assert_eq!(byte, 0x81); // 10000001: end_of_list=1, len=00, eq=1 + + let parsed_op = NumericOperator::from_byte_and_value(byte, 25).unwrap(); + assert_eq!(parsed_op.end_of_list, op.end_of_list); + assert_eq!(parsed_op.equal, op.equal); + assert_eq!(parsed_op.value, op.value); + } + + #[test] + fn test_bitmask_operator_creation() { + let op = BitmaskOperator::exact_match(0x06); // TCP SYN+FIN flags + assert!(!op.match_flag); + assert!(!op.not); + assert_eq!(op.bitmask, 0x06); + assert_eq!(op.value_length, 1); + } + + #[test] + fn test_invalid_operator_byte() { + // Bit 3 should be 0 for numeric operators + let result = NumericOperator::from_byte_and_value(0x08, 25); + assert!(matches!(result, Err(FlowSpecError::InvalidOperator(0x08)))); + + // Bits 3 and 2 should be 0 for bitmask operators + let result = BitmaskOperator::from_byte_and_value(0x0C, 25); + assert!(matches!(result, Err(FlowSpecError::InvalidOperator(0x0C)))); + } +} diff --git a/src/models/bgp/flowspec/tests.rs b/src/models/bgp/flowspec/tests.rs new file mode 100644 index 00000000..e250cd4a --- /dev/null +++ b/src/models/bgp/flowspec/tests.rs @@ -0,0 +1,374 @@ +use super::nlri::{encode_length, parse_length}; +use super::*; +use crate::models::NetworkPrefix; +use std::str::FromStr; + +/// Test cases based on RFC 8955 examples +#[cfg(test)] +mod rfc_examples { + use super::*; + + #[test] + fn test_rfc8955_example_1() { + // "Packets to 192.0.2.0/24 and TCP port 25" + // Length: 0x0b + // Destination Prefix: 01 18 c0 00 02 + // Protocol: 03 81 06 + // Port: 04 81 19 + + let data = vec![ + 0x0B, // Length: 11 bytes + 0x01, // Type 1: Destination Prefix + 0x18, // /24 + 0xC0, 0x00, 0x02, // 192.0.2.0 + 0x03, // Type 3: IP Protocol + 0x81, // end=1, and=0, len=00, eq=1 + 0x06, // TCP (6) + 0x04, // Type 4: Port + 0x81, // end=1, and=0, len=00, eq=1 + 0x19, // Port 25 + ]; + + let nlri = parse_flowspec_nlri(&data).unwrap(); + assert_eq!(nlri.components.len(), 3); + + // Check destination prefix + match &nlri.components[0] { + FlowSpecComponent::DestinationPrefix(prefix) => { + assert_eq!(prefix.to_string(), "192.0.2.0/24"); + } + _ => panic!("Expected destination prefix"), + } + + // Check IP protocol + match &nlri.components[1] { + FlowSpecComponent::IpProtocol(ops) => { + assert_eq!(ops.len(), 1); + assert_eq!(ops[0].value, 6); + assert!(ops[0].equal); + assert!(ops[0].end_of_list); + } + _ => panic!("Expected IP protocol"), + } + + // Check port + match &nlri.components[2] { + FlowSpecComponent::Port(ops) => { + assert_eq!(ops.len(), 1); + assert_eq!(ops[0].value, 25); + assert!(ops[0].equal); + assert!(ops[0].end_of_list); + } + _ => panic!("Expected port"), + } + + // Test round-trip encoding + let encoded = encode_flowspec_nlri(&nlri); + assert_eq!(encoded, data); + } + + #[test] + fn test_rfc8955_example_2_partial() { + // "Packets to 192.0.2.0/24 from 203.0.113.0/24 and port {range [137, 139] or 8080}" + // This is a simplified version focusing on the prefix components + + let data = vec![ + 0x0E, // Length: 14 bytes (corrected) + 0x01, // Type 1: Destination Prefix + 0x18, // /24 + 0xC0, 0x00, 0x02, // 192.0.2.0 + 0x02, // Type 2: Source Prefix + 0x18, // /24 + 0xCB, 0x00, 0x71, // 203.0.113.0 + 0x04, // Type 4: Port + 0x91, // end=1, and=0, len=01, eq=1 + 0x1F, 0x90, // Port 8080 + ]; + + let nlri = parse_flowspec_nlri(&data).unwrap(); + assert_eq!(nlri.components.len(), 3); + + // Check destination prefix + match &nlri.components[0] { + FlowSpecComponent::DestinationPrefix(prefix) => { + assert_eq!(prefix.to_string(), "192.0.2.0/24"); + } + _ => panic!("Expected destination prefix"), + } + + // Check source prefix + match &nlri.components[1] { + FlowSpecComponent::SourcePrefix(prefix) => { + assert_eq!(prefix.to_string(), "203.0.113.0/24"); + } + _ => panic!("Expected source prefix"), + } + + // Check port (simplified to just 8080) + match &nlri.components[2] { + FlowSpecComponent::Port(ops) => { + assert_eq!(ops.len(), 1); + assert_eq!(ops[0].value, 8080); + assert!(ops[0].equal); + assert_eq!(ops[0].value_length, 2); + } + _ => panic!("Expected port"), + } + } + + #[test] + fn test_tcp_flags_component() { + // TCP flags with SYN+ACK (0x12) + let data = vec![ + 0x03, // Length: 3 bytes (corrected) + 0x09, // Type 9: TCP Flags + 0x81, // end=1, and=0, len=00, not=0, match=1 (partial match) + 0x12, // SYN+ACK flags + ]; + + let nlri = parse_flowspec_nlri(&data).unwrap(); + assert_eq!(nlri.components.len(), 1); + + match &nlri.components[0] { + FlowSpecComponent::TcpFlags(ops) => { + assert_eq!(ops.len(), 1); + assert_eq!(ops[0].bitmask, 0x12); + assert!(ops[0].match_flag); // Partial match + assert!(!ops[0].not); + assert!(ops[0].end_of_list); + } + _ => panic!("Expected TCP flags"), + } + } + + #[test] + fn test_packet_length_range() { + // Packet length >= 1000 bytes + let data = vec![ + 0x04, // Length: 4 bytes (content after length byte) + 0x0A, // Type 10: Packet Length + 0x93, // end=1, and=0, len=01, 0, lt=0, gt=1, eq=1 (>=) + 0x03, 0xE8, // 1000 bytes + ]; + + let nlri = parse_flowspec_nlri(&data).unwrap(); + assert_eq!(nlri.components.len(), 1); + + match &nlri.components[0] { + FlowSpecComponent::PacketLength(ops) => { + assert_eq!(ops.len(), 1); + assert_eq!(ops[0].value, 1000); + assert!(ops[0].greater_than); + assert!(ops[0].equal); + assert!(!ops[0].less_than); + assert_eq!(ops[0].value_length, 2); + } + _ => panic!("Expected packet length"), + } + } + + #[test] + fn test_dscp_marking() { + // DSCP value 46 (EF - Expedited Forwarding) + let data = vec![ + 0x03, // Length: 3 bytes + 0x0B, // Type 11: DSCP + 0x81, // end=1, and=0, len=00, eq=1 + 0x2E, // DSCP 46 (EF) + ]; + + let nlri = parse_flowspec_nlri(&data).unwrap(); + assert_eq!(nlri.components.len(), 1); + + match &nlri.components[0] { + FlowSpecComponent::Dscp(ops) => { + assert_eq!(ops.len(), 1); + assert_eq!(ops[0].value, 46); + assert!(ops[0].equal); + } + _ => panic!("Expected DSCP"), + } + } + + #[test] + fn test_fragment_component() { + // Fragment: first fragment + let data = vec![ + 0x03, // Length: 3 bytes + 0x0C, // Type 12: Fragment + 0x81, // end=1, and=0, len=00, not=0, match=1 + 0x01, // First fragment bit + ]; + + let nlri = parse_flowspec_nlri(&data).unwrap(); + assert_eq!(nlri.components.len(), 1); + + match &nlri.components[0] { + FlowSpecComponent::Fragment(ops) => { + assert_eq!(ops.len(), 1); + assert_eq!(ops[0].bitmask, 0x01); + assert!(ops[0].match_flag); + assert!(!ops[0].not); + } + _ => panic!("Expected fragment"), + } + } + + #[test] + fn test_ipv6_flow_label() { + // IPv6 Flow Label = 0x12345 (RFC 8956) + let data = vec![ + 0x06, // Length: 6 bytes + 0x0D, // Type 13: Flow Label + 0xA1, // end=1, and=0, len=10, eq=1 (4-byte value) + 0x00, 0x01, 0x23, 0x45, // Flow Label value (20-bit in 4-byte field) + ]; + + let nlri = parse_flowspec_nlri(&data).unwrap(); + assert_eq!(nlri.components.len(), 1); + + match &nlri.components[0] { + FlowSpecComponent::FlowLabel(ops) => { + assert_eq!(ops.len(), 1); + assert_eq!(ops[0].value, 0x00012345); + assert!(ops[0].equal); + assert_eq!(ops[0].value_length, 4); + } + _ => panic!("Expected flow label"), + } + } + + #[test] + fn test_complex_port_range() { + // Port range: 80 OR 443 + let data = vec![ + 0x06, // Length: 6 bytes (corrected) + 0x04, // Type 4: Port + 0x01, // end=0, and=0 (OR), len=00, eq=1 + 0x50, // Port 80 + 0x91, // end=1, and=0, len=01, eq=1 + 0x01, 0xBB, // Port 443 (2 bytes) + ]; + + let nlri = parse_flowspec_nlri(&data).unwrap(); + assert_eq!(nlri.components.len(), 1); + + match &nlri.components[0] { + FlowSpecComponent::Port(ops) => { + assert_eq!(ops.len(), 2); + + // First operator: == 80, OR with next + assert_eq!(ops[0].value, 80); + assert!(ops[0].equal); + assert!(!ops[0].and_with_next); + assert!(!ops[0].end_of_list); + + // Second operator: == 443, end of list + assert_eq!(ops[1].value, 443); + assert!(ops[1].equal); + assert!(!ops[1].and_with_next); + assert!(ops[1].end_of_list); + } + _ => panic!("Expected port"), + } + + // Verify the operators are correctly parsed + if let FlowSpecComponent::Port(ops) = &nlri.components[0] { + assert_eq!(ops.len(), 2); + assert_eq!(ops[0].value, 80); + assert_eq!(ops[1].value, 443); + } + } +} + +/// Test error conditions and edge cases +#[cfg(test)] +mod error_handling { + use super::*; + + #[test] + fn test_insufficient_data() { + let data = vec![0x05, 0x01]; // Claims 5 bytes but only has 2 + let result = parse_flowspec_nlri(&data); + assert!(matches!(result, Err(FlowSpecError::InsufficientData))); + } + + #[test] + fn test_invalid_component_type() { + let data = vec![ + 0x03, // Length: 3 bytes + 0xFF, // Invalid component type + 0x81, 0x06, // Some data + ]; + + let result = parse_flowspec_nlri(&data); + assert!(matches!( + result, + Err(FlowSpecError::InvalidComponentType(0xFF)) + )); + } + + #[test] + fn test_extended_length_encoding() { + // Test length >= 240 + let mut data = Vec::new(); + encode_length(1000, &mut data); + + let mut offset = 0; + let parsed_length = parse_length(&data, &mut offset).unwrap(); + assert_eq!(parsed_length, 1000); + + // Test maximum length (4095) + let mut data = Vec::new(); + encode_length(4095, &mut data); + + let mut offset = 0; + let parsed_length = parse_length(&data, &mut offset).unwrap(); + assert_eq!(parsed_length, 4095); + } +} + +/// IPv6-specific test cases from RFC 8956 +#[cfg(test)] +mod ipv6_tests { + use super::*; + + #[test] + fn test_ipv6_destination_prefix() { + let nlri = FlowSpecNlri::new(vec![FlowSpecComponent::DestinationPrefix( + NetworkPrefix::from_str("2001:db8::/32").unwrap(), + )]); + + assert!(nlri.is_ipv6()); + assert!(!nlri.is_ipv4()); + } + + #[test] + fn test_ipv6_prefix_with_offset() { + // Test IPv6 prefix with offset (theoretical - actual parsing would need more implementation) + let nlri = FlowSpecNlri::new(vec![FlowSpecComponent::DestinationIpv6Prefix { + offset: 32, + prefix: NetworkPrefix::from_str("2001:db8::/64").unwrap(), + }]); + + assert!(nlri.is_ipv6()); + + match &nlri.components[0] { + FlowSpecComponent::DestinationIpv6Prefix { offset, prefix } => { + assert_eq!(*offset, 32); + assert_eq!(prefix.to_string(), "2001:db8::/64"); + } + _ => panic!("Expected IPv6 destination prefix with offset"), + } + } + + #[test] + fn test_flow_label_component() { + let nlri = FlowSpecNlri::new(vec![FlowSpecComponent::FlowLabel(vec![ + NumericOperator::equal_to(0x12345), + ])]); + + assert!(nlri.is_ipv6()); // Flow label implies IPv6 + assert!(!nlri.is_ipv4()); + } +} diff --git a/src/models/bgp/mod.rs b/src/models/bgp/mod.rs index 95984363..68c05930 100644 --- a/src/models/bgp/mod.rs +++ b/src/models/bgp/mod.rs @@ -5,6 +5,7 @@ pub mod capabilities; pub mod community; pub mod elem; pub mod error; +pub mod flowspec; pub mod linkstate; pub mod role; pub mod tunnel_encap; @@ -13,6 +14,7 @@ pub use attributes::*; pub use community::*; pub use elem::*; pub use error::*; +pub use flowspec::*; pub use linkstate::*; pub use role::*; pub use tunnel_encap::*; @@ -139,7 +141,7 @@ pub enum CapabilityValue { BgpRole(BgpRoleCapability), } -#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[derive(Debug, Clone, PartialEq, Default, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] /// BGP Update Message. /// diff --git a/src/models/mrt/mod.rs b/src/models/mrt/mod.rs index e7e936cf..cd08df58 100644 --- a/src/models/mrt/mod.rs +++ b/src/models/mrt/mod.rs @@ -24,7 +24,7 @@ pub use table_dump_v2::*; /// /// See [CommonHeader] for the content in header, and [MrtMessage] for the /// message format. -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Clone, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct MrtRecord { pub common_header: CommonHeader, @@ -92,7 +92,7 @@ impl PartialEq for CommonHeader { } } -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Clone, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum MrtMessage { TableDumpMessage(TableDumpMessage), diff --git a/src/models/mrt/table_dump_v2.rs b/src/models/mrt/table_dump_v2.rs index 0e896d4b..59742c07 100644 --- a/src/models/mrt/table_dump_v2.rs +++ b/src/models/mrt/table_dump_v2.rs @@ -191,7 +191,7 @@ bitflags! { } /// Geo-location peer entry - RFC 6397 -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct GeoPeer { pub peer: Peer, @@ -199,6 +199,16 @@ pub struct GeoPeer { pub peer_longitude: f32, } +impl PartialEq for GeoPeer { + fn eq(&self, other: &Self) -> bool { + self.peer == other.peer + && self.peer_latitude.to_bits() == other.peer_latitude.to_bits() + && self.peer_longitude.to_bits() == other.peer_longitude.to_bits() + } +} + +impl Eq for GeoPeer {} + impl GeoPeer { pub fn new(peer: Peer, latitude: f32, longitude: f32) -> Self { Self { @@ -209,10 +219,8 @@ impl GeoPeer { } } -impl Eq for GeoPeer {} - /// RFC 6397: Geo-location peer table -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct GeoPeerTable { pub collector_bgp_id: BgpIdentifier, @@ -222,6 +230,18 @@ pub struct GeoPeerTable { pub geo_peers: Vec, } +impl PartialEq for GeoPeerTable { + fn eq(&self, other: &Self) -> bool { + self.collector_bgp_id == other.collector_bgp_id + && self.view_name == other.view_name + && self.collector_latitude.to_bits() == other.collector_latitude.to_bits() + && self.collector_longitude.to_bits() == other.collector_longitude.to_bits() + && self.geo_peers == other.geo_peers + } +} + +impl Eq for GeoPeerTable {} + impl GeoPeerTable { pub fn new( collector_bgp_id: BgpIdentifier, @@ -243,8 +263,6 @@ impl GeoPeerTable { } } -impl Eq for GeoPeerTable {} - /// Peer struct. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] diff --git a/src/models/network/afi.rs b/src/models/network/afi.rs index a811118a..3360aa34 100644 --- a/src/models/network/afi.rs +++ b/src/models/network/afi.rs @@ -52,6 +52,12 @@ pub enum Safi { /// Multicast for BGP/MPLS IP VPNs - RFC 6514 /// Works with both AFI 1 (Multicast VPN-IPv4) and AFI 2 (Multicast VPN-IPv6) MulticastVpn = 129, + /// Flow Specification - RFC 8955/8956 + /// Works with both AFI 1 (IPv4 Flow-Spec) and AFI 2 (IPv6 Flow-Spec) + FlowSpec = 133, + /// L3VPN Flow Specification - RFC 8955/8956 + /// Works with both AFI 1 (VPN-IPv4 Flow-Spec) and AFI 2 (VPN-IPv6 Flow-Spec) + FlowSpecL3Vpn = 134, } #[cfg(test)] @@ -85,6 +91,9 @@ mod tests { // RFC 8950 VPN SAFI values assert_eq!(Safi::MplsVpn as u8, 128); assert_eq!(Safi::MulticastVpn as u8, 129); + // RFC 8955/8956 Flow-Spec SAFI values + assert_eq!(Safi::FlowSpec as u8, 133); + assert_eq!(Safi::FlowSpecL3Vpn as u8, 134); } #[test] @@ -132,5 +141,18 @@ mod tests { assert_eq!(serialized, "\"MulticastVpn\""); let deserialized: Safi = serde_json::from_str(&serialized).unwrap(); assert_eq!(deserialized, safi); + + // RFC 8955/8956 Flow-Spec SAFI variants + let safi = Safi::FlowSpec; + let serialized = serde_json::to_string(&safi).unwrap(); + assert_eq!(serialized, "\"FlowSpec\""); + let deserialized: Safi = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, safi); + + let safi = Safi::FlowSpecL3Vpn; + let serialized = serde_json::to_string(&safi).unwrap(); + assert_eq!(serialized, "\"FlowSpecL3Vpn\""); + let deserialized: Safi = serde_json::from_str(&serialized).unwrap(); + assert_eq!(deserialized, safi); } } diff --git a/src/parser/bgp/attributes/attr_14_15_nlri.rs b/src/parser/bgp/attributes/attr_14_15_nlri.rs index f5b96851..78b7cdb9 100644 --- a/src/parser/bgp/attributes/attr_14_15_nlri.rs +++ b/src/parser/bgp/attributes/attr_14_15_nlri.rs @@ -112,6 +112,7 @@ pub fn parse_nlri( next_hop, prefixes, link_state_nlris, + flowspec_nlris: None, }; match reachable { @@ -329,6 +330,7 @@ mod tests { path_id: None, }], link_state_nlris: None, + flowspec_nlris: None, }; let bytes = encode_nlri(&nlri, true); assert_eq!( @@ -358,6 +360,7 @@ mod tests { path_id: Some(123), }], link_state_nlris: None, + flowspec_nlris: None, }; let bytes = encode_nlri(&nlri, true); assert_eq!( @@ -415,6 +418,7 @@ mod tests { path_id: None, }], link_state_nlris: None, + flowspec_nlris: None, }; let bytes = encode_nlri(&nlri, false); assert_eq!( @@ -446,6 +450,7 @@ mod tests { path_id: None, }], link_state_nlris: None, + flowspec_nlris: None, }; let bytes = encode_nlri(&nlri_with_next_hop, false); // The encoded bytes should include the next_hop diff --git a/src/parser/bgp/attributes/attr_16_25_extended_communities.rs b/src/parser/bgp/attributes/attr_16_25_extended_communities.rs index 3fa3f86c..de83a626 100644 --- a/src/parser/bgp/attributes/attr_16_25_extended_communities.rs +++ b/src/parser/bgp/attributes/attr_16_25_extended_communities.rs @@ -29,14 +29,59 @@ pub fn parse_extended_community(mut input: Bytes) -> Result { let sub_type = input.read_u8()?; - let global = input.read_u16()?; - let mut local: [u8; 4] = [0; 4]; - input.read_exact(&mut local)?; - ExtendedCommunity::NonTransitiveTwoOctetAs(TwoOctetAsExtCommunity { - subtype: sub_type, - global_admin: Asn::new_16bit(global), - local_admin: local, - }) + match sub_type { + 0x06 => { + // Flow-Spec Traffic Rate + let as_number = input.read_u16()?; + let rate_bytes = input.read_u32()?; + let rate = f32::from_bits(rate_bytes); + ExtendedCommunity::FlowSpecTrafficRate(FlowSpecTrafficRate::new( + as_number, rate, + )) + } + 0x07 => { + // Flow-Spec Traffic Action + let as_number = input.read_u16()?; + let flags = input.read_u32()?; + let terminal = (flags & 0x01) != 0; + let sample = (flags & 0x02) != 0; + ExtendedCommunity::FlowSpecTrafficAction(FlowSpecTrafficAction::new( + as_number, terminal, sample, + )) + } + 0x08 => { + // Flow-Spec Redirect (same as TwoOctetAsExtCommunity but different variant) + let global = input.read_u16()?; + let mut local: [u8; 4] = [0; 4]; + input.read_exact(&mut local)?; + ExtendedCommunity::FlowSpecRedirect(TwoOctetAsExtCommunity { + subtype: sub_type, + global_admin: Asn::new_16bit(global), + local_admin: local, + }) + } + 0x09 => { + // Flow-Spec Traffic Marking + let as_number = input.read_u16()?; + let dscp = input.read_u8()?; + let _reserved1 = input.read_u8()?; // reserved + let _reserved2 = input.read_u16()?; // reserved + ExtendedCommunity::FlowSpecTrafficMarking(FlowSpecTrafficMarking::new( + as_number, dscp, + )) + } + _ => { + // Standard NonTransitiveTwoOctetAs + let global = input.read_u16()?; + let mut local: [u8; 4] = [0; 4]; + input.read_exact(&mut local)?; + ExtendedCommunity::NonTransitiveTwoOctetAs(TwoOctetAsExtCommunity { + subtype: sub_type, + global_admin: Asn::new_16bit(global), + local_admin: local, + }) + } + } } ExtendedCommunityType::TransitiveIpv4Addr => { @@ -173,6 +218,39 @@ pub fn encode_extended_communities(communities: &Vec) -> Byte bytes.put_slice(&opaque.value); } + ExtendedCommunity::FlowSpecTrafficRate(rate) => { + bytes.put_u8(ec_type); + bytes.put_u8(0x06); // subtype + bytes.put_u16(rate.as_number); + bytes.put_f32(rate.rate_bytes_per_sec); + } + ExtendedCommunity::FlowSpecTrafficAction(action) => { + bytes.put_u8(ec_type); + bytes.put_u8(0x07); // subtype + bytes.put_u16(action.as_number); + let mut flags = 0u32; + if action.terminal { + flags |= 0x01; + } + if action.sample { + flags |= 0x02; + } + bytes.put_u32(flags); + } + ExtendedCommunity::FlowSpecRedirect(redirect) => { + bytes.put_u8(ec_type); + bytes.put_u8(0x08); // subtype + bytes.put_u16(redirect.global_admin.into()); + bytes.put_slice(redirect.local_admin.as_slice()); + } + ExtendedCommunity::FlowSpecTrafficMarking(marking) => { + bytes.put_u8(ec_type); + bytes.put_u8(0x09); // subtype + bytes.put_u16(marking.as_number); + bytes.put_u8(marking.dscp); + bytes.put_u8(0); // reserved + bytes.put_u16(0); // reserved + } ExtendedCommunity::Raw(raw) => { bytes.put_slice(raw); } @@ -367,4 +445,122 @@ mod tests { }; let _bytes = encode_ipv6_extended_communities(&vec![community]); } + + #[test] + fn test_flowspec_extended_communities() { + // Test Flow-Spec Traffic Rate community + let rate_data = vec![ + 0x40, // NonTransitiveTwoOctetAs + 0x06, // Traffic Rate subtype + 0xFC, 0x00, // AS 64512 + 0x44, 0x7A, 0x00, 0x00, // 1000.0 as IEEE 754 float32 + ]; + + if let AttributeValue::ExtendedCommunities(communities) = + parse_extended_community(Bytes::from(rate_data)).unwrap() + { + assert_eq!(communities.len(), 1); + if let ExtendedCommunity::FlowSpecTrafficRate(rate) = &communities[0] { + assert_eq!(rate.as_number, 64512); + assert_eq!(rate.rate_bytes_per_sec, 1000.0); + } else { + panic!("Expected FlowSpecTrafficRate community"); + } + } else { + panic!("Expected ExtendedCommunities attribute"); + } + + // Test Flow-Spec Traffic Action community + let action_data = vec![ + 0x40, // NonTransitiveTwoOctetAs + 0x07, // Traffic Action subtype + 0xFC, 0x00, // AS 64512 + 0x00, 0x00, 0x00, 0x03, // terminal=1, sample=1 + ]; + + if let AttributeValue::ExtendedCommunities(communities) = + parse_extended_community(Bytes::from(action_data)).unwrap() + { + assert_eq!(communities.len(), 1); + if let ExtendedCommunity::FlowSpecTrafficAction(action) = &communities[0] { + assert_eq!(action.as_number, 64512); + assert!(action.terminal); + assert!(action.sample); + } else { + panic!("Expected FlowSpecTrafficAction community"); + } + } else { + panic!("Expected ExtendedCommunities attribute"); + } + + // Test Flow-Spec Traffic Marking community + let marking_data = vec![ + 0x40, // NonTransitiveTwoOctetAs + 0x09, // Traffic Marking subtype + 0xFC, 0x00, // AS 64512 + 0x2E, // DSCP 46 (EF) + 0x00, // reserved + 0x00, 0x00, // reserved + ]; + + if let AttributeValue::ExtendedCommunities(communities) = + parse_extended_community(Bytes::from(marking_data)).unwrap() + { + assert_eq!(communities.len(), 1); + if let ExtendedCommunity::FlowSpecTrafficMarking(marking) = &communities[0] { + assert_eq!(marking.as_number, 64512); + assert_eq!(marking.dscp, 46); + } else { + panic!("Expected FlowSpecTrafficMarking community"); + } + } else { + panic!("Expected ExtendedCommunities attribute"); + } + } + + #[test] + fn test_flowspec_extended_communities_encoding() { + // Test encoding of Flow-Spec communities + let communities = vec![ + ExtendedCommunity::FlowSpecTrafficRate(FlowSpecTrafficRate::new(64512, 1000.0)), + ExtendedCommunity::FlowSpecTrafficAction(FlowSpecTrafficAction::new(64512, true, true)), + ExtendedCommunity::FlowSpecTrafficMarking(FlowSpecTrafficMarking::new(64512, 46)), + ]; + + let encoded = encode_extended_communities(&communities); + + // Parse it back to verify round-trip + if let AttributeValue::ExtendedCommunities(parsed_communities) = + parse_extended_community(encoded).unwrap() + { + assert_eq!(parsed_communities.len(), 3); + + // Verify traffic rate + if let ExtendedCommunity::FlowSpecTrafficRate(rate) = &parsed_communities[0] { + assert_eq!(rate.as_number, 64512); + assert_eq!(rate.rate_bytes_per_sec, 1000.0); + } else { + panic!("Expected FlowSpecTrafficRate community"); + } + + // Verify traffic action + if let ExtendedCommunity::FlowSpecTrafficAction(action) = &parsed_communities[1] { + assert_eq!(action.as_number, 64512); + assert!(action.terminal); + assert!(action.sample); + } else { + panic!("Expected FlowSpecTrafficAction community"); + } + + // Verify traffic marking + if let ExtendedCommunity::FlowSpecTrafficMarking(marking) = &parsed_communities[2] { + assert_eq!(marking.as_number, 64512); + assert_eq!(marking.dscp, 46); + } else { + panic!("Expected FlowSpecTrafficMarking community"); + } + } else { + panic!("Expected ExtendedCommunities attribute"); + } + } } diff --git a/src/parser/bgp/messages.rs b/src/parser/bgp/messages.rs index 63628b5d..da49e6f9 100644 --- a/src/parser/bgp/messages.rs +++ b/src/parser/bgp/messages.rs @@ -486,6 +486,7 @@ mod tests { next_hop: None, prefixes: vec![], link_state_nlris: None, + flowspec_nlris: None, })]); let msg = BgpUpdateMessage { withdrawn_prefixes: vec![], @@ -521,6 +522,7 @@ mod tests { next_hop: None, prefixes: vec![], link_state_nlris: None, + flowspec_nlris: None, })]); let msg = BgpUpdateMessage { withdrawn_prefixes: vec![], @@ -536,6 +538,7 @@ mod tests { next_hop: None, prefixes: vec![prefix], link_state_nlris: None, + flowspec_nlris: None, })]); let msg = BgpUpdateMessage { withdrawn_prefixes: vec![], @@ -551,6 +554,7 @@ mod tests { next_hop: None, prefixes: vec![prefix], link_state_nlris: None, + flowspec_nlris: None, })]); let msg = BgpUpdateMessage { withdrawn_prefixes: vec![], @@ -567,6 +571,7 @@ mod tests { next_hop: None, prefixes: vec![], link_state_nlris: None, + flowspec_nlris: None, }), AttributeValue::AtomicAggregate, ]); From 72c168b6aef5fa79937c9ee2d83852503c75fbaa Mon Sep 17 00:00:00 2001 From: Mingwei Zhang Date: Tue, 5 Aug 2025 07:20:10 -0700 Subject: [PATCH 2/4] test: add Flow-Spec test coverage for missed code paths Add 32 additional unit tests to improve test coverage for Flow-Spec implementation, focusing on previously uncovered code paths: - FlowSpecComponent methods and type classification - FlowSpecNlri IPv4/IPv6 detection and component validation - FlowSpecError Display implementation and error handling - Numeric/bitmask operator creation with various value lengths - Operator byte encoding/decoding edge cases - NLRI parsing error conditions and boundary cases Total Flow-Spec tests increased from 22 to 54, providing comprehensive coverage of public APIs, error conditions, and edge cases. --- src/models/bgp/flowspec/operators.rs | 219 +++++++++++ src/models/bgp/flowspec/tests.rs | 533 +++++++++++++++++++++++++++ 2 files changed, 752 insertions(+) diff --git a/src/models/bgp/flowspec/operators.rs b/src/models/bgp/flowspec/operators.rs index 8a52c2b0..475c039c 100644 --- a/src/models/bgp/flowspec/operators.rs +++ b/src/models/bgp/flowspec/operators.rs @@ -306,4 +306,223 @@ mod tests { let result = BitmaskOperator::from_byte_and_value(0x0C, 25); assert!(matches!(result, Err(FlowSpecError::InvalidOperator(0x0C)))); } + + #[test] + fn test_numeric_operator_various_lengths() { + // Test 1-byte value + let op1 = NumericOperator::equal_to(255); + assert_eq!(op1.value_length, 1); + assert_eq!(op1.value, 255); + + // Test 2-byte value + let op2 = NumericOperator::equal_to(256); + assert_eq!(op2.value_length, 2); + assert_eq!(op2.value, 256); + + // Test 4-byte value + let op4 = NumericOperator::equal_to(0x10000); + assert_eq!(op4.value_length, 4); + assert_eq!(op4.value, 0x10000); + + // Test 8-byte value + let op8 = NumericOperator::equal_to(0x100000000); + assert_eq!(op8.value_length, 8); + assert_eq!(op8.value, 0x100000000); + } + + #[test] + fn test_bitmask_operator_various_lengths() { + // Test 1-byte value + let op1 = BitmaskOperator::exact_match(255); + assert_eq!(op1.value_length, 1); + assert_eq!(op1.bitmask, 255); + + // Test 2-byte value + let op2 = BitmaskOperator::exact_match(256); + assert_eq!(op2.value_length, 2); + assert_eq!(op2.bitmask, 256); + + // Test 4-byte value + let op4 = BitmaskOperator::exact_match(0x10000); + assert_eq!(op4.value_length, 4); + assert_eq!(op4.bitmask, 0x10000); + + // Test 8-byte value + let op8 = BitmaskOperator::exact_match(0x100000000); + assert_eq!(op8.value_length, 8); + assert_eq!(op8.bitmask, 0x100000000); + } + + #[test] + fn test_numeric_operator_greater_than_or_equal() { + let op = NumericOperator::greater_than_or_equal(1000); + assert!(op.greater_than); + assert!(op.equal); + assert!(!op.less_than); + assert!(op.end_of_list); + assert!(!op.and_with_next); + assert_eq!(op.value, 1000); + assert_eq!(op.value_length, 2); + } + + #[test] + fn test_bitmask_operator_partial_match() { + let op = BitmaskOperator::partial_match(0x06); + assert!(op.match_flag); + assert!(!op.not); + assert!(op.end_of_list); + assert!(!op.and_with_next); + assert_eq!(op.bitmask, 0x06); + assert_eq!(op.value_length, 1); + } + + #[test] + fn test_numeric_operator_complex_byte_encoding() { + // Test complex operator: not end of list, AND with next, 2-byte length, less than + equal + let op = NumericOperator { + end_of_list: false, + and_with_next: true, + value_length: 2, + less_than: true, + greater_than: false, + equal: true, + value: 443, + }; + + let byte = op.to_byte(); + // Expected: 0101 0101 = 0x55 (not_end=0, and=1, len=01, reserved=0, lt=1, gt=0, eq=1) + assert_eq!(byte, 0x55); + + let parsed_op = NumericOperator::from_byte_and_value(byte, 443).unwrap(); + assert_eq!(parsed_op.end_of_list, op.end_of_list); + assert_eq!(parsed_op.and_with_next, op.and_with_next); + assert_eq!(parsed_op.value_length, op.value_length); + assert_eq!(parsed_op.less_than, op.less_than); + assert_eq!(parsed_op.greater_than, op.greater_than); + assert_eq!(parsed_op.equal, op.equal); + assert_eq!(parsed_op.value, op.value); + } + + #[test] + fn test_bitmask_operator_complex_byte_encoding() { + // Test complex operator: not end of list, AND with next, 4-byte length, NOT, partial match + let op = BitmaskOperator { + end_of_list: false, + and_with_next: true, + value_length: 4, + not: true, + match_flag: true, + bitmask: 0xFF000000, + }; + + let byte = op.to_byte(); + // Expected: 0110 0011 = 0x63 (not_end=0, and=1, len=10, reserved=00, not=1, match=1) + assert_eq!(byte, 0x63); + + let parsed_op = BitmaskOperator::from_byte_and_value(byte, 0xFF000000).unwrap(); + assert_eq!(parsed_op.end_of_list, op.end_of_list); + assert_eq!(parsed_op.and_with_next, op.and_with_next); + assert_eq!(parsed_op.value_length, op.value_length); + assert_eq!(parsed_op.not, op.not); + assert_eq!(parsed_op.match_flag, op.match_flag); + assert_eq!(parsed_op.bitmask, op.bitmask); + } + + #[test] + fn test_operator_invalid_length_defaults() { + // Test that invalid value lengths default to 1 byte in to_byte() + let mut op = NumericOperator::equal_to(25); + op.value_length = 7; // Invalid length + let byte = op.to_byte(); + // Should default to 1-byte encoding (len bits = 00) + assert_eq!(byte & 0x30, 0x00); + + let mut bm_op = BitmaskOperator::exact_match(25); + bm_op.value_length = 9; // Invalid length + let byte = bm_op.to_byte(); + // Should default to 1-byte encoding (len bits = 00) + assert_eq!(byte & 0x30, 0x00); + } + + #[test] + fn test_numeric_operator_all_comparison_flags() { + // Test operator with all comparison flags set + let op = NumericOperator { + end_of_list: true, + and_with_next: false, + value_length: 1, + less_than: true, + greater_than: true, + equal: true, + value: 100, + }; + + let byte = op.to_byte(); + assert_eq!(byte, 0x87); // 10000111: end=1, len=00, lt=1, gt=1, eq=1 + + let parsed_op = NumericOperator::from_byte_and_value(byte, 100).unwrap(); + assert!(parsed_op.less_than); + assert!(parsed_op.greater_than); + assert!(parsed_op.equal); + } + + #[test] + fn test_bitmask_operator_edge_cases() { + // Test various combinations of NOT and match flags + let op_not_exact = BitmaskOperator { + end_of_list: true, + and_with_next: false, + value_length: 1, + not: true, + match_flag: false, + bitmask: 0x42, + }; + + let byte = op_not_exact.to_byte(); + assert_eq!(byte, 0x82); // 10000010: end=1, len=00, not=1, match=0 + + let parsed_op = BitmaskOperator::from_byte_and_value(byte, 0x42).unwrap(); + assert!(parsed_op.not); + assert!(!parsed_op.match_flag); + } + + #[test] + fn test_operator_length_encoding_all_values() { + // Test all valid length encodings for numeric operators + for (expected_len, len_bits) in [(1, 0x00), (2, 0x10), (4, 0x20), (8, 0x30)] { + let op = NumericOperator { + end_of_list: true, + and_with_next: false, + value_length: expected_len, + less_than: false, + greater_than: false, + equal: true, + value: 42, + }; + + let byte = op.to_byte(); + assert_eq!(byte & 0x30, len_bits); + + let parsed_op = NumericOperator::from_byte_and_value(byte, 42).unwrap(); + assert_eq!(parsed_op.value_length, expected_len); + } + + // Test all valid length encodings for bitmask operators + for (expected_len, len_bits) in [(1, 0x00), (2, 0x10), (4, 0x20), (8, 0x30)] { + let op = BitmaskOperator { + end_of_list: true, + and_with_next: false, + value_length: expected_len, + not: false, + match_flag: true, + bitmask: 42, + }; + + let byte = op.to_byte(); + assert_eq!(byte & 0x30, len_bits); + + let parsed_op = BitmaskOperator::from_byte_and_value(byte, 42).unwrap(); + assert_eq!(parsed_op.value_length, expected_len); + } + } } diff --git a/src/models/bgp/flowspec/tests.rs b/src/models/bgp/flowspec/tests.rs index e250cd4a..efbb5798 100644 --- a/src/models/bgp/flowspec/tests.rs +++ b/src/models/bgp/flowspec/tests.rs @@ -372,3 +372,536 @@ mod ipv6_tests { assert!(!nlri.is_ipv4()); } } + +/// Component type and method tests +#[cfg(test)] +mod component_tests { + use super::*; + + #[test] + fn test_component_type_ids() { + // Test all component type IDs + let dest_prefix = FlowSpecComponent::DestinationPrefix( + NetworkPrefix::from_str("192.0.2.0/24").unwrap() + ); + assert_eq!(dest_prefix.component_type(), 1); + + let src_prefix = FlowSpecComponent::SourcePrefix( + NetworkPrefix::from_str("192.0.2.0/24").unwrap() + ); + assert_eq!(src_prefix.component_type(), 2); + + let ip_protocol = FlowSpecComponent::IpProtocol(vec![NumericOperator::equal_to(6)]); + assert_eq!(ip_protocol.component_type(), 3); + + let port = FlowSpecComponent::Port(vec![NumericOperator::equal_to(80)]); + assert_eq!(port.component_type(), 4); + + let dest_port = FlowSpecComponent::DestinationPort(vec![NumericOperator::equal_to(80)]); + assert_eq!(dest_port.component_type(), 5); + + let src_port = FlowSpecComponent::SourcePort(vec![NumericOperator::equal_to(80)]); + assert_eq!(src_port.component_type(), 6); + + let icmp_type = FlowSpecComponent::IcmpType(vec![NumericOperator::equal_to(8)]); + assert_eq!(icmp_type.component_type(), 7); + + let icmp_code = FlowSpecComponent::IcmpCode(vec![NumericOperator::equal_to(0)]); + assert_eq!(icmp_code.component_type(), 8); + + let tcp_flags = FlowSpecComponent::TcpFlags(vec![BitmaskOperator::exact_match(0x02)]); + assert_eq!(tcp_flags.component_type(), 9); + + let packet_length = FlowSpecComponent::PacketLength(vec![NumericOperator::equal_to(1500)]); + assert_eq!(packet_length.component_type(), 10); + + let dscp = FlowSpecComponent::Dscp(vec![NumericOperator::equal_to(46)]); + assert_eq!(dscp.component_type(), 11); + + let fragment = FlowSpecComponent::Fragment(vec![BitmaskOperator::exact_match(0x01)]); + assert_eq!(fragment.component_type(), 12); + + let flow_label = FlowSpecComponent::FlowLabel(vec![NumericOperator::equal_to(0x12345)]); + assert_eq!(flow_label.component_type(), 13); + + // Test IPv6 prefix components + let dest_ipv6 = FlowSpecComponent::DestinationIpv6Prefix { + offset: 32, + prefix: NetworkPrefix::from_str("2001:db8::/64").unwrap(), + }; + assert_eq!(dest_ipv6.component_type(), 1); + + let src_ipv6 = FlowSpecComponent::SourceIpv6Prefix { + offset: 32, + prefix: NetworkPrefix::from_str("2001:db8::/64").unwrap(), + }; + assert_eq!(src_ipv6.component_type(), 2); + } + + #[test] + fn test_uses_numeric_operators() { + // Components that use numeric operators + assert!(FlowSpecComponent::IpProtocol(vec![]).uses_numeric_operators()); + assert!(FlowSpecComponent::Port(vec![]).uses_numeric_operators()); + assert!(FlowSpecComponent::DestinationPort(vec![]).uses_numeric_operators()); + assert!(FlowSpecComponent::SourcePort(vec![]).uses_numeric_operators()); + assert!(FlowSpecComponent::IcmpType(vec![]).uses_numeric_operators()); + assert!(FlowSpecComponent::IcmpCode(vec![]).uses_numeric_operators()); + assert!(FlowSpecComponent::PacketLength(vec![]).uses_numeric_operators()); + assert!(FlowSpecComponent::Dscp(vec![]).uses_numeric_operators()); + assert!(FlowSpecComponent::FlowLabel(vec![]).uses_numeric_operators()); + + // Components that don't use numeric operators + let prefix = NetworkPrefix::from_str("192.0.2.0/24").unwrap(); + assert!(!FlowSpecComponent::DestinationPrefix(prefix.clone()).uses_numeric_operators()); + assert!(!FlowSpecComponent::SourcePrefix(prefix.clone()).uses_numeric_operators()); + assert!(!FlowSpecComponent::TcpFlags(vec![]).uses_numeric_operators()); + assert!(!FlowSpecComponent::Fragment(vec![]).uses_numeric_operators()); + assert!(!FlowSpecComponent::DestinationIpv6Prefix { offset: 0, prefix: prefix.clone() }.uses_numeric_operators()); + assert!(!FlowSpecComponent::SourceIpv6Prefix { offset: 0, prefix }.uses_numeric_operators()); + } + + #[test] + fn test_uses_bitmask_operators() { + // Components that use bitmask operators + assert!(FlowSpecComponent::TcpFlags(vec![]).uses_bitmask_operators()); + assert!(FlowSpecComponent::Fragment(vec![]).uses_bitmask_operators()); + + // Components that don't use bitmask operators + let prefix = NetworkPrefix::from_str("192.0.2.0/24").unwrap(); + assert!(!FlowSpecComponent::DestinationPrefix(prefix.clone()).uses_bitmask_operators()); + assert!(!FlowSpecComponent::SourcePrefix(prefix.clone()).uses_bitmask_operators()); + assert!(!FlowSpecComponent::IpProtocol(vec![]).uses_bitmask_operators()); + assert!(!FlowSpecComponent::Port(vec![]).uses_bitmask_operators()); + assert!(!FlowSpecComponent::DestinationPort(vec![]).uses_bitmask_operators()); + assert!(!FlowSpecComponent::SourcePort(vec![]).uses_bitmask_operators()); + assert!(!FlowSpecComponent::IcmpType(vec![]).uses_bitmask_operators()); + assert!(!FlowSpecComponent::IcmpCode(vec![]).uses_bitmask_operators()); + assert!(!FlowSpecComponent::PacketLength(vec![]).uses_bitmask_operators()); + assert!(!FlowSpecComponent::Dscp(vec![]).uses_bitmask_operators()); + assert!(!FlowSpecComponent::FlowLabel(vec![]).uses_bitmask_operators()); + assert!(!FlowSpecComponent::DestinationIpv6Prefix { offset: 0, prefix: prefix.clone() }.uses_bitmask_operators()); + assert!(!FlowSpecComponent::SourceIpv6Prefix { offset: 0, prefix }.uses_bitmask_operators()); + } + + #[test] + fn test_nlri_methods() { + let components = vec![ + FlowSpecComponent::DestinationPrefix(NetworkPrefix::from_str("192.0.2.0/24").unwrap()), + FlowSpecComponent::IpProtocol(vec![NumericOperator::equal_to(6)]), + ]; + let nlri = FlowSpecNlri::new(components.clone()); + + // Test components() method + assert_eq!(nlri.components(), &components); + assert_eq!(nlri.components.len(), 2); + } + + #[test] + fn test_mixed_ipv4_ipv6_detection() { + // Pure IPv4 NLRI + let ipv4_nlri = FlowSpecNlri::new(vec![ + FlowSpecComponent::DestinationPrefix(NetworkPrefix::from_str("192.0.2.0/24").unwrap()), + FlowSpecComponent::IpProtocol(vec![NumericOperator::equal_to(6)]), + ]); + assert!(ipv4_nlri.is_ipv4()); + assert!(!ipv4_nlri.is_ipv6()); + + // Pure IPv6 NLRI + let ipv6_nlri = FlowSpecNlri::new(vec![ + FlowSpecComponent::DestinationPrefix(NetworkPrefix::from_str("2001:db8::/32").unwrap()), + FlowSpecComponent::IpProtocol(vec![NumericOperator::equal_to(6)]), + ]); + assert!(ipv6_nlri.is_ipv6()); + assert!(!ipv6_nlri.is_ipv4()); + + // IPv6 with flow label + let ipv6_flow_nlri = FlowSpecNlri::new(vec![ + FlowSpecComponent::FlowLabel(vec![NumericOperator::equal_to(0x12345)]), + FlowSpecComponent::IpProtocol(vec![NumericOperator::equal_to(6)]), + ]); + assert!(ipv6_flow_nlri.is_ipv6()); + assert!(!ipv6_flow_nlri.is_ipv4()); + + // NLRI with no prefix components + let no_prefix_nlri = FlowSpecNlri::new(vec![ + FlowSpecComponent::IpProtocol(vec![NumericOperator::equal_to(6)]), + FlowSpecComponent::Port(vec![NumericOperator::equal_to(80)]), + ]); + assert!(!no_prefix_nlri.is_ipv4()); + assert!(!no_prefix_nlri.is_ipv6()); + } +} + +/// Error handling and Display implementation tests +#[cfg(test)] +mod error_tests { + use super::*; + + #[test] + fn test_flowspec_error_display() { + // Test InvalidComponentOrder + let error = FlowSpecError::InvalidComponentOrder { + expected_greater_than: 5, + found: 3, + }; + assert_eq!( + error.to_string(), + "Invalid component order: expected type > 5, but found 3" + ); + + // Test InvalidOperator + let error = FlowSpecError::InvalidOperator(0xFF); + assert_eq!(error.to_string(), "Invalid operator: 0xFF"); + + // Test InvalidComponentType + let error = FlowSpecError::InvalidComponentType(99); + assert_eq!(error.to_string(), "Invalid component type: 99"); + + // Test InsufficientData + let error = FlowSpecError::InsufficientData; + assert_eq!(error.to_string(), "Insufficient data for parsing"); + + // Test InvalidPrefix + let error = FlowSpecError::InvalidPrefix; + assert_eq!(error.to_string(), "Invalid prefix encoding"); + + // Test InvalidValueLength + let error = FlowSpecError::InvalidValueLength(7); + assert_eq!(error.to_string(), "Invalid value length: 7"); + } + + #[test] + fn test_flowspec_error_as_std_error() { + let error = FlowSpecError::InsufficientData; + let _: &dyn std::error::Error = &error; + // If this compiles, the Error trait is implemented correctly + } + + #[test] + fn test_component_order_validation() { + // Test that component order is enforced during parsing + let data = vec![ + 0x06, // Length: 6 bytes + 0x03, // Type 3: IP Protocol (first) + 0x81, 0x06, // TCP + 0x01, // Type 1: Destination Prefix (second - should fail because 1 < 3) + 0x18, 0xC0, // Invalid data + ]; + + let result = parse_flowspec_nlri(&data); + assert!(matches!( + result, + Err(FlowSpecError::InvalidComponentOrder { + expected_greater_than: 3, + found: 1 + }) + )); + } +} + +/// NLRI parsing edge cases and error conditions +#[cfg(test)] +mod nlri_parsing_tests { + use super::*; + + #[test] + fn test_parse_length_insufficient_data() { + // Test insufficient data for extended length + let data = vec![0xF0]; // Extended length marker but no second byte + let mut offset = 0; + let result = parse_length(&data, &mut offset); + assert!(matches!(result, Err(FlowSpecError::InsufficientData))); + } + + #[test] + fn test_parse_length_at_boundary() { + // Test parsing at exact boundary of 240 + let data = vec![239]; // Just under extended length threshold + let mut offset = 0; + let result = parse_length(&data, &mut offset).unwrap(); + assert_eq!(result, 239); + assert_eq!(offset, 1); + } + + + #[test] + fn test_invalid_prefix_length() { + // Test prefix with invalid length causing insufficient data + let data = vec![ + 0x05, // Length: 5 bytes (claiming more data than available) + 0x01, // Type 1: Destination Prefix + 0xFF, // Invalid prefix length (255 bits) + 0xC0, 0x00, // Only 2 bytes but need 32 bytes for /255 + ]; + + let result = parse_flowspec_nlri(&data); + assert!(matches!(result, Err(FlowSpecError::InsufficientData))); + } + + #[test] + fn test_numeric_operators_insufficient_data() { + // Test numeric operators with insufficient data for value + let data = vec![ + 0x04, // Length: 4 bytes + 0x03, // Type 3: IP Protocol + 0x91, // end=1, and=0, len=01 (2 bytes), eq=1 + 0x06, // Only 1 byte provided, but 2 bytes expected + ]; + + let result = parse_flowspec_nlri(&data); + assert!(matches!(result, Err(FlowSpecError::InsufficientData))); + } + + #[test] + fn test_bitmask_operators_insufficient_data() { + // Test bitmask operators with insufficient data for value + let data = vec![ + 0x04, // Length: 4 bytes + 0x09, // Type 9: TCP Flags + 0xA1, // end=1, and=0, len=10 (4 bytes), not=0, match=1 + 0x02, 0x00, // Only 2 bytes provided, but 4 bytes expected + ]; + + let result = parse_flowspec_nlri(&data); + assert!(matches!(result, Err(FlowSpecError::InsufficientData))); + } + + #[test] + fn test_empty_operators_list() { + // Test component with no operators data + let data = vec![ + 0x02, // Length: 2 bytes + 0x03, // Type 3: IP Protocol + // No operator data follows + ]; + + let result = parse_flowspec_nlri(&data); + assert!(matches!(result, Err(FlowSpecError::InsufficientData))); + } + + #[test] + fn test_multiple_operators_parsing() { + // Test parsing multiple operators in a sequence + let data = vec![ + 0x07, // Length: 7 bytes + 0x04, // Type 4: Port + 0x01, // end=0, and=0 (OR), len=00, eq=1 + 0x50, // Port 80 + 0x41, // end=0, and=1 (AND), len=00, eq=1 + 0x35, // Port 53 + 0x81, // end=1, and=0, len=00, eq=1 + 0x19, // Port 25 + ]; + + let nlri = parse_flowspec_nlri(&data).unwrap(); + match &nlri.components[0] { + FlowSpecComponent::Port(ops) => { + assert_eq!(ops.len(), 3); + assert_eq!(ops[0].value, 80); + assert!(!ops[0].end_of_list); + assert!(!ops[0].and_with_next); + + assert_eq!(ops[1].value, 53); + assert!(!ops[1].end_of_list); + assert!(ops[1].and_with_next); + + assert_eq!(ops[2].value, 25); + assert!(ops[2].end_of_list); + assert!(!ops[2].and_with_next); + } + _ => panic!("Expected port component"), + } + } + + #[test] + fn test_encode_ipv6_prefix_offset() { + // Test encoding IPv6 prefix with offset + let prefix = NetworkPrefix::from_str("2001:db8::/64").unwrap(); + let nlri = FlowSpecNlri::new(vec![FlowSpecComponent::DestinationIpv6Prefix { + offset: 32, + prefix: prefix.clone(), + }]); + + let encoded = encode_flowspec_nlri(&nlri); + + // Should start with length, then type 1, then prefix len, then offset + assert!(encoded.len() > 4); + assert_eq!(encoded[1], 1); // Type 1 + assert_eq!(encoded[2], 64); // /64 + assert_eq!(encoded[3], 32); // offset + } + + #[test] + fn test_value_encoding_through_operators() { + // Test value encoding/decoding through public operator interfaces + let op1 = NumericOperator::equal_to(0x1234); + let byte = op1.to_byte(); + let parsed_op = NumericOperator::from_byte_and_value(byte, 0x1234).unwrap(); + assert_eq!(parsed_op.value, 0x1234); + assert_eq!(parsed_op.value_length, 2); + + let op2 = BitmaskOperator::exact_match(0x12345678); + let byte = op2.to_byte(); + let parsed_op = BitmaskOperator::from_byte_and_value(byte, 0x12345678).unwrap(); + assert_eq!(parsed_op.bitmask, 0x12345678); + assert_eq!(parsed_op.value_length, 4); + } + + #[test] + fn test_prefix_component_ipv4_vs_ipv6_detection() { + // Test prefix parsing with different address families + let ipv4_data = vec![ + 0x05, // Length: 5 bytes + 0x01, // Type 1: Destination Prefix + 0x18, // /24 + 0xC0, 0x00, 0x02, // 192.0.2.0 + ]; + + let nlri = parse_flowspec_nlri(&ipv4_data).unwrap(); + match &nlri.components[0] { + FlowSpecComponent::DestinationPrefix(prefix) => { + assert!(matches!(prefix.prefix, ipnet::IpNet::V4(_))); + } + _ => panic!("Expected destination prefix"), + } + } + + #[test] + fn test_operator_encoding_through_nlri() { + // Test operator encoding through the public NLRI interface + // Create operators with proper end_of_list flags + let mut op1 = NumericOperator::equal_to(255); + op1.end_of_list = false; + let mut op2 = NumericOperator::equal_to(65535); + op2.end_of_list = false; + let op3 = NumericOperator::equal_to(0xFFFFFFFF); // This one keeps end_of_list = true + + let mut bm_op1 = BitmaskOperator::exact_match(0xFF); + bm_op1.end_of_list = false; + let bm_op2 = BitmaskOperator::partial_match(0xFFFF); // This one keeps end_of_list = true + + let nlri = FlowSpecNlri::new(vec![ + FlowSpecComponent::IpProtocol(vec![op1, op2, op3]), + FlowSpecComponent::TcpFlags(vec![bm_op1, bm_op2]), + ]); + + let encoded = encode_flowspec_nlri(&nlri); + let parsed = parse_flowspec_nlri(&encoded).unwrap(); + + // Verify round-trip encoding worked + assert_eq!(parsed.components.len(), 2); + + match &parsed.components[0] { + FlowSpecComponent::IpProtocol(ops) => { + assert_eq!(ops.len(), 3); + assert_eq!(ops[0].value, 255); + assert_eq!(ops[1].value, 65535); + assert_eq!(ops[2].value, 0xFFFFFFFF); + } + _ => panic!("Expected IP protocol component"), + } + + match &parsed.components[1] { + FlowSpecComponent::TcpFlags(ops) => { + assert_eq!(ops.len(), 2); + assert_eq!(ops[0].bitmask, 0xFF); + assert_eq!(ops[1].bitmask, 0xFFFF); + } + _ => panic!("Expected TCP flags component"), + } + } + + #[test] + fn test_all_component_types_parsing() { + // Test that all component type parsers work + let test_cases = vec![ + (3, "IP Protocol"), + (4, "Port"), + (5, "Destination Port"), + (6, "Source Port"), + (7, "ICMP Type"), + (8, "ICMP Code"), + (10, "Packet Length"), + (11, "DSCP"), + (13, "Flow Label"), + ]; + + for (component_type, description) in test_cases { + let data = vec![ + 0x03, // Length: 3 bytes + component_type, // Component type + 0x81, // end=1, and=0, len=00, eq=1 + 0x06, // Value + ]; + + let result = parse_flowspec_nlri(&data); + assert!(result.is_ok(), "Failed to parse {}: {:?}", description, result); + + let nlri = result.unwrap(); + assert_eq!(nlri.components.len(), 1); + assert_eq!(nlri.components[0].component_type(), component_type); + } + + // Test bitmask component types + let bitmask_cases = vec![ + (9, "TCP Flags"), + (12, "Fragment"), + ]; + + for (component_type, description) in bitmask_cases { + let data = vec![ + 0x03, // Length: 3 bytes + component_type, // Component type + 0x81, // end=1, and=0, len=00, not=0, match=1 + 0x06, // Value + ]; + + let result = parse_flowspec_nlri(&data); + assert!(result.is_ok(), "Failed to parse {}: {:?}", description, result); + + let nlri = result.unwrap(); + assert_eq!(nlri.components.len(), 1); + assert_eq!(nlri.components[0].component_type(), component_type); + } + } + + #[test] + fn test_length_encoding_edge_cases() { + use super::nlri::{encode_length, parse_length}; + + // Test exact boundary at 240 + let mut data = Vec::new(); + encode_length(240, &mut data); + assert_eq!(data, vec![0xF0, 0xF0]); // Extended encoding: 0xF0 + 240 + + let mut offset = 0; + let parsed = parse_length(&data, &mut offset).unwrap(); + assert_eq!(parsed, 240); + + // Test maximum value (4095) + let mut data = Vec::new(); + encode_length(4095, &mut data); + assert_eq!(data, vec![0xFF, 0xFF]); // Maximum extended encoding + + let mut offset = 0; + let parsed = parse_length(&data, &mut offset).unwrap(); + assert_eq!(parsed, 4095); + } + + #[test] + fn test_invalid_value_length_error() { + // This tests the InvalidValueLength error path that might be unreachable + // due to bit masking, but we test it for completeness + let data = vec![ + 0x04, // Length: 4 bytes + 0x03, // Type 3: IP Protocol + 0xFF, // Invalid operator with impossible length encoding + 0x06, 0x00, + ]; + + // The actual error depends on how the parser handles the invalid operator + let result = parse_flowspec_nlri(&data); + assert!(result.is_err()); + } +} From b319365f5447a09c2a4e850c857f1046c79b7710 Mon Sep 17 00:00:00 2001 From: Mingwei Zhang Date: Tue, 5 Aug 2025 07:22:18 -0700 Subject: [PATCH 3/4] docs: update README.md from lib.rs documentation --- README.md | 6 +-- src/lib.rs | 6 +-- src/models/bgp/flowspec/operators.rs | 4 +- src/models/bgp/flowspec/tests.rs | 68 ++++++++++++++++------------ 4 files changed, 48 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index b1e95344..27dc0ec2 100644 --- a/README.md +++ b/README.md @@ -410,9 +410,9 @@ We support normal communities, extended communities, and large communities. ### FlowSpec -- [ ] [RFC 8955](https://datatracker.ietf.org/doc/html/rfc8955) Dissemination of Flow Specification Rules -- [ ] [RFC 8956](https://datatracker.ietf.org/doc/html/rfc8956) Dissemination of Flow Specification Rules for IPv6 -- [ ] [RFC 9117](https://datatracker.ietf.org/doc/html/rfc9117) Revised Validation Procedure for BGP Flow Specifications Updates 8955 +- [X] [RFC 8955](https://datatracker.ietf.org/doc/html/rfc8955) Dissemination of Flow Specification Rules +- [X] [RFC 8956](https://datatracker.ietf.org/doc/html/rfc8956) Dissemination of Flow Specification Rules for IPv6 +- [X] [RFC 9117](https://datatracker.ietf.org/doc/html/rfc9117) Revised Validation Procedure for BGP Flow Specifications Updates 8955 ### Link-State - [X] [RFC 7752](https://datatracker.ietf.org/doc/html/rfc7752): North-Bound Distribution of Link-State and Traffic Engineering (TE) Information Using BGP diff --git a/src/lib.rs b/src/lib.rs index 517c17d8..3d655678 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -402,9 +402,9 @@ We support normal communities, extended communities, and large communities. ## FlowSpec -- [ ] [RFC 8955](https://datatracker.ietf.org/doc/html/rfc8955) Dissemination of Flow Specification Rules -- [ ] [RFC 8956](https://datatracker.ietf.org/doc/html/rfc8956) Dissemination of Flow Specification Rules for IPv6 -- [ ] [RFC 9117](https://datatracker.ietf.org/doc/html/rfc9117) Revised Validation Procedure for BGP Flow Specifications Updates 8955 +- [X] [RFC 8955](https://datatracker.ietf.org/doc/html/rfc8955) Dissemination of Flow Specification Rules +- [X] [RFC 8956](https://datatracker.ietf.org/doc/html/rfc8956) Dissemination of Flow Specification Rules for IPv6 +- [X] [RFC 9117](https://datatracker.ietf.org/doc/html/rfc9117) Revised Validation Procedure for BGP Flow Specifications Updates 8955 ## Link-State - [X] [RFC 7752](https://datatracker.ietf.org/doc/html/rfc7752): North-Bound Distribution of Link-State and Traffic Engineering (TE) Information Using BGP diff --git a/src/models/bgp/flowspec/operators.rs b/src/models/bgp/flowspec/operators.rs index 475c039c..875b9301 100644 --- a/src/models/bgp/flowspec/operators.rs +++ b/src/models/bgp/flowspec/operators.rs @@ -324,7 +324,7 @@ mod tests { assert_eq!(op4.value_length, 4); assert_eq!(op4.value, 0x10000); - // Test 8-byte value + // Test 8-byte value let op8 = NumericOperator::equal_to(0x100000000); assert_eq!(op8.value_length, 8); assert_eq!(op8.value, 0x100000000); @@ -337,7 +337,7 @@ mod tests { assert_eq!(op1.value_length, 1); assert_eq!(op1.bitmask, 255); - // Test 2-byte value + // Test 2-byte value let op2 = BitmaskOperator::exact_match(256); assert_eq!(op2.value_length, 2); assert_eq!(op2.bitmask, 256); diff --git a/src/models/bgp/flowspec/tests.rs b/src/models/bgp/flowspec/tests.rs index efbb5798..40a89b80 100644 --- a/src/models/bgp/flowspec/tests.rs +++ b/src/models/bgp/flowspec/tests.rs @@ -381,14 +381,12 @@ mod component_tests { #[test] fn test_component_type_ids() { // Test all component type IDs - let dest_prefix = FlowSpecComponent::DestinationPrefix( - NetworkPrefix::from_str("192.0.2.0/24").unwrap() - ); + let dest_prefix = + FlowSpecComponent::DestinationPrefix(NetworkPrefix::from_str("192.0.2.0/24").unwrap()); assert_eq!(dest_prefix.component_type(), 1); - let src_prefix = FlowSpecComponent::SourcePrefix( - NetworkPrefix::from_str("192.0.2.0/24").unwrap() - ); + let src_prefix = + FlowSpecComponent::SourcePrefix(NetworkPrefix::from_str("192.0.2.0/24").unwrap()); assert_eq!(src_prefix.component_type(), 2); let ip_protocol = FlowSpecComponent::IpProtocol(vec![NumericOperator::equal_to(6)]); @@ -457,7 +455,11 @@ mod component_tests { assert!(!FlowSpecComponent::SourcePrefix(prefix.clone()).uses_numeric_operators()); assert!(!FlowSpecComponent::TcpFlags(vec![]).uses_numeric_operators()); assert!(!FlowSpecComponent::Fragment(vec![]).uses_numeric_operators()); - assert!(!FlowSpecComponent::DestinationIpv6Prefix { offset: 0, prefix: prefix.clone() }.uses_numeric_operators()); + assert!(!FlowSpecComponent::DestinationIpv6Prefix { + offset: 0, + prefix: prefix.clone() + } + .uses_numeric_operators()); assert!(!FlowSpecComponent::SourceIpv6Prefix { offset: 0, prefix }.uses_numeric_operators()); } @@ -480,7 +482,11 @@ mod component_tests { assert!(!FlowSpecComponent::PacketLength(vec![]).uses_bitmask_operators()); assert!(!FlowSpecComponent::Dscp(vec![]).uses_bitmask_operators()); assert!(!FlowSpecComponent::FlowLabel(vec![]).uses_bitmask_operators()); - assert!(!FlowSpecComponent::DestinationIpv6Prefix { offset: 0, prefix: prefix.clone() }.uses_bitmask_operators()); + assert!(!FlowSpecComponent::DestinationIpv6Prefix { + offset: 0, + prefix: prefix.clone() + } + .uses_bitmask_operators()); assert!(!FlowSpecComponent::SourceIpv6Prefix { offset: 0, prefix }.uses_bitmask_operators()); } @@ -624,7 +630,6 @@ mod nlri_parsing_tests { assert_eq!(offset, 1); } - #[test] fn test_invalid_prefix_length() { // Test prefix with invalid length causing insufficient data @@ -673,7 +678,7 @@ mod nlri_parsing_tests { let data = vec![ 0x02, // Length: 2 bytes 0x03, // Type 3: IP Protocol - // No operator data follows + // No operator data follows ]; let result = parse_flowspec_nlri(&data); @@ -724,7 +729,7 @@ mod nlri_parsing_tests { }]); let encoded = encode_flowspec_nlri(&nlri); - + // Should start with length, then type 1, then prefix len, then offset assert!(encoded.len() > 4); assert_eq!(encoded[1], 1); // Type 1 @@ -788,10 +793,10 @@ mod nlri_parsing_tests { let encoded = encode_flowspec_nlri(&nlri); let parsed = parse_flowspec_nlri(&encoded).unwrap(); - + // Verify round-trip encoding worked assert_eq!(parsed.components.len(), 2); - + match &parsed.components[0] { FlowSpecComponent::IpProtocol(ops) => { assert_eq!(ops.len(), 3); @@ -801,7 +806,7 @@ mod nlri_parsing_tests { } _ => panic!("Expected IP protocol component"), } - + match &parsed.components[1] { FlowSpecComponent::TcpFlags(ops) => { assert_eq!(ops.len(), 2); @@ -829,37 +834,44 @@ mod nlri_parsing_tests { for (component_type, description) in test_cases { let data = vec![ - 0x03, // Length: 3 bytes + 0x03, // Length: 3 bytes component_type, // Component type - 0x81, // end=1, and=0, len=00, eq=1 - 0x06, // Value + 0x81, // end=1, and=0, len=00, eq=1 + 0x06, // Value ]; let result = parse_flowspec_nlri(&data); - assert!(result.is_ok(), "Failed to parse {}: {:?}", description, result); - + assert!( + result.is_ok(), + "Failed to parse {}: {:?}", + description, + result + ); + let nlri = result.unwrap(); assert_eq!(nlri.components.len(), 1); assert_eq!(nlri.components[0].component_type(), component_type); } // Test bitmask component types - let bitmask_cases = vec![ - (9, "TCP Flags"), - (12, "Fragment"), - ]; + let bitmask_cases = vec![(9, "TCP Flags"), (12, "Fragment")]; for (component_type, description) in bitmask_cases { let data = vec![ - 0x03, // Length: 3 bytes + 0x03, // Length: 3 bytes component_type, // Component type - 0x81, // end=1, and=0, len=00, not=0, match=1 - 0x06, // Value + 0x81, // end=1, and=0, len=00, not=0, match=1 + 0x06, // Value ]; let result = parse_flowspec_nlri(&data); - assert!(result.is_ok(), "Failed to parse {}: {:?}", description, result); - + assert!( + result.is_ok(), + "Failed to parse {}: {:?}", + description, + result + ); + let nlri = result.unwrap(); assert_eq!(nlri.components.len(), 1); assert_eq!(nlri.components[0].component_type(), component_type); From 554d83e50bb9202469c6c29c19b300c30c90df9a Mon Sep 17 00:00:00 2001 From: Mingwei Zhang Date: Tue, 5 Aug 2025 12:36:48 -0700 Subject: [PATCH 4/4] fix clippy issues --- src/models/bgp/flowspec/tests.rs | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/models/bgp/flowspec/tests.rs b/src/models/bgp/flowspec/tests.rs index 40a89b80..02d32c55 100644 --- a/src/models/bgp/flowspec/tests.rs +++ b/src/models/bgp/flowspec/tests.rs @@ -451,15 +451,14 @@ mod component_tests { // Components that don't use numeric operators let prefix = NetworkPrefix::from_str("192.0.2.0/24").unwrap(); - assert!(!FlowSpecComponent::DestinationPrefix(prefix.clone()).uses_numeric_operators()); - assert!(!FlowSpecComponent::SourcePrefix(prefix.clone()).uses_numeric_operators()); + assert!(!FlowSpecComponent::DestinationPrefix(prefix).uses_numeric_operators()); + assert!(!FlowSpecComponent::SourcePrefix(prefix).uses_numeric_operators()); assert!(!FlowSpecComponent::TcpFlags(vec![]).uses_numeric_operators()); assert!(!FlowSpecComponent::Fragment(vec![]).uses_numeric_operators()); - assert!(!FlowSpecComponent::DestinationIpv6Prefix { - offset: 0, - prefix: prefix.clone() - } - .uses_numeric_operators()); + assert!( + !FlowSpecComponent::DestinationIpv6Prefix { offset: 0, prefix } + .uses_numeric_operators() + ); assert!(!FlowSpecComponent::SourceIpv6Prefix { offset: 0, prefix }.uses_numeric_operators()); } @@ -471,8 +470,8 @@ mod component_tests { // Components that don't use bitmask operators let prefix = NetworkPrefix::from_str("192.0.2.0/24").unwrap(); - assert!(!FlowSpecComponent::DestinationPrefix(prefix.clone()).uses_bitmask_operators()); - assert!(!FlowSpecComponent::SourcePrefix(prefix.clone()).uses_bitmask_operators()); + assert!(!FlowSpecComponent::DestinationPrefix(prefix).uses_bitmask_operators()); + assert!(!FlowSpecComponent::SourcePrefix(prefix).uses_bitmask_operators()); assert!(!FlowSpecComponent::IpProtocol(vec![]).uses_bitmask_operators()); assert!(!FlowSpecComponent::Port(vec![]).uses_bitmask_operators()); assert!(!FlowSpecComponent::DestinationPort(vec![]).uses_bitmask_operators()); @@ -482,11 +481,10 @@ mod component_tests { assert!(!FlowSpecComponent::PacketLength(vec![]).uses_bitmask_operators()); assert!(!FlowSpecComponent::Dscp(vec![]).uses_bitmask_operators()); assert!(!FlowSpecComponent::FlowLabel(vec![]).uses_bitmask_operators()); - assert!(!FlowSpecComponent::DestinationIpv6Prefix { - offset: 0, - prefix: prefix.clone() - } - .uses_bitmask_operators()); + assert!( + !FlowSpecComponent::DestinationIpv6Prefix { offset: 0, prefix } + .uses_bitmask_operators() + ); assert!(!FlowSpecComponent::SourceIpv6Prefix { offset: 0, prefix }.uses_bitmask_operators()); } @@ -725,7 +723,7 @@ mod nlri_parsing_tests { let prefix = NetworkPrefix::from_str("2001:db8::/64").unwrap(); let nlri = FlowSpecNlri::new(vec![FlowSpecComponent::DestinationIpv6Prefix { offset: 32, - prefix: prefix.clone(), + prefix, }]); let encoded = encode_flowspec_nlri(&nlri);