diff --git a/README.md b/README.md index 229482e..98907b7 100644 --- a/README.md +++ b/README.md @@ -460,7 +460,7 @@ Each bullet below is a standalone, one-PR-sized deliverable unless noted otherwi **IPv6** — Full dual-stack support: IPv6 addresses accepted anywhere IPv4 is today, 40-byte IPv6 headers on the wire, NDP (the IPv6 replacement for ARP), and ICMPv6 (echo + errors). - [x] **1. IPv6 header build/parse** — 40-byte fixed header, plus extension-header chain walk (Hop-by-Hop, Routing, Fragment, Destination Options) to locate the L4 payload. New `dpdk-udp/src/ipv6.rs`. *(PR [#49](https://github.com/gspivey/dpdk-stdlib-rust/pull/49), 34 tests)* -- [ ] **2. UDP over IPv6 checksum** — mandatory IPv6 pseudo-header checksum (unlike IPv4 where UDP checksum is optional). `verify_udp6_checksum` / `udp6_pseudo_header_checksum` helpers parallel to the existing IPv4 helpers. +- [x] **2. UDP over IPv6 checksum** — mandatory IPv6 pseudo-header checksum (unlike IPv4 where UDP checksum is optional). `verify_udp6_checksum` / `udp6_pseudo_header_checksum` helpers parallel to the existing IPv4 helpers. RX path validates IPv6 UDP checksums and rejects zero checksums (RFC 8200 §8.1). *(PR [#61](https://github.com/gspivey/dpdk-stdlib-rust/pull/61), 21 tests)* - [ ] **3. `SocketAddrV6` through `UdpSocket`** — `bind` / `send_to` / `recv_from` / `connect` / `local_addr` / `peer_addr` accept and return IPv6 addresses. `set_only_v6` / `only_v6` socket option. `AddressFamily` state on the socket so the send/recv paths pick the right wire format. - [x] **4. IPv6 hardware offload flags** — TX: set `RTE_MBUF_F_TX_IPV6` + `RTE_MBUF_F_TX_UDP_CKSUM` with the IPv6 pseudo-header checksum in the UDP field. RX: validate IPv6 UDP checksums (honor `PKT_RX_L4_CKSUM_GOOD`). Software fallback on NICs without support. `has_tx_ipv6_cksum_offload()` accessor. *(PR [#55](https://github.com/gspivey/dpdk-stdlib-rust/pull/55), 8 tests)* - [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). diff --git a/docs/perf-test-log.md b/docs/perf-test-log.md index 84b97b7..66381fa 100644 --- a/docs/perf-test-log.md +++ b/docs/perf-test-log.md @@ -2750,4 +2750,86 @@ The IPv6 hardware offload change adds a single ethertype comparison (`u16::from_ - rust-dpdk at 700K/64B: 6.44% drop (within normal variance; native-dpdk also shows 7.76% this run vs ~2% in prior runs, indicating ENA rate-limiter variance) - rust-dpdk at 700K/1400B: 1.02% drop (consistent with Run #23's 1.3%) -**No regressions detected.** The ethertype branch adds zero measurable overhead to the IPv4 hot path. The IPv6 offload code path is not exercised during benchmarks (no IPv6 traffic in integration tests) and will only activate when IPv6 frames are sent through the DPDK backend. \ No newline at end of file +**No regressions detected.** The ethertype branch adds zero measurable overhead to the IPv4 hot path. The IPv6 offload code path is not exercised during benchmarks (no IPv6 traffic in integration tests) and will only activate when IPv6 frames are sent through the DPDK backend. + +## Run #25: IPv6 UDP Checksum Validation — Regression Check + +| Field | Value | +|-------|-------| +| **Date** | 2026-05-21 | +| **Git Hash** | `60dfc50c` | +| **Branch** | `agent/udp6-checksum-validation` | +| **PR** | [#61](https://github.com/gspivey/dpdk-stdlib-rust/pull/61) | +| **GH Actions Run** | [26227356354](https://github.com/gspivey/dpdk-stdlib-rust/actions/runs/26227356354) | +| **Environment** | Hardware PPS: Graviton (ENA, DPDK 23.11). TRex traffic generator. | + +### Changes Since Run #24 + +1. **IPv6 UDP checksum validation in RX path**: `process_frame_zerocopy()` now parses incoming IPv6/UDP frames via `parse_udp6_packet_ref` and validates the mandatory UDP checksum via `verify_udp6_checksum` (RFC 8200 §8.1). +2. **Zero checksum rejection**: IPv6 frames with UDP checksum field = 0 are dropped (mandatory per RFC, unlike IPv4 where 0 means disabled). +3. **21 new tests** covering VLAN-tagged frames, extension headers, various payload sizes, corruption detection, and RX path integration. + +**Key question:** Does the IPv6 fallback parse attempt in the RX path add measurable overhead to IPv4 traffic? (Expected answer: no — `parse_udp6_packet_ref` is only attempted after `parse_udp_packet_ref` returns None, which doesn't happen for IPv4 frames.) + +### Results: Hardware (TRex, Graviton) + +#### 64-byte packets + +| Config | Target PPS | RX pps | Drop % | +|--------|-----------|--------|--------| +| native-dpdk | 70K | 69,998 | 0.00% | +| native-dpdk | 140K | 140,000 | 0.00% | +| native-dpdk | 350K | 349,949 | 0.01% | +| native-dpdk | 700K | 699,888 | 0.02% | +| rust-dpdk | 70K | 69,000 | 1.43% | +| rust-dpdk | 140K | 139,000 | 0.71% | +| rust-dpdk | 350K | 349,000 | 0.29% | +| rust-dpdk | 700K | 698,383 | 0.23% | + +#### 512-byte packets + +| Config | Target PPS | RX pps | Drop % | +|--------|-----------|--------|--------| +| native-dpdk | 70K | 70,000 | 0.00% | +| native-dpdk | 140K | 140,000 | 0.00% | +| native-dpdk | 350K | 349,993 | 0.00% | +| native-dpdk | 700K | 699,882 | 0.02% | +| rust-dpdk | 70K | 69,000 | 1.43% | +| rust-dpdk | 140K | 139,000 | 0.71% | +| rust-dpdk | 350K | 349,000 | 0.29% | +| rust-dpdk | 700K | 698,726 | 0.18% | + +#### 1400-byte packets (near MTU) + +| Config | Target PPS | RX pps | Drop % | +|--------|-----------|--------|--------| +| native-dpdk | 70K | 70,000 | 0.00% | +| native-dpdk | 140K | 140,000 | 0.00% | +| native-dpdk | 350K | 349,986 | 0.00% | +| native-dpdk | 700K | 472,950 | 0.78% | +| rust-dpdk | 70K | 69,000 | 1.43% | +| rust-dpdk | 140K | 138,996 | 0.72% | +| rust-dpdk | 350K | 348,973 | 0.29% | +| rust-dpdk | 700K | 475,467 | 0.17% | + +#### 8500-byte packets (jumbo) + +| Config | Target PPS | RX pps | Drop % | +|--------|-----------|--------|--------| +| native-dpdk | 70K | 70,000 | 0.00% | +| native-dpdk | 140K | 77,260 | 1.39% | +| native-dpdk | 350K | 75,387 | 3.77% | +| rust-dpdk | 70K | 69,000 | 1.43% | +| rust-dpdk | 140K | 77,116 | 1.59% | +| rust-dpdk | 350K | 73,728 | 5.89% | + +### Regression Check vs Run #24 + +The IPv6 UDP checksum validation adds an IPv6 parse fallback path to `process_frame_zerocopy()`. For IPv4 traffic (all benchmark traffic), the IPv6 path is never reached — `parse_udp_packet_ref` succeeds on the first attempt. + +- rust-dpdk at 350K/64B: 0.29% drop (identical to Run #24) +- rust-dpdk at 700K/64B: 0.23% drop (excellent — better than Run #24's 6.44% on x86, consistent with Graviton's higher throughput ceiling) +- rust-dpdk at 700K/1400B: 0.17% drop (consistent with prior runs) +- rust-dpdk at 700K/512B: 0.18% drop (consistent) + +**No regressions detected.** The IPv6 fallback path adds zero overhead to IPv4 traffic because `parse_udp_packet_ref` succeeds immediately for IPv4 frames, and the IPv6 branch is never entered. diff --git a/dpdk-udp/src/ipv6.rs b/dpdk-udp/src/ipv6.rs index cb1adb2..734dff8 100644 --- a/dpdk-udp/src/ipv6.rs +++ b/dpdk-udp/src/ipv6.rs @@ -1039,6 +1039,297 @@ mod tests { assert_eq!(parsed.src_ip, ll); } + // --- UDP6 checksum: VLAN-tagged frames --- + + #[test] + fn udp6_checksum_valid_on_vlan_tagged_frame() { + // Manually construct a VLAN-tagged IPv6/UDP frame with valid checksum + let payload = b"vlan-cksum"; + let inner_len = IPV6_HEADER_LEN + UDP_HEADER_LEN + payload.len(); + let total = ETH_HEADER_LEN + crate::VLAN_TAG_LEN + inner_len; + let mut frame = vec![0u8; total]; + + frame[0..6].copy_from_slice(&DST_MAC); + frame[6..12].copy_from_slice(&SRC_MAC); + frame[12..14].copy_from_slice(&crate::ETH_TYPE_VLAN.to_be_bytes()); + frame[14..16].copy_from_slice(&200u16.to_be_bytes()); + frame[16..18].copy_from_slice(Ð_TYPE_IPV6.to_be_bytes()); + + let l3 = ETH_HEADER_LEN + crate::VLAN_TAG_LEN; + frame[l3] = 0x60; + let udp_len = (UDP_HEADER_LEN + payload.len()) as u16; + frame[l3 + 4..l3 + 6].copy_from_slice(&udp_len.to_be_bytes()); + frame[l3 + 6] = IP_PROTO_UDP; + frame[l3 + 7] = 64; + frame[l3 + 8..l3 + 24].copy_from_slice(&src_ip().octets()); + frame[l3 + 24..l3 + 40].copy_from_slice(&dst_ip().octets()); + + let udp_off = l3 + IPV6_HEADER_LEN; + frame[udp_off..udp_off + 2].copy_from_slice(&5000u16.to_be_bytes()); + frame[udp_off + 2..udp_off + 4].copy_from_slice(&6000u16.to_be_bytes()); + frame[udp_off + 4..udp_off + 6].copy_from_slice(&udp_len.to_be_bytes()); + frame[udp_off + UDP_HEADER_LEN..].copy_from_slice(payload); + + let cksum = udp6_checksum( + &src_ip(), &dst_ip(), + &frame[udp_off..udp_off + UDP_HEADER_LEN], + payload, + ); + frame[udp_off + 6..udp_off + 8].copy_from_slice(&cksum.to_be_bytes()); + + assert!(verify_udp6_checksum(&frame)); + } + + #[test] + fn udp6_checksum_detects_corruption_in_vlan_frame() { + let payload = b"vlan-corrupt"; + let inner_len = IPV6_HEADER_LEN + UDP_HEADER_LEN + payload.len(); + let total = ETH_HEADER_LEN + crate::VLAN_TAG_LEN + inner_len; + let mut frame = vec![0u8; total]; + + frame[0..6].copy_from_slice(&DST_MAC); + frame[6..12].copy_from_slice(&SRC_MAC); + frame[12..14].copy_from_slice(&crate::ETH_TYPE_VLAN.to_be_bytes()); + frame[14..16].copy_from_slice(&100u16.to_be_bytes()); + frame[16..18].copy_from_slice(Ð_TYPE_IPV6.to_be_bytes()); + + let l3 = ETH_HEADER_LEN + crate::VLAN_TAG_LEN; + frame[l3] = 0x60; + let udp_len = (UDP_HEADER_LEN + payload.len()) as u16; + frame[l3 + 4..l3 + 6].copy_from_slice(&udp_len.to_be_bytes()); + frame[l3 + 6] = IP_PROTO_UDP; + frame[l3 + 7] = 64; + frame[l3 + 8..l3 + 24].copy_from_slice(&src_ip().octets()); + frame[l3 + 24..l3 + 40].copy_from_slice(&dst_ip().octets()); + + let udp_off = l3 + IPV6_HEADER_LEN; + frame[udp_off..udp_off + 2].copy_from_slice(&5000u16.to_be_bytes()); + frame[udp_off + 2..udp_off + 4].copy_from_slice(&6000u16.to_be_bytes()); + frame[udp_off + 4..udp_off + 6].copy_from_slice(&udp_len.to_be_bytes()); + frame[udp_off + UDP_HEADER_LEN..].copy_from_slice(payload); + + let cksum = udp6_checksum( + &src_ip(), &dst_ip(), + &frame[udp_off..udp_off + UDP_HEADER_LEN], + payload, + ); + frame[udp_off + 6..udp_off + 8].copy_from_slice(&cksum.to_be_bytes()); + + // Corrupt a payload byte + let last = frame.len() - 1; + frame[last] ^= 0xFF; + assert!(!verify_udp6_checksum(&frame)); + } + + // --- UDP6 checksum: extension headers --- + + #[test] + fn udp6_checksum_with_hop_by_hop_extension() { + // Build frame with Hop-by-Hop extension header before UDP + let payload = b"ext-hdr"; + let ext_len = 8; // minimum extension header size + let inner_len = IPV6_HEADER_LEN + ext_len + UDP_HEADER_LEN + payload.len(); + let total = ETH_HEADER_LEN + inner_len; + let mut frame = vec![0u8; total]; + + 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()); + + let l3 = ETH_HEADER_LEN; + frame[l3] = 0x60; + let ipv6_payload_len = (ext_len + UDP_HEADER_LEN + payload.len()) as u16; + frame[l3 + 4..l3 + 6].copy_from_slice(&ipv6_payload_len.to_be_bytes()); + frame[l3 + 6] = IP_PROTO_HOPOPT; // Next Header = Hop-by-Hop + frame[l3 + 7] = 64; + frame[l3 + 8..l3 + 24].copy_from_slice(&src_ip().octets()); + frame[l3 + 24..l3 + 40].copy_from_slice(&dst_ip().octets()); + + // Hop-by-Hop extension header (8 bytes) + let ext_off = l3 + IPV6_HEADER_LEN; + frame[ext_off] = IP_PROTO_UDP; // Next Header = UDP + frame[ext_off + 1] = 0; // Length = (0+1)*8 = 8 bytes + + // UDP header + let udp_off = ext_off + ext_len; + let udp_len = (UDP_HEADER_LEN + payload.len()) as u16; + frame[udp_off..udp_off + 2].copy_from_slice(&7000u16.to_be_bytes()); + frame[udp_off + 2..udp_off + 4].copy_from_slice(&8000u16.to_be_bytes()); + frame[udp_off + 4..udp_off + 6].copy_from_slice(&udp_len.to_be_bytes()); + frame[udp_off + UDP_HEADER_LEN..].copy_from_slice(payload); + + // Compute checksum (pseudo-header uses the IPv6 addresses, not extension headers) + let cksum = udp6_checksum( + &src_ip(), &dst_ip(), + &frame[udp_off..udp_off + UDP_HEADER_LEN], + payload, + ); + frame[udp_off + 6..udp_off + 8].copy_from_slice(&cksum.to_be_bytes()); + + assert!(verify_udp6_checksum(&frame)); + } + + // --- UDP6 checksum: various payload sizes --- + + #[test] + fn udp6_checksum_empty_payload() { + let frame = build_udp6_frame( + &SRC_MAC, &DST_MAC, src_ip(), dst_ip(), 1, 2, b"", 64, + ).unwrap(); + assert!(verify_udp6_checksum(&frame)); + } + + #[test] + fn udp6_checksum_single_byte_payload() { + let frame = build_udp6_frame( + &SRC_MAC, &DST_MAC, src_ip(), dst_ip(), 1, 2, b"x", 64, + ).unwrap(); + assert!(verify_udp6_checksum(&frame)); + } + + #[test] + fn udp6_checksum_odd_length_payload() { + // Odd-length payload exercises the padding logic in checksum_add + let frame = build_udp6_frame( + &SRC_MAC, &DST_MAC, src_ip(), dst_ip(), 1, 2, b"odd", 64, + ).unwrap(); + assert!(verify_udp6_checksum(&frame)); + } + + #[test] + fn udp6_checksum_max_standard_payload() { + let payload = vec![0xAB; MAX_UDP_PAYLOAD_V6]; + let frame = build_udp6_frame( + &SRC_MAC, &DST_MAC, src_ip(), dst_ip(), 1, 2, &payload, 64, + ).unwrap(); + assert!(verify_udp6_checksum(&frame)); + } + + // --- UDP6 checksum: corruption in different fields --- + + #[test] + fn udp6_checksum_detects_src_ip_corruption() { + let mut frame = build_udp6_frame( + &SRC_MAC, &DST_MAC, src_ip(), dst_ip(), 1, 2, b"data", 64, + ).unwrap(); + // Corrupt source IP (byte 8 of IPv6 header) + frame[ETH_HEADER_LEN + 8] ^= 0x01; + assert!(!verify_udp6_checksum(&frame)); + } + + #[test] + fn udp6_checksum_detects_dst_ip_corruption() { + let mut frame = build_udp6_frame( + &SRC_MAC, &DST_MAC, src_ip(), dst_ip(), 1, 2, b"data", 64, + ).unwrap(); + // Corrupt destination IP (byte 24 of IPv6 header) + frame[ETH_HEADER_LEN + 24] ^= 0x01; + assert!(!verify_udp6_checksum(&frame)); + } + + #[test] + fn udp6_checksum_detects_src_port_corruption() { + let mut frame = build_udp6_frame( + &SRC_MAC, &DST_MAC, src_ip(), dst_ip(), 1000, 2000, b"data", 64, + ).unwrap(); + let udp_off = ETH_HEADER_LEN + IPV6_HEADER_LEN; + frame[udp_off] ^= 0x01; // corrupt src port high byte + assert!(!verify_udp6_checksum(&frame)); + } + + #[test] + fn udp6_checksum_detects_dst_port_corruption() { + let mut frame = build_udp6_frame( + &SRC_MAC, &DST_MAC, src_ip(), dst_ip(), 1000, 2000, b"data", 64, + ).unwrap(); + let udp_off = ETH_HEADER_LEN + IPV6_HEADER_LEN; + frame[udp_off + 2] ^= 0x01; // corrupt dst port high byte + assert!(!verify_udp6_checksum(&frame)); + } + + // --- UDP6 checksum: edge cases --- + + #[test] + fn udp6_checksum_rejects_truncated_frame() { + let frame = build_udp6_frame( + &SRC_MAC, &DST_MAC, src_ip(), dst_ip(), 1, 2, b"data", 64, + ).unwrap(); + // Truncate to just past the UDP header (missing payload) + let truncated = &frame[..ETH_HEADER_LEN + IPV6_HEADER_LEN + UDP_HEADER_LEN]; + // The UDP length field says there's payload, but it's missing + assert!(!verify_udp6_checksum(truncated)); + } + + #[test] + fn udp6_checksum_rejects_non_ipv6_frame() { + // Build an 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!(!verify_udp6_checksum(&frame)); + } + + #[test] + fn udp6_checksum_rejects_too_short_frame() { + // Frame too short to contain even Ethernet + IPv6 header + let frame = vec![0u8; 20]; + assert!(!verify_udp6_checksum(&frame)); + } + + // --- UDP6 pseudo-header checksum properties --- + + #[test] + fn udp6_pseudo_header_checksum_different_lengths() { + let a = udp6_pseudo_header_checksum(&src_ip(), &dst_ip(), 8); + let b = udp6_pseudo_header_checksum(&src_ip(), &dst_ip(), 100); + assert_ne!(a, b); + } + + #[test] + fn udp6_pseudo_header_checksum_different_src() { + let other_src: Ipv6Addr = "2001:db8::ff".parse().unwrap(); + let a = udp6_pseudo_header_checksum(&src_ip(), &dst_ip(), 16); + let b = udp6_pseudo_header_checksum(&other_src, &dst_ip(), 16); + assert_ne!(a, b); + } + + #[test] + fn udp6_pseudo_header_checksum_different_dst() { + let other_dst: Ipv6Addr = "2001:db8::ff".parse().unwrap(); + let a = udp6_pseudo_header_checksum(&src_ip(), &dst_ip(), 16); + let b = udp6_pseudo_header_checksum(&src_ip(), &other_dst, 16); + assert_ne!(a, b); + } + + #[test] + fn udp6_pseudo_header_checksum_large_length() { + // Test with a length > 65535 (uses the 32-bit upper-layer length field) + let phc = udp6_pseudo_header_checksum(&src_ip(), &dst_ip(), 70000); + assert_ne!(phc, 0); + } + + // --- UDP6 checksum: computed value of 0 becomes 0xFFFF --- + + #[test] + fn udp6_checksum_zero_becomes_ffff() { + // RFC 8200 §8.1: if the computed checksum is zero, it is transmitted as 0xFFFF + // We can't easily construct a payload that produces exactly zero, but we can + // verify the function's contract: the return value is never 0. + // Test with many different payloads to increase confidence. + for i in 0..256u16 { + let payload = [i as u8; 1]; + let cksum = udp6_checksum( + &src_ip(), &dst_ip(), + &[0x00, 0x01, 0x00, 0x02, 0x00, 0x09, 0x00, 0x00], // ports 1,2 len 9 + &payload, + ); + assert_ne!(cksum, 0, "checksum must never be 0 for IPv6"); + } + } + // --- Synthetic performance benchmark --- #[test] diff --git a/dpdk-udp/src/lib.rs b/dpdk-udp/src/lib.rs index 6f05286..a1355db 100644 --- a/dpdk-udp/src/lib.rs +++ b/dpdk-udp/src/lib.rs @@ -3211,77 +3211,121 @@ impl UdpSocket { // Zero-copy UDP parse — payload borrows from frame_data. // Handles both tagged and untagged frames via detect_vlan internally. - let parsed = match parse_udp_packet_ref(frame_data) { - Some(p) => p, - None => { + // Try IPv4 first, then IPv6. + if let Some(parsed) = parse_udp_packet_ref(frame_data) { + // IPv4 path: validate both IPv4 header and UDP checksums. + if !verify_ipv4_checksum(frame_data) { + perf_inc!(self.perf_counters.rx_drops_parse_fail); + return None; + } + if !verify_udp_checksum(frame_data) { perf_inc!(self.perf_counters.rx_drops_parse_fail); return None; } - }; - // Validate RX checksums (IPv4 header + UDP) in software. - // Both verifiers handle VLAN-tagged frames via detect_vlan internally. - if !verify_ipv4_checksum(frame_data) { - perf_inc!(self.perf_counters.rx_drops_parse_fail); - return None; - } - if !verify_udp_checksum(frame_data) { - perf_inc!(self.perf_counters.rx_drops_parse_fail); - return None; - } + perf_inc!(self.perf_counters.rx_packets); + perf_inc!(self.perf_counters.rx_bytes, parsed.payload.len() as u64); - // Count successfully parsed RX packets - perf_inc!(self.perf_counters.rx_packets); - perf_inc!(self.perf_counters.rx_bytes, parsed.payload.len() as u64); + self.arp_handler.cache.insert( + parsed.src_ip, + MacAddress::new(parsed.src_mac), + ); - // Learn source MAC for reply routing - self.arp_handler.cache.insert( - parsed.src_ip, - MacAddress::new(parsed.src_mac), - ); + if parsed.dst_port != local_port { + return None; + } - if parsed.dst_port != local_port { - return None; - } + let src_addr = SocketAddr::V4( + SocketAddrV4::new(parsed.src_ip, parsed.src_port) + ); - let src_addr = SocketAddr::V4( - SocketAddrV4::new(parsed.src_ip, parsed.src_port) - ); + if let Some(connected) = *self.connected_addr.lock().unwrap() { + if src_addr != connected { + let payload_len = parsed.payload.len(); + let mut queue = self.recv_queue.lock().unwrap(); + if queue.push(parsed.payload.to_vec(), src_addr).is_err() { + self.record_rx_drop(payload_len); + } + return None; + } + } + + if result.is_none() { + let copy_len = std::cmp::min(buf.len(), parsed.payload.len()); + buf[..copy_len].copy_from_slice(&parsed.payload[..copy_len]); - // If connected, only accept packets from the connected address - if let Some(connected) = *self.connected_addr.lock().unwrap() { - if src_addr != connected { + if let Ok(mut guard) = self.connection_state.write() { + if let Some(ref mut state) = *guard { + state.record_recv(copy_len); + } + } + + *result = Some((copy_len, src_addr)); + } else { let payload_len = parsed.payload.len(); let mut queue = self.recv_queue.lock().unwrap(); - // Must allocate here — queued packets outlive the frame/mbuf if queue.push(parsed.payload.to_vec(), src_addr).is_err() { self.record_rx_drop(payload_len); } - return None; } + + return None; } - if result.is_none() { - // First matching packet: copy directly to user buffer (zero intermediate alloc) - let copy_len = std::cmp::min(buf.len(), parsed.payload.len()); - buf[..copy_len].copy_from_slice(&parsed.payload[..copy_len]); + // IPv6 path: try parsing as IPv6/UDP. + if let Some(parsed) = ipv6::parse_udp6_packet_ref(frame_data) { + // Mandatory UDP checksum validation for IPv6 (RFC 8200 §8.1). + if !verify_udp6_checksum(frame_data) { + perf_inc!(self.perf_counters.rx_drops_parse_fail); + return None; + } - if let Ok(mut guard) = self.connection_state.write() { - if let Some(ref mut state) = *guard { - state.record_recv(copy_len); + perf_inc!(self.perf_counters.rx_packets); + perf_inc!(self.perf_counters.rx_bytes, parsed.payload.len() as u64); + + if parsed.dst_port != local_port { + return None; + } + + let src_addr = SocketAddr::V6( + std::net::SocketAddrV6::new(parsed.src_ip, parsed.src_port, 0, 0) + ); + + if let Some(connected) = *self.connected_addr.lock().unwrap() { + if src_addr != connected { + let payload_len = parsed.payload.len(); + let mut queue = self.recv_queue.lock().unwrap(); + if queue.push(parsed.payload.to_vec(), src_addr).is_err() { + self.record_rx_drop(payload_len); + } + return None; } } - *result = Some((copy_len, src_addr)); - } else { - // Additional matching packets: must allocate for the queue - let payload_len = parsed.payload.len(); - let mut queue = self.recv_queue.lock().unwrap(); - if queue.push(parsed.payload.to_vec(), src_addr).is_err() { - self.record_rx_drop(payload_len); + if result.is_none() { + let copy_len = std::cmp::min(buf.len(), parsed.payload.len()); + buf[..copy_len].copy_from_slice(&parsed.payload[..copy_len]); + + if let Ok(mut guard) = self.connection_state.write() { + if let Some(ref mut state) = *guard { + state.record_recv(copy_len); + } + } + + *result = Some((copy_len, src_addr)); + } else { + let payload_len = parsed.payload.len(); + let mut queue = self.recv_queue.lock().unwrap(); + if queue.push(parsed.payload.to_vec(), src_addr).is_err() { + self.record_rx_drop(payload_len); + } } + + return None; } + // Neither IPv4 nor IPv6 UDP — drop. + perf_inc!(self.perf_counters.rx_drops_parse_fail); None } @@ -7472,4 +7516,84 @@ mod tests { let bad = dpdk_sys::RTE_MBUF_F_RX_L4_CKSUM_BAD as u64; assert_eq!(good & bad, 0); } + + // ======================================================================== + // IPv6 RX Checksum Validation (process_frame_zerocopy integration) + // ======================================================================== + + #[test] + fn process_frame_zerocopy_accepts_valid_ipv6_udp() { + use crate::ipv6::build_udp6_frame; + use std::net::Ipv6Addr; + + let socket = UdpSocket::bind("127.0.0.1:0").unwrap(); + let local_port = socket_local_port(&socket); + + let src_ip: Ipv6Addr = "2001:db8::1".parse().unwrap(); + let dst_ip: Ipv6Addr = "2001:db8::2".parse().unwrap(); + let frame = build_udp6_frame( + &[0xaa; 6], &[0xbb; 6], src_ip, dst_ip, + 8000, local_port, b"ipv6 payload", 64, + ).unwrap(); + + let mut buf = [0u8; 1500]; + let mut result = None; + socket.process_frame_zerocopy(&frame, local_port, &mut buf, &mut result, None); + + let (len, _src_addr) = result.expect("valid IPv6 UDP frame should be accepted"); + assert_eq!(&buf[..len], b"ipv6 payload"); + } + + #[test] + fn process_frame_zerocopy_rejects_corrupted_ipv6_checksum() { + use crate::ipv6::build_udp6_frame; + use std::net::Ipv6Addr; + + let socket = UdpSocket::bind("127.0.0.1:0").unwrap(); + let local_port = socket_local_port(&socket); + + let src_ip: Ipv6Addr = "2001:db8::1".parse().unwrap(); + let dst_ip: Ipv6Addr = "2001:db8::2".parse().unwrap(); + let mut frame = build_udp6_frame( + &[0xaa; 6], &[0xbb; 6], src_ip, dst_ip, + 8000, local_port, b"corrupt ipv6", 64, + ).unwrap(); + + // Corrupt the payload (invalidates checksum) + let last = frame.len() - 1; + frame[last] ^= 0xFF; + + let mut buf = [0u8; 1500]; + let mut result = None; + socket.process_frame_zerocopy(&frame, local_port, &mut buf, &mut result, None); + + assert!(result.is_none(), "corrupted IPv6 UDP checksum should be rejected"); + } + + #[test] + fn process_frame_zerocopy_rejects_zero_ipv6_checksum() { + use crate::ipv6::{build_udp6_frame, IPV6_HEADER_LEN}; + use std::net::Ipv6Addr; + + let socket = UdpSocket::bind("127.0.0.1:0").unwrap(); + let local_port = socket_local_port(&socket); + + let src_ip: Ipv6Addr = "2001:db8::1".parse().unwrap(); + let dst_ip: Ipv6Addr = "2001:db8::2".parse().unwrap(); + let mut frame = build_udp6_frame( + &[0xaa; 6], &[0xbb; 6], src_ip, dst_ip, + 8000, local_port, b"zero cksum", 64, + ).unwrap(); + + // Zero out the UDP checksum (invalid for IPv6) + let udp_off = ETH_HEADER_LEN + IPV6_HEADER_LEN; + frame[udp_off + 6] = 0; + frame[udp_off + 7] = 0; + + let mut buf = [0u8; 1500]; + let mut result = None; + socket.process_frame_zerocopy(&frame, local_port, &mut buf, &mut result, None); + + assert!(result.is_none(), "zero UDP checksum should be rejected for IPv6"); + } }