diff --git a/README.md b/README.md index 18cb2e7..a4d7a0c 100644 --- a/README.md +++ b/README.md @@ -464,7 +464,7 @@ Each bullet below is a standalone, one-PR-sized deliverable unless noted otherwi - [x] **5. Link-local / scope IDs / solicited-node multicast MAC** — `fe80::/10` handling, `%ifindex` scope parsing, `33:33:ff:XX:XX:XX` MAC derivation from the low 24 bits of the target IPv6 address. Prereq for task 6 (NDP). - [ ] **6. NDP (Neighbor Discovery Protocol)** — `NdpHandler` mirroring `ArpHandler`: Neighbor Solicitation and Neighbor Advertisement message types, atomic NDP cache with fast-path lookup, auto-resolution on send, gratuitous NA on bind (parallel to our Gratuitous ARP feature), and seeding the cache from `/proc/net/ipv6_neigh` on Linux. - [x] **7. ICMPv6 echo reply** — auto-respond to `ping6`, parallel to our existing IPv4 ICMP echo reply. -- [ ] **8. ICMPv6 error handling** — Destination Unreachable, Packet Too Big (with Next-Hop MTU), Time Exceeded, and Parameter Problem parsed and matched back to the originating socket. Plugs into the existing per-socket error queue (introduced for IPv4 ICMP errors) so `take_error()` works for IPv6 destinations too. +- [x] **8. ICMPv6 error handling** — Destination Unreachable, Packet Too Big (with Next-Hop MTU), Time Exceeded, and Parameter Problem parsed and matched back to the originating socket. Plugs into the existing per-socket error queue (introduced for IPv4 ICMP errors) so `take_error()` works for IPv6 destinations too. *(PR [#58](https://github.com/gspivey/dpdk-stdlib-rust/pull/58), 24 tests)* - [ ] **9. Performance tests** — TRex PPS run at 64 / 512 / 1400B, plus the synthetic CPU-only benchmark, compared against the IPv4 baseline. Results posted to `docs/perf-test-log.md`. No PPS regression vs IPv4 required to cross off the IPv6 feature. **Encap: IPv6 outer** — Adds IPv6 outer support to all three encapsulation protocols (VXLAN, GENEVE, GUE), closing out dual-stack encap in a single PR. Depends on IPv6 tasks 1 (header build/parse), 2 (UDP pseudo-header checksum), and 4 (offload flags). Does NOT require NDP or ICMPv6 — only the wire-format subset of IPv6. diff --git a/docs/perf-test-log.md b/docs/perf-test-log.md index 9af7334..bd26dcc 100644 --- a/docs/perf-test-log.md +++ b/docs/perf-test-log.md @@ -10,6 +10,59 @@ Each entry captures the git context, test configuration, results, and analysis. --- +## Run #17: ICMPv6 Error Handling — No Regression + +| Field | Value | +|-------|-------| +| **Date** | 2026-05-20 | +| **Git Hash** | `62d8c5c` | +| **Branch** | `agent/icmpv6-error-handling` | +| **PR** | [#58](https://github.com/gspivey/dpdk-stdlib-rust/pull/58) | + +### Changes Since Run #16 + +1. **`62d8c5c` — ICMPv6 error handling with socket error queue integration.** Added ICMPv6 error message parsing (Destination Unreachable, Packet Too Big, Time Exceeded, Parameter Problem), extraction of original IPv6+UDP datagram context (src/dst IP + ports), mapping to `io::Error` kinds matching Linux errno conventions, and integration with the existing bounded per-socket error queue via `take_error()`. The ICMPv6 error processing is wired into the RX path but only fires on received ICMPv6 error packets matching the socket's local port — zero impact on the UDP hot path. + +### Synthetic PPS Benchmark (CPU-only, no NIC) + +Measures `process_frame_zerocopy()` throughput on stub backend (500K iterations, warmed up). + +| Scenario | PPS (K) | ns/pkt | Overhead vs baseline | +|---|---|---|---| +| No VLAN config (baseline, untagged) | 1,337 | 748 | — | +| No VLAN config (baseline, tagged frame) | 1,212 | 825 | -9.4% | +| PortTagging mode (matching VID) | 1,223 | 818 | -8.5% | +| Access mode (untagged frame) | 1,327 | 754 | -0.8% | +| Access mode (matching VID) | 1,176 | 850 | -12.0% | +| Trunk mode (VID in allowed set) | 1,146 | 873 | -14.3% | +| Trunk mode (untagged, native_vlan) | 1,343 | 745 | baseline | +| PortTagging DROP (wrong VID) | 20,025 | 50 | — | +| PortTagging DROP (untagged) | 34,809 | 29 | — | + +### HW VLAN Strip Benchmark (CPU-only, no NIC) + +| Approach | PPS (K) | ns/pkt | Notes | +|---|---|---|---| +| A: Reconstruct frame + detect_vlan parse | 972 | 1,028 | Legacy: Vec alloc + memcpy per packet | +| B: Direct hw_vlan_tci (no reconstruction) | 1,282 | 780 | Current: zero-alloc TCI passthrough | + +**Speedup: 1.32x (248 ns saved per packet).** + +### ICMPv6 Error Parse Benchmark (CPU-only) + +| Operation | Iterations | ns/op | +|---|---|---| +| ICMPv6 error parse | 10,000 | 124 | +| ICMPv6 echo build+parse | 10,000 | 1,228 | + +### Analysis + +**No performance regression from ICMPv6 error handling.** The synthetic PPS numbers are consistent with Run #16 (baseline 1,337K vs 1,012K in Run #16 — improvement is due to different host machine, not code changes). The ICMPv6 error parsing path is only invoked when an ICMPv6 error packet arrives (type 1-4), which is a rare event in normal operation. The main UDP RX hot path (`process_frame_zerocopy`) is unchanged for non-ICMPv6 packets. + +**ICMPv6 error parse cost: 124 ns/packet.** This is ~6x cheaper than a full echo build+parse cycle (1,228 ns) because error parsing only extracts addresses and ports from the embedded original datagram without building a response frame. + +--- + ## Run #16: Eliminate HW VLAN Frame Reconstruction | Field | Value | diff --git a/dpdk-udp/src/icmpv6.rs b/dpdk-udp/src/icmpv6.rs index 1a8cb53..64c787d 100644 --- a/dpdk-udp/src/icmpv6.rs +++ b/dpdk-udp/src/icmpv6.rs @@ -1,17 +1,24 @@ //! ICMPv6 (Internet Control Message Protocol for IPv6) implementation //! -//! Handles ICMPv6 echo request/reply (ping6) for IPv6 network diagnostics. -//! Parallels the IPv4 ICMP echo reply in `icmp.rs`. +//! Handles ICMPv6 echo request/reply (ping6) and ICMPv6 error processing. +//! Parallels the IPv4 ICMP implementation in `icmp.rs`. +//! +//! ICMPv6 error messages (Destination Unreachable, Packet Too Big, Time Exceeded, +//! Parameter Problem) carry as much of the invoking packet as possible without +//! exceeding the minimum IPv6 MTU. For UDP, the embedded original packet contains +//! the IPv6 header + UDP header, which lets us match errors back to the originating +//! socket and surface them via `take_error()`. //! //! ICMPv6 uses the IPv6 pseudo-header in its checksum calculation (RFC 4443 §2.3), //! unlike IPv4 ICMP which checksums only the ICMP message itself. +use std::io; use std::net::Ipv6Addr; use crate::ipv6::{ ETH_TYPE_IPV6, IPV6_HEADER_LEN, IP_PROTO_ICMPV6, walk_extension_headers, }; -use crate::ETH_HEADER_LEN; +use crate::{ETH_HEADER_LEN, UDP_HEADER_LEN}; // ============================================================================ // Constants @@ -26,12 +33,65 @@ pub const ICMPV6_TYPE_ECHO_REPLY: u8 = 129; /// ICMPv6 code for echo messages (always 0) pub const ICMPV6_CODE_ECHO: u8 = 0; -/// ICMPv6 header size: type(1) + code(1) + checksum(2) + identifier(2) + sequence(2) +/// ICMPv6 header size: type(1) + code(1) + checksum(2) + message body(4) pub const ICMPV6_HEADER_LEN: usize = 8; /// Minimum ICMPv6 echo packet: Ethernet + IPv6 + ICMPv6 header pub const MIN_ICMPV6_PACKET_LEN: usize = ETH_HEADER_LEN + IPV6_HEADER_LEN + ICMPV6_HEADER_LEN; +// ICMPv6 error types (RFC 4443 §3) + +/// ICMPv6 type: Destination Unreachable (RFC 4443 §3.1) +pub const ICMPV6_TYPE_DEST_UNREACHABLE: u8 = 1; + +/// ICMPv6 type: Packet Too Big (RFC 4443 §3.2) +pub const ICMPV6_TYPE_PACKET_TOO_BIG: u8 = 2; + +/// ICMPv6 type: Time Exceeded (RFC 4443 §3.3) +pub const ICMPV6_TYPE_TIME_EXCEEDED: u8 = 3; + +/// ICMPv6 type: Parameter Problem (RFC 4443 §3.4) +pub const ICMPV6_TYPE_PARAMETER_PROBLEM: u8 = 4; + +// Destination Unreachable codes (RFC 4443 §3.1) + +/// No route to destination +pub const ICMPV6_CODE_NO_ROUTE: u8 = 0; +/// Communication with destination administratively prohibited +pub const ICMPV6_CODE_ADMIN_PROHIBITED: u8 = 1; +/// Beyond scope of source address +pub const ICMPV6_CODE_BEYOND_SCOPE: u8 = 2; +/// Address unreachable +pub const ICMPV6_CODE_ADDR_UNREACHABLE: u8 = 3; +/// Port unreachable +pub const ICMPV6_CODE_PORT_UNREACHABLE: u8 = 4; +/// Source address failed ingress/egress policy (RFC 5095) +pub const ICMPV6_CODE_POLICY_FAIL: u8 = 5; +/// Reject route to destination (RFC 6550) +pub const ICMPV6_CODE_REJECT_ROUTE: u8 = 6; + +// Time Exceeded codes (RFC 4443 §3.3) + +/// Hop limit exceeded in transit +pub const ICMPV6_CODE_HOP_LIMIT_EXCEEDED: u8 = 0; +/// Fragment reassembly time exceeded +pub const ICMPV6_CODE_FRAG_REASSEMBLY_EXCEEDED: u8 = 1; + +// Parameter Problem codes (RFC 4443 §3.4) + +/// Erroneous header field encountered +pub const ICMPV6_CODE_ERRONEOUS_HEADER: u8 = 0; +/// Unrecognized Next Header type encountered +pub const ICMPV6_CODE_UNRECOGNIZED_NEXT_HEADER: u8 = 1; +/// Unrecognized IPv6 option encountered +pub const ICMPV6_CODE_UNRECOGNIZED_OPTION: u8 = 2; + +/// IP protocol number for UDP (used when parsing original datagram in ICMPv6 errors) +const IP_PROTO_UDP: u8 = 17; + +/// Minimum ICMPv6 error payload: original IPv6 header + UDP ports (first 8 bytes) +pub const MIN_ICMPV6_ERROR_PAYLOAD: usize = IPV6_HEADER_LEN + UDP_HEADER_LEN; + // ============================================================================ // ICMPv6 Packet Structure // ============================================================================ @@ -276,11 +336,211 @@ pub fn build_echo6_request( }) } +// ============================================================================ +// ICMPv6 Error Handling +// ============================================================================ + +/// Parsed ICMPv6 error with context from the original datagram. +/// +/// ICMPv6 error messages (types 1-4) carry as much of the invoking packet as +/// possible. For UDP, the embedded original packet contains the IPv6 header + +/// UDP header (src/dst ports), which lets us match errors to the originating socket. +#[derive(Debug, Clone)] +pub struct Icmpv6ErrorInfo { + /// ICMPv6 error type (1=Dest Unreachable, 2=Packet Too Big, 3=Time Exceeded, 4=Parameter Problem) + pub icmp_type: u8, + /// ICMPv6 error code (sub-type within the error category) + pub icmp_code: u8, + /// IPv6 address of the router/host that generated the error + pub error_source: Ipv6Addr, + /// Original destination IPv6 from the packet that triggered the error + pub original_dst_ip: Ipv6Addr, + /// Original source IPv6 from the packet that triggered the error + pub original_src_ip: Ipv6Addr, + /// Original destination port (from the UDP header in the ICMPv6 payload) + pub original_dst_port: u16, + /// Original source port (from the UDP header in the ICMPv6 payload) + pub original_src_port: u16, + /// MTU value (only valid for Packet Too Big, type 2) + pub mtu: u32, +} + +impl Icmpv6ErrorInfo { + /// Convert this ICMPv6 error into an `io::Error` matching Linux kernel behavior. + /// + /// Linux maps ICMPv6 errors to errno values surfaced via `SO_ERROR` / + /// `take_error()`. We replicate that mapping here. + pub fn to_io_error(&self) -> io::Error { + match (self.icmp_type, self.icmp_code) { + (ICMPV6_TYPE_DEST_UNREACHABLE, ICMPV6_CODE_NO_ROUTE) => io::Error::new( + io::ErrorKind::Other, + format!("ICMPv6: no route to destination (from {})", self.error_source), + ), + (ICMPV6_TYPE_DEST_UNREACHABLE, ICMPV6_CODE_ADMIN_PROHIBITED) => io::Error::new( + io::ErrorKind::PermissionDenied, + format!("ICMPv6: administratively prohibited (from {})", self.error_source), + ), + (ICMPV6_TYPE_DEST_UNREACHABLE, ICMPV6_CODE_BEYOND_SCOPE) => io::Error::new( + io::ErrorKind::Other, + format!("ICMPv6: beyond scope of source address (from {})", self.error_source), + ), + (ICMPV6_TYPE_DEST_UNREACHABLE, ICMPV6_CODE_ADDR_UNREACHABLE) => io::Error::new( + io::ErrorKind::Other, + format!("ICMPv6: address unreachable (from {})", self.error_source), + ), + (ICMPV6_TYPE_DEST_UNREACHABLE, ICMPV6_CODE_PORT_UNREACHABLE) => io::Error::new( + io::ErrorKind::ConnectionRefused, + format!("ICMPv6: port unreachable (from {})", self.error_source), + ), + (ICMPV6_TYPE_DEST_UNREACHABLE, code) => io::Error::new( + io::ErrorKind::Other, + format!("ICMPv6: destination unreachable code {} (from {})", code, self.error_source), + ), + (ICMPV6_TYPE_PACKET_TOO_BIG, _) => io::Error::new( + io::ErrorKind::Other, + format!("ICMPv6: packet too big, MTU {} (from {})", self.mtu, self.error_source), + ), + (ICMPV6_TYPE_TIME_EXCEEDED, ICMPV6_CODE_HOP_LIMIT_EXCEEDED) => io::Error::new( + io::ErrorKind::TimedOut, + format!("ICMPv6: hop limit exceeded in transit (from {})", self.error_source), + ), + (ICMPV6_TYPE_TIME_EXCEEDED, ICMPV6_CODE_FRAG_REASSEMBLY_EXCEEDED) => io::Error::new( + io::ErrorKind::TimedOut, + format!("ICMPv6: fragment reassembly time exceeded (from {})", self.error_source), + ), + (ICMPV6_TYPE_PARAMETER_PROBLEM, _) => io::Error::new( + io::ErrorKind::InvalidData, + format!("ICMPv6: parameter problem (from {})", self.error_source), + ), + (typ, code) => io::Error::new( + io::ErrorKind::Other, + format!("ICMPv6 error type {} code {} (from {})", typ, code, self.error_source), + ), + } + } +} + +/// Parse an ICMPv6 error message and extract the original datagram context. +/// +/// ICMPv6 error messages have this structure: +/// ```text +/// [Ethernet 14B][Outer IPv6 40B][ICMPv6 Header 8B][Original IPv6 Header 40B][Original Transport 8B+] +/// ``` +/// +/// We extract the original IPv6 src/dst and the original UDP src/dst ports from +/// the embedded datagram, so the socket layer can match the error to the right socket. +/// +/// Returns `None` if the frame is not a valid ICMPv6 error for a UDP datagram. +pub fn parse_icmpv6_error(frame: &[u8]) -> Option { + let layout = crate::detect_vlan(frame, None)?; + let l3 = layout.l3_offset; + + if layout.ethertype != ETH_TYPE_IPV6 { + return None; + } + if frame.len() < l3 + IPV6_HEADER_LEN { + return None; + } + if (frame[l3] >> 4) != 6 { + return None; + } + + // Walk extension headers to find ICMPv6 + let nh = walk_extension_headers(&frame[l3..])?; + if nh.protocol != IP_PROTO_ICMPV6 { + return None; + } + + let icmp_start = l3 + nh.payload_offset; + if frame.len() < icmp_start + ICMPV6_HEADER_LEN { + return None; + } + + let icmp_type = frame[icmp_start]; + let icmp_code = frame[icmp_start + 1]; + + // Only process error types (1-4) + if !matches!( + icmp_type, + ICMPV6_TYPE_DEST_UNREACHABLE + | ICMPV6_TYPE_PACKET_TOO_BIG + | ICMPV6_TYPE_TIME_EXCEEDED + | ICMPV6_TYPE_PARAMETER_PROBLEM + ) { + return None; + } + + // For Packet Too Big (type 2), bytes 4-7 of the ICMPv6 header contain the MTU + let mtu = if icmp_type == ICMPV6_TYPE_PACKET_TOO_BIG { + u32::from_be_bytes([ + frame[icmp_start + 4], + frame[icmp_start + 5], + frame[icmp_start + 6], + frame[icmp_start + 7], + ]) + } else { + 0 + }; + + // The original packet starts after the ICMPv6 header + let orig_pkt_start = icmp_start + ICMPV6_HEADER_LEN; + if frame.len() < orig_pkt_start + IPV6_HEADER_LEN + UDP_HEADER_LEN { + return None; + } + + // Parse the original IPv6 header + let orig_ipv6 = &frame[orig_pkt_start..]; + if (orig_ipv6[0] >> 4) != 6 { + return None; + } + + let original_src_ip = Ipv6Addr::from(<[u8; 16]>::try_from(&orig_ipv6[8..24]).unwrap()); + let original_dst_ip = Ipv6Addr::from(<[u8; 16]>::try_from(&orig_ipv6[24..40]).unwrap()); + + // Walk extension headers in the original packet to find UDP + let remaining = &frame[orig_pkt_start..]; + let orig_nh = walk_extension_headers(remaining)?; + if orig_nh.protocol != IP_PROTO_UDP { + return None; + } + + let orig_udp_start = orig_pkt_start + orig_nh.payload_offset; + if frame.len() < orig_udp_start + 4 { + return None; + } + + let original_src_port = u16::from_be_bytes([frame[orig_udp_start], frame[orig_udp_start + 1]]); + let original_dst_port = u16::from_be_bytes([frame[orig_udp_start + 2], frame[orig_udp_start + 3]]); + + // Extract error source from the outer IPv6 header + let error_source = Ipv6Addr::from(<[u8; 16]>::try_from(&frame[l3 + 8..l3 + 24]).unwrap()); + + Some(Icmpv6ErrorInfo { + icmp_type, + icmp_code, + error_source, + original_dst_ip, + original_src_ip, + original_dst_port, + original_src_port, + mtu, + }) +} + +/// Result of processing an ICMPv6 packet: either a reply frame to send, or +/// an error to queue on the matching socket. +pub enum Icmpv6Action { + /// An echo reply frame that should be transmitted back. + Reply(Vec), + /// An ICMPv6 error that should be queued on the originating socket. + Error(Icmpv6ErrorInfo), +} + // ============================================================================ // ICMPv6 Handler // ============================================================================ -/// Handles ICMPv6 echo request/reply for IPv6 sockets. +/// Handles ICMPv6 echo request/reply and error processing for IPv6 sockets. /// /// Parallels `IcmpHandler` for IPv4. pub struct Icmpv6Handler { @@ -304,7 +564,7 @@ impl Icmpv6Handler { } } - /// Process an incoming ICMPv6 packet. + /// Process an incoming ICMPv6 packet (legacy API — echo only). /// /// Returns an echo reply frame if this was an echo request for one of our /// IPv6 addresses, or `None` if no response is needed. @@ -316,6 +576,99 @@ impl Icmpv6Handler { } None } + + /// Process an incoming ICMPv6 packet, handling both echo requests and error messages. + /// + /// Returns `Some(Icmpv6Action::Reply(frame))` for echo requests addressed to us, + /// or `Some(Icmpv6Action::Error(info))` for ICMPv6 errors that reference a UDP + /// datagram originating from one of our local IPs. + pub fn process_icmpv6_full(&self, frame: &[u8]) -> Option { + // Try echo request first (most common in-bound ICMPv6) + if let Some(packet) = parse_icmpv6_packet(frame) { + if packet.is_echo_request() && self.local_ips.contains(&packet.dst_ip) { + let reply = packet.make_echo_reply(self.local_mac)?; + return Some(Icmpv6Action::Reply(build_icmpv6_frame(&reply))); + } + } + + // Try ICMPv6 error (types 1-4 with embedded original datagram) + if let Some(error_info) = parse_icmpv6_error(frame) { + // Only accept errors about datagrams that originated from us + if self.local_ips.contains(&error_info.original_src_ip) { + return Some(Icmpv6Action::Error(error_info)); + } + } + + None + } +} + +/// Build an ICMPv6 error frame for testing purposes. +/// +/// Constructs a valid ICMPv6 error message containing the original IPv6+UDP +/// headers as the error payload. +#[cfg(test)] +pub fn build_icmpv6_error_frame( + src_mac: [u8; 6], + dst_mac: [u8; 6], + error_source: Ipv6Addr, + error_dst: Ipv6Addr, + icmp_type: u8, + icmp_code: u8, + mtu: u32, + original_src_ip: Ipv6Addr, + original_dst_ip: Ipv6Addr, + original_src_port: u16, + original_dst_port: u16, +) -> Vec { + // Original packet: IPv6 header (40B) + UDP header (8B) + let orig_pkt_len = IPV6_HEADER_LEN + UDP_HEADER_LEN; + let icmp_len = ICMPV6_HEADER_LEN + orig_pkt_len; + let total_len = ETH_HEADER_LEN + IPV6_HEADER_LEN + icmp_len; + let mut frame = vec![0u8; total_len]; + + // Ethernet header + frame[0..6].copy_from_slice(&dst_mac); + frame[6..12].copy_from_slice(&src_mac); + frame[12..14].copy_from_slice(Ð_TYPE_IPV6.to_be_bytes()); + + // Outer IPv6 header + let ip = ETH_HEADER_LEN; + frame[ip] = 0x60; // version 6 + frame[ip + 4..ip + 6].copy_from_slice(&(icmp_len as u16).to_be_bytes()); + frame[ip + 6] = IP_PROTO_ICMPV6; + frame[ip + 7] = 64; // hop limit + frame[ip + 8..ip + 24].copy_from_slice(&error_source.octets()); + frame[ip + 24..ip + 40].copy_from_slice(&error_dst.octets()); + + // ICMPv6 header + let icmp = ETH_HEADER_LEN + IPV6_HEADER_LEN; + frame[icmp] = icmp_type; + frame[icmp + 1] = icmp_code; + // Bytes 4-7: MTU for Packet Too Big, unused (zero) for others + frame[icmp + 4..icmp + 8].copy_from_slice(&mtu.to_be_bytes()); + + // Original IPv6 header (embedded in ICMPv6 payload) + let orig = icmp + ICMPV6_HEADER_LEN; + frame[orig] = 0x60; // version 6 + // Payload length = UDP header (8 bytes) + frame[orig + 4..orig + 6].copy_from_slice(&(UDP_HEADER_LEN as u16).to_be_bytes()); + frame[orig + 6] = IP_PROTO_UDP; // Next Header = UDP + frame[orig + 7] = 64; // hop limit + frame[orig + 8..orig + 24].copy_from_slice(&original_src_ip.octets()); + frame[orig + 24..orig + 40].copy_from_slice(&original_dst_ip.octets()); + + // Original UDP header (first 8 bytes) + let udp = orig + IPV6_HEADER_LEN; + frame[udp..udp + 2].copy_from_slice(&original_src_port.to_be_bytes()); + frame[udp + 2..udp + 4].copy_from_slice(&original_dst_port.to_be_bytes()); + frame[udp + 4..udp + 6].copy_from_slice(&(UDP_HEADER_LEN as u16).to_be_bytes()); + + // Compute ICMPv6 checksum + let cksum = icmpv6_checksum(&error_source, &error_dst, &frame[icmp..]); + frame[icmp + 2..icmp + 4].copy_from_slice(&cksum.to_be_bytes()); + + frame } // ============================================================================ @@ -664,4 +1017,446 @@ mod tests { ); assert!(ns_per_op < 10_000, "build+parse too slow: {} ns/op", ns_per_op); } + + // ========================================================================= + // ICMPv6 Error Handling Tests + // ========================================================================= + + fn router_ip() -> Ipv6Addr { + "2001:db8::ffff".parse().unwrap() + } + + // --- Constants --- + + #[test] + fn error_constants_are_correct() { + assert_eq!(ICMPV6_TYPE_DEST_UNREACHABLE, 1); + assert_eq!(ICMPV6_TYPE_PACKET_TOO_BIG, 2); + assert_eq!(ICMPV6_TYPE_TIME_EXCEEDED, 3); + assert_eq!(ICMPV6_TYPE_PARAMETER_PROBLEM, 4); + assert_eq!(ICMPV6_CODE_NO_ROUTE, 0); + assert_eq!(ICMPV6_CODE_ADMIN_PROHIBITED, 1); + assert_eq!(ICMPV6_CODE_BEYOND_SCOPE, 2); + assert_eq!(ICMPV6_CODE_ADDR_UNREACHABLE, 3); + assert_eq!(ICMPV6_CODE_PORT_UNREACHABLE, 4); + assert_eq!(ICMPV6_CODE_HOP_LIMIT_EXCEEDED, 0); + assert_eq!(ICMPV6_CODE_FRAG_REASSEMBLY_EXCEEDED, 1); + assert_eq!(ICMPV6_CODE_ERRONEOUS_HEADER, 0); + assert_eq!(ICMPV6_CODE_UNRECOGNIZED_NEXT_HEADER, 1); + assert_eq!(ICMPV6_CODE_UNRECOGNIZED_OPTION, 2); + assert_eq!(MIN_ICMPV6_ERROR_PAYLOAD, 40 + 8); // IPv6 + UDP + } + + // --- parse_icmpv6_error --- + + #[test] + fn parse_dest_unreachable_port() { + let frame = build_icmpv6_error_frame( + SRC_MAC, DST_MAC, + router_ip(), dst_ip(), + ICMPV6_TYPE_DEST_UNREACHABLE, ICMPV6_CODE_PORT_UNREACHABLE, + 0, + src_ip(), "2001:db8::99".parse().unwrap(), + 12345, 80, + ); + let info = parse_icmpv6_error(&frame).unwrap(); + assert_eq!(info.icmp_type, ICMPV6_TYPE_DEST_UNREACHABLE); + assert_eq!(info.icmp_code, ICMPV6_CODE_PORT_UNREACHABLE); + assert_eq!(info.error_source, router_ip()); + assert_eq!(info.original_src_ip, src_ip()); + assert_eq!(info.original_dst_ip, "2001:db8::99".parse::().unwrap()); + assert_eq!(info.original_src_port, 12345); + assert_eq!(info.original_dst_port, 80); + assert_eq!(info.mtu, 0); + } + + #[test] + fn parse_dest_unreachable_no_route() { + let frame = build_icmpv6_error_frame( + SRC_MAC, DST_MAC, + router_ip(), dst_ip(), + ICMPV6_TYPE_DEST_UNREACHABLE, ICMPV6_CODE_NO_ROUTE, + 0, + src_ip(), dst_ip(), + 5000, 6000, + ); + let info = parse_icmpv6_error(&frame).unwrap(); + assert_eq!(info.icmp_type, ICMPV6_TYPE_DEST_UNREACHABLE); + assert_eq!(info.icmp_code, ICMPV6_CODE_NO_ROUTE); + assert_eq!(info.original_src_port, 5000); + assert_eq!(info.original_dst_port, 6000); + } + + #[test] + fn parse_dest_unreachable_admin_prohibited() { + let frame = build_icmpv6_error_frame( + SRC_MAC, DST_MAC, + router_ip(), dst_ip(), + ICMPV6_TYPE_DEST_UNREACHABLE, ICMPV6_CODE_ADMIN_PROHIBITED, + 0, + src_ip(), dst_ip(), + 1000, 2000, + ); + let info = parse_icmpv6_error(&frame).unwrap(); + assert_eq!(info.icmp_code, ICMPV6_CODE_ADMIN_PROHIBITED); + } + + #[test] + fn parse_packet_too_big() { + let frame = build_icmpv6_error_frame( + SRC_MAC, DST_MAC, + router_ip(), dst_ip(), + ICMPV6_TYPE_PACKET_TOO_BIG, 0, + 1280, // MTU + src_ip(), dst_ip(), + 9000, 9001, + ); + let info = parse_icmpv6_error(&frame).unwrap(); + assert_eq!(info.icmp_type, ICMPV6_TYPE_PACKET_TOO_BIG); + assert_eq!(info.icmp_code, 0); + assert_eq!(info.mtu, 1280); + assert_eq!(info.original_src_port, 9000); + assert_eq!(info.original_dst_port, 9001); + } + + #[test] + fn parse_time_exceeded_hop_limit() { + let frame = build_icmpv6_error_frame( + SRC_MAC, DST_MAC, + router_ip(), dst_ip(), + ICMPV6_TYPE_TIME_EXCEEDED, ICMPV6_CODE_HOP_LIMIT_EXCEEDED, + 0, + src_ip(), dst_ip(), + 4000, 5000, + ); + let info = parse_icmpv6_error(&frame).unwrap(); + assert_eq!(info.icmp_type, ICMPV6_TYPE_TIME_EXCEEDED); + assert_eq!(info.icmp_code, ICMPV6_CODE_HOP_LIMIT_EXCEEDED); + } + + #[test] + fn parse_time_exceeded_frag_reassembly() { + let frame = build_icmpv6_error_frame( + SRC_MAC, DST_MAC, + router_ip(), dst_ip(), + ICMPV6_TYPE_TIME_EXCEEDED, ICMPV6_CODE_FRAG_REASSEMBLY_EXCEEDED, + 0, + src_ip(), dst_ip(), + 7000, 8000, + ); + let info = parse_icmpv6_error(&frame).unwrap(); + assert_eq!(info.icmp_type, ICMPV6_TYPE_TIME_EXCEEDED); + assert_eq!(info.icmp_code, ICMPV6_CODE_FRAG_REASSEMBLY_EXCEEDED); + } + + #[test] + fn parse_parameter_problem() { + let frame = build_icmpv6_error_frame( + SRC_MAC, DST_MAC, + router_ip(), dst_ip(), + ICMPV6_TYPE_PARAMETER_PROBLEM, ICMPV6_CODE_ERRONEOUS_HEADER, + 0, + src_ip(), dst_ip(), + 3000, 4000, + ); + let info = parse_icmpv6_error(&frame).unwrap(); + assert_eq!(info.icmp_type, ICMPV6_TYPE_PARAMETER_PROBLEM); + assert_eq!(info.icmp_code, ICMPV6_CODE_ERRONEOUS_HEADER); + } + + #[test] + fn parse_rejects_echo_request_as_error() { + let frame = build_echo6_request(SRC_MAC, DST_MAC, src_ip(), dst_ip(), 1, 1, b"ping"); + assert!(parse_icmpv6_error(&frame).is_none()); + } + + #[test] + fn parse_rejects_echo_reply_as_error() { + let packet = Icmpv6Packet { + src_mac: SRC_MAC, + dst_mac: DST_MAC, + src_ip: src_ip(), + dst_ip: dst_ip(), + hop_limit: 64, + icmp_type: ICMPV6_TYPE_ECHO_REPLY, + icmp_code: ICMPV6_CODE_ECHO, + checksum: 0, + identifier: 1, + sequence: 1, + payload: vec![], + }; + let frame = build_icmpv6_frame(&packet); + assert!(parse_icmpv6_error(&frame).is_none()); + } + + #[test] + fn parse_rejects_truncated_error() { + let frame = build_icmpv6_error_frame( + SRC_MAC, DST_MAC, + router_ip(), dst_ip(), + ICMPV6_TYPE_DEST_UNREACHABLE, ICMPV6_CODE_PORT_UNREACHABLE, + 0, + src_ip(), dst_ip(), + 1000, 2000, + ); + // Truncate before the original UDP ports + let truncated = &frame[..frame.len() - 5]; + assert!(parse_icmpv6_error(truncated).is_none()); + } + + #[test] + fn parse_error_rejects_ipv4_frame() { + let frame = crate::build_udp_frame( + &SRC_MAC, &DST_MAC, + "10.0.0.1".parse().unwrap(), + "10.0.0.2".parse().unwrap(), + 1000, 2000, b"ipv4", 64, + ).unwrap(); + assert!(parse_icmpv6_error(&frame).is_none()); + } + + #[test] + fn parse_error_rejects_too_short() { + assert!(parse_icmpv6_error(&[0u8; 10]).is_none()); + } + + // --- to_io_error mapping --- + + #[test] + fn error_port_unreachable_maps_to_connection_refused() { + let info = Icmpv6ErrorInfo { + icmp_type: ICMPV6_TYPE_DEST_UNREACHABLE, + icmp_code: ICMPV6_CODE_PORT_UNREACHABLE, + error_source: router_ip(), + original_dst_ip: dst_ip(), + original_src_ip: src_ip(), + original_dst_port: 80, + original_src_port: 12345, + mtu: 0, + }; + let err = info.to_io_error(); + assert_eq!(err.kind(), io::ErrorKind::ConnectionRefused); + } + + #[test] + fn error_admin_prohibited_maps_to_permission_denied() { + let info = Icmpv6ErrorInfo { + icmp_type: ICMPV6_TYPE_DEST_UNREACHABLE, + icmp_code: ICMPV6_CODE_ADMIN_PROHIBITED, + error_source: router_ip(), + original_dst_ip: dst_ip(), + original_src_ip: src_ip(), + original_dst_port: 80, + original_src_port: 12345, + mtu: 0, + }; + let err = info.to_io_error(); + assert_eq!(err.kind(), io::ErrorKind::PermissionDenied); + } + + #[test] + fn error_hop_limit_exceeded_maps_to_timed_out() { + let info = Icmpv6ErrorInfo { + icmp_type: ICMPV6_TYPE_TIME_EXCEEDED, + icmp_code: ICMPV6_CODE_HOP_LIMIT_EXCEEDED, + error_source: router_ip(), + original_dst_ip: dst_ip(), + original_src_ip: src_ip(), + original_dst_port: 80, + original_src_port: 12345, + mtu: 0, + }; + let err = info.to_io_error(); + assert_eq!(err.kind(), io::ErrorKind::TimedOut); + } + + #[test] + fn error_packet_too_big_includes_mtu() { + let info = Icmpv6ErrorInfo { + icmp_type: ICMPV6_TYPE_PACKET_TOO_BIG, + icmp_code: 0, + error_source: router_ip(), + original_dst_ip: dst_ip(), + original_src_ip: src_ip(), + original_dst_port: 80, + original_src_port: 12345, + mtu: 1280, + }; + let err = info.to_io_error(); + assert_eq!(err.kind(), io::ErrorKind::Other); + assert!(err.to_string().contains("1280")); + } + + #[test] + fn error_parameter_problem_maps_to_invalid_data() { + let info = Icmpv6ErrorInfo { + icmp_type: ICMPV6_TYPE_PARAMETER_PROBLEM, + icmp_code: ICMPV6_CODE_UNRECOGNIZED_NEXT_HEADER, + error_source: router_ip(), + original_dst_ip: dst_ip(), + original_src_ip: src_ip(), + original_dst_port: 80, + original_src_port: 12345, + mtu: 0, + }; + let err = info.to_io_error(); + assert_eq!(err.kind(), io::ErrorKind::InvalidData); + } + + #[test] + fn error_no_route_maps_to_other() { + let info = Icmpv6ErrorInfo { + icmp_type: ICMPV6_TYPE_DEST_UNREACHABLE, + icmp_code: ICMPV6_CODE_NO_ROUTE, + error_source: router_ip(), + original_dst_ip: dst_ip(), + original_src_ip: src_ip(), + original_dst_port: 80, + original_src_port: 12345, + mtu: 0, + }; + let err = info.to_io_error(); + assert_eq!(err.kind(), io::ErrorKind::Other); + assert!(err.to_string().contains("no route")); + } + + // --- Icmpv6Action / process_icmpv6_full --- + + #[test] + fn handler_full_responds_to_echo_request() { + let local_mac = DST_MAC; + let local_ip = dst_ip(); + let handler = Icmpv6Handler::new(local_mac, local_ip); + + let request = build_echo6_request(SRC_MAC, local_mac, src_ip(), local_ip, 0x1234, 1, b"ping6"); + match handler.process_icmpv6_full(&request) { + Some(Icmpv6Action::Reply(reply_frame)) => { + let reply = parse_icmpv6_packet(&reply_frame).unwrap(); + assert!(reply.is_echo_reply()); + assert_eq!(reply.identifier, 0x1234); + } + _ => panic!("expected Reply action"), + } + } + + #[test] + fn handler_full_returns_error_for_our_ip() { + let local_mac = DST_MAC; + let local_ip = src_ip(); // our IP is the original source + let handler = Icmpv6Handler::new(local_mac, local_ip); + + let frame = build_icmpv6_error_frame( + SRC_MAC, local_mac, + router_ip(), local_ip, + ICMPV6_TYPE_DEST_UNREACHABLE, ICMPV6_CODE_PORT_UNREACHABLE, + 0, + local_ip, dst_ip(), // original src = our IP + 12345, 80, + ); + match handler.process_icmpv6_full(&frame) { + Some(Icmpv6Action::Error(info)) => { + assert_eq!(info.icmp_type, ICMPV6_TYPE_DEST_UNREACHABLE); + assert_eq!(info.icmp_code, ICMPV6_CODE_PORT_UNREACHABLE); + assert_eq!(info.original_src_port, 12345); + assert_eq!(info.original_dst_port, 80); + } + _ => panic!("expected Error action"), + } + } + + #[test] + fn handler_full_ignores_error_for_other_ip() { + let local_mac = DST_MAC; + let local_ip = dst_ip(); + let handler = Icmpv6Handler::new(local_mac, local_ip); + + // Error references a different source IP (not ours) + let other_ip: Ipv6Addr = "2001:db8::99".parse().unwrap(); + let frame = build_icmpv6_error_frame( + SRC_MAC, local_mac, + router_ip(), local_ip, + ICMPV6_TYPE_DEST_UNREACHABLE, ICMPV6_CODE_PORT_UNREACHABLE, + 0, + other_ip, dst_ip(), // original src = NOT our IP + 12345, 80, + ); + assert!(handler.process_icmpv6_full(&frame).is_none()); + } + + #[test] + fn handler_full_ignores_echo_reply() { + let local_mac = DST_MAC; + let local_ip = dst_ip(); + let handler = Icmpv6Handler::new(local_mac, local_ip); + + let packet = Icmpv6Packet { + src_mac: SRC_MAC, + dst_mac: local_mac, + src_ip: src_ip(), + dst_ip: local_ip, + hop_limit: 64, + icmp_type: ICMPV6_TYPE_ECHO_REPLY, + icmp_code: ICMPV6_CODE_ECHO, + checksum: 0, + identifier: 1, + sequence: 1, + payload: vec![], + }; + let frame = build_icmpv6_frame(&packet); + assert!(handler.process_icmpv6_full(&frame).is_none()); + } + + #[test] + fn handler_full_with_multiple_ips() { + let local_mac = DST_MAC; + let ip1 = src_ip(); + let ip2: Ipv6Addr = "fe80::1".parse().unwrap(); + + let mut handler = Icmpv6Handler::new(local_mac, ip1); + handler.add_local_ip(ip2); + + // Error referencing ip2 as original source + let frame = build_icmpv6_error_frame( + SRC_MAC, local_mac, + router_ip(), ip2, + ICMPV6_TYPE_TIME_EXCEEDED, ICMPV6_CODE_HOP_LIMIT_EXCEEDED, + 0, + ip2, dst_ip(), + 5000, 6000, + ); + match handler.process_icmpv6_full(&frame) { + Some(Icmpv6Action::Error(info)) => { + assert_eq!(info.original_src_ip, ip2); + assert_eq!(info.original_src_port, 5000); + } + _ => panic!("expected Error action for ip2"), + } + } + + // --- Synthetic performance --- + + #[test] + fn perf_parse_icmpv6_error() { + let frame = build_icmpv6_error_frame( + SRC_MAC, DST_MAC, + router_ip(), dst_ip(), + ICMPV6_TYPE_DEST_UNREACHABLE, ICMPV6_CODE_PORT_UNREACHABLE, + 0, + src_ip(), dst_ip(), + 12345, 80, + ); + let iterations = 10_000; + + let start = std::time::Instant::now(); + for _ in 0..iterations { + let _ = parse_icmpv6_error(&frame).unwrap(); + } + let elapsed = start.elapsed(); + let ns_per_op = elapsed.as_nanos() / iterations as u128; + eprintln!( + "[PERF] ICMPv6 error parse: {} iterations in {:?} ({} ns/op)", + iterations, elapsed, ns_per_op + ); + assert!(ns_per_op < 10_000, "error parse too slow: {} ns/op", ns_per_op); + } } diff --git a/dpdk-udp/src/lib.rs b/dpdk-udp/src/lib.rs index 1448817..a181762 100644 --- a/dpdk-udp/src/lib.rs +++ b/dpdk-udp/src/lib.rs @@ -62,7 +62,7 @@ pub mod vxlan; pub use arp::{ArpCache, ArpHandler, ArpPacket}; pub use icmp::{IcmpAction, IcmpErrorInfo, IcmpHandler, IcmpPacket}; -pub use icmpv6::{Icmpv6Handler, Icmpv6Packet, build_icmpv6_frame, parse_icmpv6_packet, icmpv6_checksum, build_echo6_request, ICMPV6_TYPE_ECHO_REQUEST, ICMPV6_TYPE_ECHO_REPLY, ICMPV6_HEADER_LEN}; +pub use icmpv6::{Icmpv6Action, Icmpv6ErrorInfo, Icmpv6Handler, Icmpv6Packet, build_icmpv6_frame, parse_icmpv6_packet, parse_icmpv6_error, icmpv6_checksum, build_echo6_request, ICMPV6_TYPE_ECHO_REQUEST, ICMPV6_TYPE_ECHO_REPLY, ICMPV6_HEADER_LEN, ICMPV6_TYPE_DEST_UNREACHABLE, ICMPV6_TYPE_PACKET_TOO_BIG, ICMPV6_TYPE_TIME_EXCEEDED, ICMPV6_TYPE_PARAMETER_PROBLEM}; pub use backend::{PacketBackend, BackendConfig, BackendType}; pub use backend_dpdk::DpdkBackend; pub use backend_raw::RawSocketBackend; @@ -3026,12 +3026,27 @@ impl UdpSocket { } } - // Handle ICMPv6 (echo reply for ping6) + // Handle ICMPv6 (echo reply + error messages) if layout.ethertype == ETH_TYPE_IPV6 && frame_data.len() > layout.l3_offset + IPV6_HEADER_LEN { if let Some(nh) = ipv6::walk_extension_headers(&frame_data[layout.l3_offset..]) { - if nh.protocol == IP_PROTO_ICMPV6 && self.auto_icmp { - if let Some(reply_frame) = self.icmpv6_handler.process_icmpv6(frame_data) { - let _ = self.socket_backend.send_frame(&reply_frame, None); + if nh.protocol == IP_PROTO_ICMPV6 { + if let Some(action) = self.icmpv6_handler.process_icmpv6_full(frame_data) { + match action { + icmpv6::Icmpv6Action::Reply(reply_frame) => { + if self.auto_icmp { + let _ = self.socket_backend.send_frame(&reply_frame, None); + } + } + icmpv6::Icmpv6Action::Error(error_info) => { + let local_port = match self.local_addr { + SocketAddr::V4(v4) => v4.port(), + SocketAddr::V6(v6) => v6.port(), + }; + if error_info.original_src_port == local_port { + self.queue_icmp_error(error_info.to_io_error()); + } + } + } } perf_inc!(self.perf_counters.rx_icmp_handled); return None;