From 28febfd694d31a91d6b37b2883410b6963cb260b Mon Sep 17 00:00:00 2001 From: Agent Router Date: Wed, 20 May 2026 23:33:14 -0400 Subject: [PATCH 1/2] feat(encap): add IPv6 outer support for GUE, VXLAN, GENEVE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add IPv6 outer encapsulation to all three tunnel protocols, closing out dual-stack encap as described in the roadmap. For each protocol (GUE, VXLAN, GENEVE): - Config6 struct with Ipv6Addr for remote tunnel endpoint - build_*_frame_into_v6() using outer IPv6 header + mandatory UDP6 checksum (RFC 8200 §8.1) - try_decap_*_v6() for decapsulating IPv6-outer frames - *DecapResult6 with Ipv6Addr for outer source - *_ENCAP_OVERHEAD_V6 constant Wire format: [Outer Eth 14B][Outer IPv6 40B][Outer UDP 8B][Protocol Header][Inner frame] The outer UDP checksum is always computed (mandatory for IPv6), using the existing udp6_checksum() helper from ipv6.rs. 41 new unit tests including synthetic PPS benchmarks for each protocol. Dependencies satisfied: IPv6 header build/parse (PR #49), UDP6 checksum (same PR), IPv6 offload flags (PR #55). --- dpdk-udp/src/geneve.rs | 537 +++++++++++++++++++++++++++++++++++++++++ dpdk-udp/src/gue.rs | 452 ++++++++++++++++++++++++++++++++++ dpdk-udp/src/lib.rs | 22 +- dpdk-udp/src/vxlan.rs | 491 +++++++++++++++++++++++++++++++++++++ 4 files changed, 1496 insertions(+), 6 deletions(-) diff --git a/dpdk-udp/src/geneve.rs b/dpdk-udp/src/geneve.rs index 4e5a175..8376154 100644 --- a/dpdk-udp/src/geneve.rs +++ b/dpdk-udp/src/geneve.rs @@ -19,11 +19,13 @@ //! ``` use std::net::Ipv4Addr; +use std::net::Ipv6Addr; use crate::{ ipv4_checksum, udp_checksum, ETH_HEADER_LEN, ETH_TYPE_IPV4, IPV4_HEADER_LEN, IP_PROTO_UDP, UDP_HEADER_LEN, }; +use crate::ipv6::{ETH_TYPE_IPV6, IPV6_HEADER_LEN, udp6_checksum}; /// GENEVE base header size (8 bytes, no options). pub const GENEVE_BASE_HEADER_LEN: usize = 8; @@ -518,6 +520,299 @@ pub fn try_decap_geneve<'a>( }) } +// ============================================================================ +// IPv6 Outer Support +// ============================================================================ + +/// Encapsulation overhead for GENEVE with IPv6 outer (no options): +/// IPv6(40) + UDP(8) + GENEVE base(8) + inner Eth(14) = 70. +pub const GENEVE_ENCAP_OVERHEAD_V6: usize = + IPV6_HEADER_LEN + UDP_HEADER_LEN + GENEVE_BASE_HEADER_LEN + ETH_HEADER_LEN; + +/// Configuration for a GENEVE tunnel endpoint with IPv6 outer. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GeneveConfig6 { + /// Remote tunnel endpoint IPv6 address (VTEP peer). + pub remote_ip: Ipv6Addr, + /// GENEVE Virtual Network Identifier (24-bit). + pub vni: u32, + /// Outer UDP destination port (default: 6081). + pub remote_port: u16, + /// Outer UDP source port (default: 6081). + pub local_port: u16, + /// Inner source MAC address for encapsulated frames. + pub inner_src_mac: [u8; 6], + /// Inner destination MAC address for encapsulated frames. + pub inner_dst_mac: [u8; 6], +} + +impl GeneveConfig6 { + pub fn new(remote_ip: Ipv6Addr, vni: u32) -> Self { + assert!(vni <= GENEVE_VNI_MAX, "VNI must be 24-bit (max {})", GENEVE_VNI_MAX); + Self { + remote_ip, + vni, + remote_port: GENEVE_DEFAULT_PORT, + local_port: GENEVE_DEFAULT_PORT, + inner_src_mac: [0; 6], + inner_dst_mac: [0xFF; 6], + } + } + + pub fn with_remote_port(mut self, port: u16) -> Self { + self.remote_port = port; + self + } + + pub fn with_local_port(mut self, port: u16) -> Self { + self.local_port = port; + self + } + + pub fn with_inner_src_mac(mut self, mac: [u8; 6]) -> Self { + self.inner_src_mac = mac; + self + } + + pub fn with_inner_dst_mac(mut self, mac: [u8; 6]) -> Self { + self.inner_dst_mac = mac; + self + } +} + +/// Build a GENEVE-encapsulated frame with IPv6 outer into a caller-provided buffer. +/// +/// Produces: +/// `[Outer Eth][Outer IPv6][Outer UDP][GENEVE][Inner Eth][Inner IPv4][Inner UDP][Payload]` +#[allow(clippy::too_many_arguments)] +pub fn build_geneve_frame_into_v6( + out: &mut Vec, + outer_src_mac: &[u8; 6], + outer_dst_mac: &[u8; 6], + outer_src_ip: Ipv6Addr, + outer_dst_ip: Ipv6Addr, + outer_src_port: u16, + outer_dst_port: u16, + geneve_header: &GeneveHeader, + inner_src_mac: &[u8; 6], + inner_dst_mac: &[u8; 6], + inner_src_ip: Ipv4Addr, + inner_dst_ip: Ipv4Addr, + inner_src_port: u16, + inner_dst_port: u16, + payload: &[u8], + hop_limit: u8, +) -> Result { + let geneve_len = geneve_header.header_len(); + + let inner_udp_len = (UDP_HEADER_LEN + payload.len()) as u16; + let inner_ip_total = (IPV4_HEADER_LEN + UDP_HEADER_LEN + payload.len()) as u16; + let inner_frame_len = ETH_HEADER_LEN + inner_ip_total as usize; + + let outer_udp_payload_len = geneve_len + inner_frame_len; + let outer_udp_len = (UDP_HEADER_LEN + outer_udp_payload_len) as u16; + let ipv6_payload_len = outer_udp_len; + + let total_len = ETH_HEADER_LEN + IPV6_HEADER_LEN + outer_udp_len as usize; + out.resize(total_len, 0); + + let inner_src_bytes = inner_src_ip.octets(); + let inner_dst_bytes = inner_dst_ip.octets(); + + // === Outer Ethernet Header (14 bytes) === + out[0..6].copy_from_slice(outer_dst_mac); + out[6..12].copy_from_slice(outer_src_mac); + out[12..14].copy_from_slice(Ð_TYPE_IPV6.to_be_bytes()); + + // === Outer IPv6 Header (40 bytes) === + let oip = ETH_HEADER_LEN; + out[oip] = 0x60; + out[oip + 1] = 0x00; + out[oip + 2] = 0x00; + out[oip + 3] = 0x00; + out[oip + 4..oip + 6].copy_from_slice(&ipv6_payload_len.to_be_bytes()); + out[oip + 6] = IP_PROTO_UDP; + out[oip + 7] = hop_limit; + out[oip + 8..oip + 24].copy_from_slice(&outer_src_ip.octets()); + out[oip + 24..oip + 40].copy_from_slice(&outer_dst_ip.octets()); + + // === Outer UDP Header (8 bytes) === + let oudp = oip + IPV6_HEADER_LEN; + out[oudp..oudp + 2].copy_from_slice(&outer_src_port.to_be_bytes()); + out[oudp + 2..oudp + 4].copy_from_slice(&outer_dst_port.to_be_bytes()); + out[oudp + 4..oudp + 6].copy_from_slice(&outer_udp_len.to_be_bytes()); + out[oudp + 6..oudp + 8].copy_from_slice(&[0x00, 0x00]); + + // === GENEVE Header === + let geneve_off = oudp + UDP_HEADER_LEN; + geneve_header.encode(&mut out[geneve_off..geneve_off + geneve_len]); + + // === Inner Ethernet Header (14 bytes) === + let ieth = geneve_off + geneve_len; + out[ieth..ieth + 6].copy_from_slice(inner_dst_mac); + out[ieth + 6..ieth + 12].copy_from_slice(inner_src_mac); + out[ieth + 12..ieth + 14].copy_from_slice(Ð_TYPE_IPV4.to_be_bytes()); + + // === Inner IPv4 Header (20 bytes) === + let iip = ieth + ETH_HEADER_LEN; + out[iip] = 0x45; + out[iip + 1] = 0x00; + out[iip + 2..iip + 4].copy_from_slice(&inner_ip_total.to_be_bytes()); + out[iip + 4..iip + 6].copy_from_slice(&[0x00, 0x00]); + out[iip + 6..iip + 8].copy_from_slice(&[0x40, 0x00]); + out[iip + 8] = 64; + out[iip + 9] = IP_PROTO_UDP; + out[iip + 10..iip + 12].copy_from_slice(&[0x00, 0x00]); + out[iip + 12..iip + 16].copy_from_slice(&inner_src_bytes); + out[iip + 16..iip + 20].copy_from_slice(&inner_dst_bytes); + let iip_cksum = ipv4_checksum(&out[iip..iip + IPV4_HEADER_LEN]); + out[iip + 10..iip + 12].copy_from_slice(&iip_cksum.to_be_bytes()); + + // === Inner UDP Header (8 bytes) === + let iudp = iip + IPV4_HEADER_LEN; + out[iudp..iudp + 2].copy_from_slice(&inner_src_port.to_be_bytes()); + out[iudp + 2..iudp + 4].copy_from_slice(&inner_dst_port.to_be_bytes()); + out[iudp + 4..iudp + 6].copy_from_slice(&inner_udp_len.to_be_bytes()); + out[iudp + 6..iudp + 8].copy_from_slice(&[0x00, 0x00]); + + // === Payload === + let poff = iudp + UDP_HEADER_LEN; + out[poff..poff + payload.len()].copy_from_slice(payload); + + // Inner UDP checksum (IPv4) + let inner_udp_cksum = udp_checksum( + &inner_src_bytes, + &inner_dst_bytes, + &out[iudp..iudp + UDP_HEADER_LEN], + payload, + ); + out[iudp + 6..iudp + 8].copy_from_slice(&inner_udp_cksum.to_be_bytes()); + + // Outer UDP checksum (IPv6 — mandatory) + let outer_udp_cksum = udp6_checksum( + &outer_src_ip, + &outer_dst_ip, + &out[oudp..oudp + UDP_HEADER_LEN], + &out[oudp + UDP_HEADER_LEN..total_len], + ); + out[oudp + 6..oudp + 8].copy_from_slice(&outer_udp_cksum.to_be_bytes()); + + Ok(total_len) +} + +/// Result of decapsulating a GENEVE frame with IPv6 outer. +#[derive(Debug)] +pub struct GeneveDecapResult6<'a> { + pub inner_src_ip: Ipv4Addr, + pub inner_dst_ip: Ipv4Addr, + pub inner_src_port: u16, + pub inner_dst_port: u16, + pub payload: &'a [u8], + pub outer_src_ip: Ipv6Addr, + pub geneve_header: GeneveHeader, + pub inner_src_mac: [u8; 6], + pub inner_dst_mac: [u8; 6], +} + +/// Try to decapsulate a GENEVE frame with IPv6 outer. +pub fn try_decap_geneve_v6<'a>( + frame: &'a [u8], + l3_offset: usize, + geneve_local_port: u16, + expected_vni: Option, +) -> Option> { + if frame.len() < l3_offset + IPV6_HEADER_LEN { + return None; + } + let oip = &frame[l3_offset..]; + if (oip[0] >> 4) != 6 { + return None; + } + if oip[6] != IP_PROTO_UDP { + return None; + } + let outer_src_ip = Ipv6Addr::from(<[u8; 16]>::try_from(&oip[8..24]).unwrap()); + + // Outer UDP header + let oudp_off = l3_offset + IPV6_HEADER_LEN; + if frame.len() < oudp_off + UDP_HEADER_LEN { + return None; + } + let outer_dst_port = u16::from_be_bytes([frame[oudp_off + 2], frame[oudp_off + 3]]); + if outer_dst_port != geneve_local_port { + return None; + } + + // GENEVE header + let geneve_off = oudp_off + UDP_HEADER_LEN; + let geneve_hdr = GeneveHeader::parse(&frame[geneve_off..])?; + + if let Some(expected) = expected_vni { + if geneve_hdr.vni != expected { + return None; + } + } + + // Inner Ethernet header + let ieth = geneve_off + geneve_hdr.header_len(); + if frame.len() < ieth + ETH_HEADER_LEN { + return None; + } + let inner_dst_mac: [u8; 6] = frame[ieth..ieth + 6].try_into().ok()?; + let inner_src_mac: [u8; 6] = frame[ieth + 6..ieth + 12].try_into().ok()?; + let inner_ethertype = u16::from_be_bytes([frame[ieth + 12], frame[ieth + 13]]); + if inner_ethertype != ETH_TYPE_IPV4 { + return None; + } + + // Inner IPv4 header + let iip_off = ieth + ETH_HEADER_LEN; + if frame.len() < iip_off + IPV4_HEADER_LEN { + return None; + } + let iip = &frame[iip_off..]; + if (iip[0] >> 4) != 4 { + return None; + } + let iip_ihl = (iip[0] & 0x0F) as usize * 4; + if iip_ihl < 20 || frame.len() < iip_off + iip_ihl { + return None; + } + if iip[9] != IP_PROTO_UDP { + return None; + } + let inner_src_ip = Ipv4Addr::new(iip[12], iip[13], iip[14], iip[15]); + let inner_dst_ip = Ipv4Addr::new(iip[16], iip[17], iip[18], iip[19]); + + // Inner UDP header + let iudp_off = iip_off + iip_ihl; + if frame.len() < iudp_off + UDP_HEADER_LEN { + return None; + } + let inner_src_port = u16::from_be_bytes([frame[iudp_off], frame[iudp_off + 1]]); + let inner_dst_port = u16::from_be_bytes([frame[iudp_off + 2], frame[iudp_off + 3]]); + let inner_udp_len = u16::from_be_bytes([frame[iudp_off + 4], frame[iudp_off + 5]]) as usize; + + if inner_udp_len < UDP_HEADER_LEN || frame.len() < iudp_off + inner_udp_len { + return None; + } + + let payload_start = iudp_off + UDP_HEADER_LEN; + let payload_len = inner_udp_len - UDP_HEADER_LEN; + + Some(GeneveDecapResult6 { + inner_src_ip, + inner_dst_ip, + inner_src_port, + inner_dst_port, + payload: &frame[payload_start..payload_start + payload_len], + outer_src_ip, + geneve_header: geneve_hdr, + inner_src_mac, + inner_dst_mac, + }) +} + // ============================================================================ // Tests // ============================================================================ @@ -963,4 +1258,246 @@ mod tests { ); assert!(ns_per_op < 10_000, "build+decap too slow: {} ns/op", ns_per_op); } + + // ========================================================================= + // IPv6 outer tests + // ========================================================================= + + mod ipv6_outer { + use super::*; + use std::net::Ipv6Addr; + use crate::ipv6::{ETH_TYPE_IPV6, IPV6_HEADER_LEN}; + + const OUTER_SRC_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01]; + const OUTER_DST_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x02]; + const INNER_SRC_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x01, 0x01]; + const INNER_DST_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x01, 0x02]; + + fn outer_src() -> Ipv6Addr { "2001:db8::1".parse().unwrap() } + fn outer_dst() -> Ipv6Addr { "2001:db8::2".parse().unwrap() } + fn inner_src() -> Ipv4Addr { Ipv4Addr::new(192, 168, 1, 10) } + fn inner_dst() -> Ipv4Addr { Ipv4Addr::new(192, 168, 1, 20) } + + #[test] + fn config6_defaults() { + let cfg = GeneveConfig6::new(outer_dst(), 100); + assert_eq!(cfg.remote_ip, outer_dst()); + assert_eq!(cfg.vni, 100); + assert_eq!(cfg.remote_port, GENEVE_DEFAULT_PORT); + assert_eq!(cfg.local_port, GENEVE_DEFAULT_PORT); + } + + #[test] + fn config6_builder() { + let cfg = GeneveConfig6::new(outer_dst(), 200) + .with_remote_port(5000) + .with_local_port(5001) + .with_inner_src_mac([0xAA; 6]) + .with_inner_dst_mac([0xBB; 6]); + assert_eq!(cfg.remote_port, 5000); + assert_eq!(cfg.local_port, 5001); + assert_eq!(cfg.inner_src_mac, [0xAA; 6]); + assert_eq!(cfg.inner_dst_mac, [0xBB; 6]); + } + + #[test] + #[should_panic(expected = "VNI must be 24-bit")] + fn config6_rejects_oversized_vni() { + GeneveConfig6::new(outer_dst(), GENEVE_VNI_MAX + 1); + } + + #[test] + fn build_and_decap_roundtrip() { + let payload = b"hello GENEVE IPv6 tunnel"; + let hdr = GeneveHeader::new(100); + let mut frame = Vec::new(); + let len = build_geneve_frame_into_v6( + &mut frame, + &OUTER_SRC_MAC, &OUTER_DST_MAC, + outer_src(), outer_dst(), + 6081, 6081, &hdr, + &INNER_SRC_MAC, &INNER_DST_MAC, + inner_src(), inner_dst(), + 9000, 9001, payload, 64, + ).unwrap(); + + assert_eq!(frame.len(), len); + // 14 (eth) + 40 (IPv6) + 8 (UDP) + 8 (GENEVE) + 14 (inner eth) + 20 (inner IPv4) + 8 (inner UDP) + payload + let expected = 14 + 40 + 8 + 8 + 14 + 20 + 8 + payload.len(); + assert_eq!(len, expected); + + let decap = try_decap_geneve_v6(&frame, ETH_HEADER_LEN, 6081, Some(100)).unwrap(); + assert_eq!(decap.inner_src_ip, inner_src()); + assert_eq!(decap.inner_dst_ip, inner_dst()); + assert_eq!(decap.inner_src_port, 9000); + assert_eq!(decap.inner_dst_port, 9001); + assert_eq!(decap.payload, payload); + assert_eq!(decap.outer_src_ip, outer_src()); + assert_eq!(decap.geneve_header.vni, 100); + assert_eq!(decap.inner_src_mac, INNER_SRC_MAC); + assert_eq!(decap.inner_dst_mac, INNER_DST_MAC); + } + + #[test] + fn build_and_decap_with_options() { + let options = vec![ + GeneveTlvOption { class: 0x0102, option_type: 1, data: vec![0xAA, 0xBB, 0xCC, 0xDD] }, + ]; + let opt_len = 4 + options[0].data.len(); // 4-byte TLV header + data + let hdr = GeneveHeader { + version: GENEVE_VERSION, + options_len: opt_len, + oam: false, + critical: false, + protocol_type: GENEVE_INNER_ETYPE_ETH, + vni: 100, + options: options.clone(), + }; + let payload = b"with opts"; + let mut frame = Vec::new(); + build_geneve_frame_into_v6( + &mut frame, + &OUTER_SRC_MAC, &OUTER_DST_MAC, + outer_src(), outer_dst(), + 6081, 6081, &hdr, + &INNER_SRC_MAC, &INNER_DST_MAC, + inner_src(), inner_dst(), + 9000, 9001, payload, 64, + ).unwrap(); + + let decap = try_decap_geneve_v6(&frame, ETH_HEADER_LEN, 6081, Some(100)).unwrap(); + assert_eq!(decap.payload, payload); + assert_eq!(decap.geneve_header.options.len(), 1); + assert_eq!(decap.geneve_header.options[0].class, 0x0102); + assert_eq!(decap.geneve_header.options[0].data, vec![0xAA, 0xBB, 0xCC, 0xDD]); + } + + #[test] + fn wire_format_ethertype_is_ipv6() { + let hdr = GeneveHeader::new(100); + let mut frame = Vec::new(); + build_geneve_frame_into_v6( + &mut frame, &OUTER_SRC_MAC, &OUTER_DST_MAC, + outer_src(), outer_dst(), 6081, 6081, &hdr, + &INNER_SRC_MAC, &INNER_DST_MAC, + inner_src(), inner_dst(), 1, 2, b"x", 64, + ).unwrap(); + let ethertype = u16::from_be_bytes([frame[12], frame[13]]); + assert_eq!(ethertype, ETH_TYPE_IPV6); + } + + #[test] + fn wire_format_ipv6_version() { + let hdr = GeneveHeader::new(100); + let mut frame = Vec::new(); + build_geneve_frame_into_v6( + &mut frame, &OUTER_SRC_MAC, &OUTER_DST_MAC, + outer_src(), outer_dst(), 6081, 6081, &hdr, + &INNER_SRC_MAC, &INNER_DST_MAC, + inner_src(), inner_dst(), 1, 2, b"x", 64, + ).unwrap(); + assert_eq!(frame[ETH_HEADER_LEN] >> 4, 6); + } + + #[test] + fn wire_format_outer_udp_checksum_valid() { + let hdr = GeneveHeader::new(100); + let mut frame = Vec::new(); + build_geneve_frame_into_v6( + &mut frame, &OUTER_SRC_MAC, &OUTER_DST_MAC, + outer_src(), outer_dst(), 6081, 6081, &hdr, + &INNER_SRC_MAC, &INNER_DST_MAC, + inner_src(), inner_dst(), 9000, 9001, b"checksum test", 64, + ).unwrap(); + assert!(crate::verify_udp6_checksum(&frame)); + } + + #[test] + fn decap_rejects_wrong_port() { + let hdr = GeneveHeader::new(100); + let mut frame = Vec::new(); + build_geneve_frame_into_v6( + &mut frame, &OUTER_SRC_MAC, &OUTER_DST_MAC, + outer_src(), outer_dst(), 6081, 6081, &hdr, + &INNER_SRC_MAC, &INNER_DST_MAC, + inner_src(), inner_dst(), 9000, 9001, b"x", 64, + ).unwrap(); + assert!(try_decap_geneve_v6(&frame, ETH_HEADER_LEN, 7000, Some(100)).is_none()); + } + + #[test] + fn decap_rejects_wrong_vni() { + let hdr = GeneveHeader::new(100); + let mut frame = Vec::new(); + build_geneve_frame_into_v6( + &mut frame, &OUTER_SRC_MAC, &OUTER_DST_MAC, + outer_src(), outer_dst(), 6081, 6081, &hdr, + &INNER_SRC_MAC, &INNER_DST_MAC, + inner_src(), inner_dst(), 9000, 9001, b"x", 64, + ).unwrap(); + assert!(try_decap_geneve_v6(&frame, ETH_HEADER_LEN, 6081, Some(200)).is_none()); + } + + #[test] + fn decap_accepts_any_vni_when_none() { + let hdr = GeneveHeader::new(999); + let mut frame = Vec::new(); + build_geneve_frame_into_v6( + &mut frame, &OUTER_SRC_MAC, &OUTER_DST_MAC, + outer_src(), outer_dst(), 6081, 6081, &hdr, + &INNER_SRC_MAC, &INNER_DST_MAC, + inner_src(), inner_dst(), 9000, 9001, b"x", 64, + ).unwrap(); + let decap = try_decap_geneve_v6(&frame, ETH_HEADER_LEN, 6081, None).unwrap(); + assert_eq!(decap.geneve_header.vni, 999); + } + + #[test] + fn build_empty_payload() { + let hdr = GeneveHeader::new(100); + let mut frame = Vec::new(); + let len = build_geneve_frame_into_v6( + &mut frame, &OUTER_SRC_MAC, &OUTER_DST_MAC, + outer_src(), outer_dst(), 6081, 6081, &hdr, + &INNER_SRC_MAC, &INNER_DST_MAC, + inner_src(), inner_dst(), 9000, 9001, &[], 64, + ).unwrap(); + // 14 + 40 + 8 + 8 + 14 + 20 + 8 = 112 + assert_eq!(len, 112); + let decap = try_decap_geneve_v6(&frame, ETH_HEADER_LEN, 6081, Some(100)).unwrap(); + assert!(decap.payload.is_empty()); + } + + #[test] + fn encap_overhead_v6_is_correct() { + // outer IPv6(40) + outer UDP(8) + GENEVE base(8) + inner Eth(14) = 70 + assert_eq!(GENEVE_ENCAP_OVERHEAD_V6, 70); + } + + #[test] + fn perf_build_decap_cycle_v6() { + let payload = vec![0xAA; 64]; + let hdr = GeneveHeader::new(100); + let mut buf = Vec::with_capacity(1500); + let iterations = 10_000; + + let start = std::time::Instant::now(); + for _ in 0..iterations { + build_geneve_frame_into_v6( + &mut buf, &OUTER_SRC_MAC, &OUTER_DST_MAC, + outer_src(), outer_dst(), 6081, 6081, &hdr, + &INNER_SRC_MAC, &INNER_DST_MAC, + inner_src(), inner_dst(), 12345, 9000, &payload, 64, + ).unwrap(); + let _ = try_decap_geneve_v6(&buf, ETH_HEADER_LEN, 6081, Some(100)).unwrap(); + } + let elapsed = start.elapsed(); + let ns_per_op = elapsed.as_nanos() / iterations as u128; + eprintln!( + "[PERF] GENEVE IPv6-outer build+decap: {} iterations in {:?} ({} ns/op)", + iterations, elapsed, ns_per_op + ); + assert!(ns_per_op < 10_000, "build+decap too slow: {} ns/op", ns_per_op); + } + } } diff --git a/dpdk-udp/src/gue.rs b/dpdk-udp/src/gue.rs index 9116403..04c498a 100644 --- a/dpdk-udp/src/gue.rs +++ b/dpdk-udp/src/gue.rs @@ -17,11 +17,13 @@ //! ``` use std::net::Ipv4Addr; +use std::net::Ipv6Addr; use crate::{ ipv4_checksum, udp_checksum, ETH_HEADER_LEN, ETH_TYPE_IPV4, IPV4_HEADER_LEN, IP_PROTO_UDP, UDP_HEADER_LEN, }; +use crate::ipv6::{ETH_TYPE_IPV6, IPV6_HEADER_LEN, udp6_checksum}; pub const GUE_HEADER_LEN: usize = 4; @@ -355,6 +357,254 @@ pub fn try_decap_gue<'a>( }) } +// ============================================================================ +// IPv6 Outer Support +// ============================================================================ + +/// Encapsulation overhead for GUE with IPv6 outer: IPv6(40) + UDP(8) + GUE(4) = 52. +pub const GUE_ENCAP_OVERHEAD_V6: usize = IPV6_HEADER_LEN + UDP_HEADER_LEN + GUE_HEADER_LEN; + +/// Configuration for a GUE tunnel endpoint with IPv6 outer. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GueConfig6 { + /// Remote tunnel endpoint IPv6 address. + pub remote_ip: Ipv6Addr, + /// Outer UDP destination port (default: 6080). + pub remote_port: u16, + /// Outer UDP source port (default: 6080). + pub local_port: u16, +} + +impl GueConfig6 { + pub fn new(remote_ip: Ipv6Addr) -> Self { + Self { + remote_ip, + remote_port: GUE_DEFAULT_PORT, + local_port: GUE_DEFAULT_PORT, + } + } + + pub fn with_remote_port(mut self, port: u16) -> Self { + self.remote_port = port; + self + } + + pub fn with_local_port(mut self, port: u16) -> Self { + self.local_port = port; + self + } +} + +/// Build a GUE-encapsulated frame with IPv6 outer into a caller-provided buffer. +/// +/// Produces: `[Outer Eth][Outer IPv6][Outer UDP][GUE][Inner IPv4][Inner UDP][Payload]` +#[allow(clippy::too_many_arguments)] +pub fn build_gue_frame_into_v6( + out: &mut Vec, + outer_src_mac: &[u8; 6], + outer_dst_mac: &[u8; 6], + outer_src_ip: Ipv6Addr, + outer_dst_ip: Ipv6Addr, + outer_src_port: u16, + outer_dst_port: u16, + inner_src_ip: Ipv4Addr, + inner_dst_ip: Ipv4Addr, + inner_src_port: u16, + inner_dst_port: u16, + payload: &[u8], + hop_limit: u8, +) -> Result { + let inner_udp_len = (UDP_HEADER_LEN + payload.len()) as u16; + let inner_ip_total = (IPV4_HEADER_LEN + UDP_HEADER_LEN + payload.len()) as u16; + let inner_pkt_len = IPV4_HEADER_LEN + UDP_HEADER_LEN + payload.len(); + + let outer_udp_payload = GUE_HEADER_LEN + inner_pkt_len; + let outer_udp_len = (UDP_HEADER_LEN + outer_udp_payload) as u16; + let ipv6_payload_len = outer_udp_len; + + let total_len = ETH_HEADER_LEN + IPV6_HEADER_LEN + outer_udp_len as usize; + out.resize(total_len, 0); + + let inner_src_bytes = inner_src_ip.octets(); + let inner_dst_bytes = inner_dst_ip.octets(); + + // === Outer Ethernet Header (14 bytes) === + out[0..6].copy_from_slice(outer_dst_mac); + out[6..12].copy_from_slice(outer_src_mac); + out[12..14].copy_from_slice(Ð_TYPE_IPV6.to_be_bytes()); + + // === Outer IPv6 Header (40 bytes) === + let oip = ETH_HEADER_LEN; + out[oip] = 0x60; // version 6, traffic class 0 + out[oip + 1] = 0x00; + out[oip + 2] = 0x00; + out[oip + 3] = 0x00; // flow label 0 + out[oip + 4..oip + 6].copy_from_slice(&ipv6_payload_len.to_be_bytes()); + out[oip + 6] = IP_PROTO_UDP; // next header + out[oip + 7] = hop_limit; + out[oip + 8..oip + 24].copy_from_slice(&outer_src_ip.octets()); + out[oip + 24..oip + 40].copy_from_slice(&outer_dst_ip.octets()); + + // === Outer UDP Header (8 bytes) === + let oudp = oip + IPV6_HEADER_LEN; + out[oudp..oudp + 2].copy_from_slice(&outer_src_port.to_be_bytes()); + out[oudp + 2..oudp + 4].copy_from_slice(&outer_dst_port.to_be_bytes()); + out[oudp + 4..oudp + 6].copy_from_slice(&outer_udp_len.to_be_bytes()); + out[oudp + 6..oudp + 8].copy_from_slice(&[0x00, 0x00]); // checksum placeholder + + // === GUE Header (4 bytes) === + let gue_off = oudp + UDP_HEADER_LEN; + let gue_hdr = GueHeader::new_data(GUE_PROTO_IPV4); + gue_hdr.encode(&mut out[gue_off..gue_off + GUE_HEADER_LEN]); + + // === Inner IPv4 Header (20 bytes) === + let iip = gue_off + GUE_HEADER_LEN; + out[iip] = 0x45; + out[iip + 1] = 0x00; + out[iip + 2..iip + 4].copy_from_slice(&inner_ip_total.to_be_bytes()); + out[iip + 4..iip + 6].copy_from_slice(&[0x00, 0x00]); + out[iip + 6..iip + 8].copy_from_slice(&[0x40, 0x00]); // DF + out[iip + 8] = 64; // inner TTL + out[iip + 9] = IP_PROTO_UDP; + out[iip + 10..iip + 12].copy_from_slice(&[0x00, 0x00]); + out[iip + 12..iip + 16].copy_from_slice(&inner_src_bytes); + out[iip + 16..iip + 20].copy_from_slice(&inner_dst_bytes); + let iip_cksum = ipv4_checksum(&out[iip..iip + IPV4_HEADER_LEN]); + out[iip + 10..iip + 12].copy_from_slice(&iip_cksum.to_be_bytes()); + + // === Inner UDP Header (8 bytes) === + let iudp = iip + IPV4_HEADER_LEN; + out[iudp..iudp + 2].copy_from_slice(&inner_src_port.to_be_bytes()); + out[iudp + 2..iudp + 4].copy_from_slice(&inner_dst_port.to_be_bytes()); + out[iudp + 4..iudp + 6].copy_from_slice(&inner_udp_len.to_be_bytes()); + out[iudp + 6..iudp + 8].copy_from_slice(&[0x00, 0x00]); + + // === Payload === + let poff = iudp + UDP_HEADER_LEN; + out[poff..poff + payload.len()].copy_from_slice(payload); + + // Inner UDP checksum (IPv4) + let inner_udp_cksum = udp_checksum( + &inner_src_bytes, + &inner_dst_bytes, + &out[iudp..iudp + UDP_HEADER_LEN], + payload, + ); + out[iudp + 6..iudp + 8].copy_from_slice(&inner_udp_cksum.to_be_bytes()); + + // Outer UDP checksum (IPv6 — mandatory) + let outer_udp_cksum = udp6_checksum( + &outer_src_ip, + &outer_dst_ip, + &out[oudp..oudp + UDP_HEADER_LEN], + &out[oudp + UDP_HEADER_LEN..total_len], + ); + out[oudp + 6..oudp + 8].copy_from_slice(&outer_udp_cksum.to_be_bytes()); + + Ok(total_len) +} + +/// Result of decapsulating a GUE frame with IPv6 outer. +#[derive(Debug)] +pub struct GueDecapResult6<'a> { + pub inner_src_ip: Ipv4Addr, + pub inner_dst_ip: Ipv4Addr, + pub inner_src_port: u16, + pub inner_dst_port: u16, + pub payload: &'a [u8], + pub outer_src_ip: Ipv6Addr, + pub gue_header: GueHeader, +} + +/// Try to decapsulate a GUE frame with IPv6 outer. +/// +/// `frame` is the full Ethernet frame. `l3_offset` is where the outer IPv6 +/// header starts. The caller has already verified the outer ethertype is IPv6. +pub fn try_decap_gue_v6<'a>( + frame: &'a [u8], + l3_offset: usize, + gue_local_port: u16, +) -> Option> { + if frame.len() < l3_offset + IPV6_HEADER_LEN { + return None; + } + let oip = &frame[l3_offset..]; + // Verify IPv6 version + if (oip[0] >> 4) != 6 { + return None; + } + // Next header must be UDP + if oip[6] != IP_PROTO_UDP { + return None; + } + let outer_src_ip = Ipv6Addr::from(<[u8; 16]>::try_from(&oip[8..24]).unwrap()); + + // Outer UDP header + let oudp_off = l3_offset + IPV6_HEADER_LEN; + if frame.len() < oudp_off + UDP_HEADER_LEN { + return None; + } + let outer_dst_port = u16::from_be_bytes([frame[oudp_off + 2], frame[oudp_off + 3]]); + if outer_dst_port != gue_local_port { + return None; + } + + // GUE header + let gue_off = oudp_off + UDP_HEADER_LEN; + let gue_hdr = GueHeader::parse(&frame[gue_off..])?; + if gue_hdr.c_flag { + return None; + } + if gue_hdr.proto != GUE_PROTO_IPV4 { + return None; + } + + // Inner IPv4 header + let inner_off = gue_off + gue_hdr.total_len(); + if frame.len() < inner_off + IPV4_HEADER_LEN { + return None; + } + let iip = &frame[inner_off..]; + if (iip[0] >> 4) != 4 { + return None; + } + let iip_ihl = (iip[0] & 0x0F) as usize * 4; + if iip_ihl < 20 || frame.len() < inner_off + iip_ihl { + return None; + } + if iip[9] != IP_PROTO_UDP { + return None; + } + let inner_src_ip = Ipv4Addr::new(iip[12], iip[13], iip[14], iip[15]); + let inner_dst_ip = Ipv4Addr::new(iip[16], iip[17], iip[18], iip[19]); + + // Inner UDP header + let iudp_off = inner_off + iip_ihl; + if frame.len() < iudp_off + UDP_HEADER_LEN { + return None; + } + let inner_src_port = u16::from_be_bytes([frame[iudp_off], frame[iudp_off + 1]]); + let inner_dst_port = u16::from_be_bytes([frame[iudp_off + 2], frame[iudp_off + 3]]); + let inner_udp_len = u16::from_be_bytes([frame[iudp_off + 4], frame[iudp_off + 5]]) as usize; + + if inner_udp_len < UDP_HEADER_LEN || frame.len() < iudp_off + inner_udp_len { + return None; + } + + let payload_start = iudp_off + UDP_HEADER_LEN; + let payload_len = inner_udp_len - UDP_HEADER_LEN; + + Some(GueDecapResult6 { + inner_src_ip, + inner_dst_ip, + inner_src_port, + inner_dst_port, + payload: &frame[payload_start..payload_start + payload_len], + outer_src_ip, + gue_header: gue_hdr, + }) +} + #[cfg(test)] mod tests { use super::*; @@ -587,4 +837,206 @@ mod tests { assert_eq!(decap.payload.len(), 1400); assert!(decap.payload.iter().all(|&b| b == 0xAB)); } + + // ========================================================================= + // IPv6 outer tests + // ========================================================================= + + mod ipv6_outer { + use super::*; + use std::net::Ipv6Addr; + use crate::ipv6::{ETH_TYPE_IPV6, IPV6_HEADER_LEN}; + + const SRC_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01]; + const DST_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x02]; + + fn outer_src() -> Ipv6Addr { "2001:db8::1".parse().unwrap() } + fn outer_dst() -> Ipv6Addr { "2001:db8::2".parse().unwrap() } + fn inner_src() -> Ipv4Addr { Ipv4Addr::new(192, 168, 1, 10) } + fn inner_dst() -> Ipv4Addr { Ipv4Addr::new(192, 168, 1, 20) } + + #[test] + fn config6_defaults() { + let cfg = GueConfig6::new(outer_dst()); + assert_eq!(cfg.remote_ip, outer_dst()); + assert_eq!(cfg.remote_port, GUE_DEFAULT_PORT); + assert_eq!(cfg.local_port, GUE_DEFAULT_PORT); + } + + #[test] + fn config6_builder() { + let cfg = GueConfig6::new(outer_dst()) + .with_remote_port(7000) + .with_local_port(7001); + assert_eq!(cfg.remote_port, 7000); + assert_eq!(cfg.local_port, 7001); + } + + #[test] + fn build_and_decap_roundtrip() { + let payload = b"hello GUE IPv6 tunnel"; + let mut frame = Vec::new(); + let len = build_gue_frame_into_v6( + &mut frame, + &SRC_MAC, &DST_MAC, + outer_src(), outer_dst(), + 6080, 6080, + inner_src(), inner_dst(), + 9000, 9001, + payload, 64, + ).unwrap(); + + assert_eq!(frame.len(), len); + // 14 (eth) + 40 (outer IPv6) + 8 (outer UDP) + 4 (GUE) + 20 (inner IPv4) + 8 (inner UDP) + payload + let expected = 14 + 40 + 8 + 4 + 20 + 8 + payload.len(); + assert_eq!(len, expected); + + let decap = try_decap_gue_v6(&frame, ETH_HEADER_LEN, 6080).unwrap(); + assert_eq!(decap.inner_src_ip, inner_src()); + assert_eq!(decap.inner_dst_ip, inner_dst()); + assert_eq!(decap.inner_src_port, 9000); + assert_eq!(decap.inner_dst_port, 9001); + assert_eq!(decap.payload, payload); + assert_eq!(decap.outer_src_ip, outer_src()); + } + + #[test] + fn wire_format_ethertype_is_ipv6() { + let mut frame = Vec::new(); + build_gue_frame_into_v6( + &mut frame, &SRC_MAC, &DST_MAC, + outer_src(), outer_dst(), 6080, 6080, + inner_src(), inner_dst(), 1, 2, b"x", 64, + ).unwrap(); + let ethertype = u16::from_be_bytes([frame[12], frame[13]]); + assert_eq!(ethertype, ETH_TYPE_IPV6); + } + + #[test] + fn wire_format_ipv6_version() { + let mut frame = Vec::new(); + build_gue_frame_into_v6( + &mut frame, &SRC_MAC, &DST_MAC, + outer_src(), outer_dst(), 6080, 6080, + inner_src(), inner_dst(), 1, 2, b"x", 64, + ).unwrap(); + assert_eq!(frame[ETH_HEADER_LEN] >> 4, 6); + } + + #[test] + fn wire_format_hop_limit() { + let mut frame = Vec::new(); + build_gue_frame_into_v6( + &mut frame, &SRC_MAC, &DST_MAC, + outer_src(), outer_dst(), 6080, 6080, + inner_src(), inner_dst(), 1, 2, b"x", 42, + ).unwrap(); + assert_eq!(frame[ETH_HEADER_LEN + 7], 42); + } + + #[test] + fn wire_format_next_header_is_udp() { + let mut frame = Vec::new(); + build_gue_frame_into_v6( + &mut frame, &SRC_MAC, &DST_MAC, + outer_src(), outer_dst(), 6080, 6080, + inner_src(), inner_dst(), 1, 2, b"x", 64, + ).unwrap(); + assert_eq!(frame[ETH_HEADER_LEN + 6], crate::IP_PROTO_UDP); + } + + #[test] + fn wire_format_outer_udp_checksum_valid() { + let mut frame = Vec::new(); + build_gue_frame_into_v6( + &mut frame, &SRC_MAC, &DST_MAC, + outer_src(), outer_dst(), 6080, 6080, + inner_src(), inner_dst(), 9000, 9001, + b"checksum test", 64, + ).unwrap(); + assert!(crate::verify_udp6_checksum(&frame)); + } + + #[test] + fn decap_rejects_wrong_port() { + let mut frame = Vec::new(); + build_gue_frame_into_v6( + &mut frame, &SRC_MAC, &DST_MAC, + outer_src(), outer_dst(), 6080, 6080, + inner_src(), inner_dst(), 9000, 9001, b"x", 64, + ).unwrap(); + assert!(try_decap_gue_v6(&frame, ETH_HEADER_LEN, 7000).is_none()); + } + + #[test] + fn decap_rejects_control_message() { + let mut frame = Vec::new(); + build_gue_frame_into_v6( + &mut frame, &SRC_MAC, &DST_MAC, + outer_src(), outer_dst(), 6080, 6080, + inner_src(), inner_dst(), 9000, 9001, b"x", 64, + ).unwrap(); + // GUE header is at: ETH(14) + IPv6(40) + UDP(8) = offset 62 + frame[62] = 0x20; // Set C flag + assert!(try_decap_gue_v6(&frame, ETH_HEADER_LEN, 6080).is_none()); + } + + #[test] + fn build_empty_payload() { + let mut frame = Vec::new(); + let len = build_gue_frame_into_v6( + &mut frame, &SRC_MAC, &DST_MAC, + outer_src(), outer_dst(), 6080, 6080, + inner_src(), inner_dst(), 9000, 9001, &[], 64, + ).unwrap(); + // 14 + 40 + 8 + 4 + 20 + 8 = 94 + assert_eq!(len, 94); + let decap = try_decap_gue_v6(&frame, ETH_HEADER_LEN, 6080).unwrap(); + assert!(decap.payload.is_empty()); + } + + #[test] + fn build_large_payload() { + let payload = vec![0xAB; 1400]; + let mut frame = Vec::new(); + build_gue_frame_into_v6( + &mut frame, &SRC_MAC, &DST_MAC, + outer_src(), outer_dst(), 6080, 6080, + inner_src(), inner_dst(), 9000, 9001, &payload, 64, + ).unwrap(); + let decap = try_decap_gue_v6(&frame, ETH_HEADER_LEN, 6080).unwrap(); + assert_eq!(decap.payload.len(), 1400); + assert!(decap.payload.iter().all(|&b| b == 0xAB)); + } + + #[test] + fn encap_overhead_v6_is_correct() { + // outer IPv6(40) + outer UDP(8) + GUE(4) = 52 + assert_eq!(GUE_ENCAP_OVERHEAD_V6, 52); + } + + #[test] + fn perf_build_decap_cycle_v6() { + let payload = vec![0xAA; 64]; + let mut buf = Vec::with_capacity(1500); + let iterations = 10_000; + + let start = std::time::Instant::now(); + for _ in 0..iterations { + build_gue_frame_into_v6( + &mut buf, &SRC_MAC, &DST_MAC, + outer_src(), outer_dst(), 6080, 6080, + inner_src(), inner_dst(), 9000, 9001, &payload, 64, + ).unwrap(); + let _ = try_decap_gue_v6(&buf, ETH_HEADER_LEN, 6080).unwrap(); + } + let elapsed = start.elapsed(); + let ns_per_op = elapsed.as_nanos() / iterations as u128; + eprintln!( + "[PERF] GUE IPv6-outer build+decap: {} iterations in {:?} ({} ns/op)", + iterations, elapsed, ns_per_op + ); + assert!(ns_per_op < 10_000, "build+decap too slow: {} ns/op", ns_per_op); + } + } } diff --git a/dpdk-udp/src/lib.rs b/dpdk-udp/src/lib.rs index befb809..6f05286 100644 --- a/dpdk-udp/src/lib.rs +++ b/dpdk-udp/src/lib.rs @@ -74,15 +74,25 @@ pub use perf::{ LatencySampler, NicStatsFn, NicStatsSnapshot, PerfCounters, PerfReporter, PerfSnapshot, }; pub use routing::{RoutingTable, NetworkConfig, RouteEntry, NextHop, ProcArpEntry}; -pub use gue::{GueConfig, GueHeader, GUE_DEFAULT_PORT, GUE_ENCAP_OVERHEAD}; +pub use gue::{ + GueConfig, GueConfig6, GueHeader, GueDecapResult6, + GUE_DEFAULT_PORT, GUE_ENCAP_OVERHEAD, GUE_ENCAP_OVERHEAD_V6, + build_gue_frame_into, build_gue_frame_into_v6, try_decap_gue, try_decap_gue_v6, + GueDecapResult, +}; pub use geneve::{ - GeneveConfig, GeneveHeader, GeneveDecapResult, GeneveTlvOption, - GENEVE_DEFAULT_PORT, GENEVE_ENCAP_OVERHEAD, GENEVE_BASE_HEADER_LEN, - GENEVE_VNI_MAX, build_geneve_frame_into, try_decap_geneve, + GeneveConfig, GeneveConfig6, GeneveHeader, GeneveDecapResult, GeneveDecapResult6, + GeneveTlvOption, GENEVE_DEFAULT_PORT, GENEVE_ENCAP_OVERHEAD, GENEVE_ENCAP_OVERHEAD_V6, + GENEVE_BASE_HEADER_LEN, GENEVE_VNI_MAX, + build_geneve_frame_into, build_geneve_frame_into_v6, + try_decap_geneve, try_decap_geneve_v6, }; pub use vxlan::{ - VxlanConfig, VxlanHeader, VxlanDecapResult, VXLAN_DEFAULT_PORT, VXLAN_ENCAP_OVERHEAD, - VXLAN_HEADER_LEN, VXLAN_VNI_MAX, build_vxlan_frame_into, try_decap_vxlan, + VxlanConfig, VxlanConfig6, VxlanHeader, VxlanDecapResult, VxlanDecapResult6, + VXLAN_DEFAULT_PORT, VXLAN_ENCAP_OVERHEAD, VXLAN_ENCAP_OVERHEAD_V6, + VXLAN_HEADER_LEN, VXLAN_VNI_MAX, + build_vxlan_frame_into, build_vxlan_frame_into_v6, + try_decap_vxlan, try_decap_vxlan_v6, }; pub use ipv6::{ build_udp6_frame, build_udp6_frame_into, parse_udp6_packet, parse_udp6_packet_ref, diff --git a/dpdk-udp/src/vxlan.rs b/dpdk-udp/src/vxlan.rs index 8313b58..3b22036 100644 --- a/dpdk-udp/src/vxlan.rs +++ b/dpdk-udp/src/vxlan.rs @@ -19,11 +19,13 @@ //! ``` use std::net::Ipv4Addr; +use std::net::Ipv6Addr; use crate::{ ipv4_checksum, udp_checksum, ETH_HEADER_LEN, ETH_TYPE_IPV4, IPV4_HEADER_LEN, IP_PROTO_UDP, UDP_HEADER_LEN, }; +use crate::ipv6::{ETH_TYPE_IPV6, IPV6_HEADER_LEN, udp6_checksum}; /// VXLAN header size (always 8 bytes). pub const VXLAN_HEADER_LEN: usize = 8; @@ -413,6 +415,296 @@ pub fn try_decap_vxlan<'a>( }) } +// ============================================================================ +// IPv6 Outer Support +// ============================================================================ + +/// Encapsulation overhead for VXLAN with IPv6 outer: IPv6(40) + UDP(8) + VXLAN(8) + inner Eth(14) = 70. +pub const VXLAN_ENCAP_OVERHEAD_V6: usize = + IPV6_HEADER_LEN + UDP_HEADER_LEN + VXLAN_HEADER_LEN + ETH_HEADER_LEN; + +/// Configuration for a VXLAN tunnel endpoint with IPv6 outer. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VxlanConfig6 { + /// Remote tunnel endpoint IPv6 address (VTEP peer). + pub remote_ip: Ipv6Addr, + /// VXLAN Network Identifier (24-bit). + pub vni: u32, + /// Outer UDP destination port (default: 4789). + pub remote_port: u16, + /// Outer UDP source port (default: 4789). + pub local_port: u16, + /// Inner source MAC address for encapsulated frames. + pub inner_src_mac: [u8; 6], + /// Inner destination MAC address for encapsulated frames. + pub inner_dst_mac: [u8; 6], +} + +impl VxlanConfig6 { + pub fn new(remote_ip: Ipv6Addr, vni: u32) -> Self { + assert!(vni <= VXLAN_VNI_MAX, "VNI must be 24-bit (max {})", VXLAN_VNI_MAX); + Self { + remote_ip, + vni, + remote_port: VXLAN_DEFAULT_PORT, + local_port: VXLAN_DEFAULT_PORT, + inner_src_mac: [0; 6], + inner_dst_mac: [0xFF; 6], + } + } + + pub fn with_remote_port(mut self, port: u16) -> Self { + self.remote_port = port; + self + } + + pub fn with_local_port(mut self, port: u16) -> Self { + self.local_port = port; + self + } + + pub fn with_inner_src_mac(mut self, mac: [u8; 6]) -> Self { + self.inner_src_mac = mac; + self + } + + pub fn with_inner_dst_mac(mut self, mac: [u8; 6]) -> Self { + self.inner_dst_mac = mac; + self + } +} + +/// Build a VXLAN-encapsulated frame with IPv6 outer into a caller-provided buffer. +/// +/// Produces: +/// `[Outer Eth][Outer IPv6][Outer UDP][VXLAN][Inner Eth][Inner IPv4][Inner UDP][Payload]` +#[allow(clippy::too_many_arguments)] +pub fn build_vxlan_frame_into_v6( + out: &mut Vec, + outer_src_mac: &[u8; 6], + outer_dst_mac: &[u8; 6], + outer_src_ip: Ipv6Addr, + outer_dst_ip: Ipv6Addr, + outer_src_port: u16, + outer_dst_port: u16, + vni: u32, + inner_src_mac: &[u8; 6], + inner_dst_mac: &[u8; 6], + inner_src_ip: Ipv4Addr, + inner_dst_ip: Ipv4Addr, + inner_src_port: u16, + inner_dst_port: u16, + payload: &[u8], + hop_limit: u8, +) -> Result { + let inner_udp_len = (UDP_HEADER_LEN + payload.len()) as u16; + let inner_ip_total = (IPV4_HEADER_LEN + UDP_HEADER_LEN + payload.len()) as u16; + let inner_frame_len = ETH_HEADER_LEN + inner_ip_total as usize; + + let outer_udp_payload_len = VXLAN_HEADER_LEN + inner_frame_len; + let outer_udp_len = (UDP_HEADER_LEN + outer_udp_payload_len) as u16; + let ipv6_payload_len = outer_udp_len; + + let total_len = ETH_HEADER_LEN + IPV6_HEADER_LEN + outer_udp_len as usize; + out.resize(total_len, 0); + + let inner_src_bytes = inner_src_ip.octets(); + let inner_dst_bytes = inner_dst_ip.octets(); + + // === Outer Ethernet Header (14 bytes) === + out[0..6].copy_from_slice(outer_dst_mac); + out[6..12].copy_from_slice(outer_src_mac); + out[12..14].copy_from_slice(Ð_TYPE_IPV6.to_be_bytes()); + + // === Outer IPv6 Header (40 bytes) === + let oip = ETH_HEADER_LEN; + out[oip] = 0x60; + out[oip + 1] = 0x00; + out[oip + 2] = 0x00; + out[oip + 3] = 0x00; + out[oip + 4..oip + 6].copy_from_slice(&ipv6_payload_len.to_be_bytes()); + out[oip + 6] = IP_PROTO_UDP; + out[oip + 7] = hop_limit; + out[oip + 8..oip + 24].copy_from_slice(&outer_src_ip.octets()); + out[oip + 24..oip + 40].copy_from_slice(&outer_dst_ip.octets()); + + // === Outer UDP Header (8 bytes) === + let oudp = oip + IPV6_HEADER_LEN; + out[oudp..oudp + 2].copy_from_slice(&outer_src_port.to_be_bytes()); + out[oudp + 2..oudp + 4].copy_from_slice(&outer_dst_port.to_be_bytes()); + out[oudp + 4..oudp + 6].copy_from_slice(&outer_udp_len.to_be_bytes()); + out[oudp + 6..oudp + 8].copy_from_slice(&[0x00, 0x00]); + + // === VXLAN Header (8 bytes) === + let vxlan_off = oudp + UDP_HEADER_LEN; + VxlanHeader::new(vni).encode(&mut out[vxlan_off..vxlan_off + VXLAN_HEADER_LEN]); + + // === Inner Ethernet Header (14 bytes) === + let ieth = vxlan_off + VXLAN_HEADER_LEN; + out[ieth..ieth + 6].copy_from_slice(inner_dst_mac); + out[ieth + 6..ieth + 12].copy_from_slice(inner_src_mac); + out[ieth + 12..ieth + 14].copy_from_slice(Ð_TYPE_IPV4.to_be_bytes()); + + // === Inner IPv4 Header (20 bytes) === + let iip = ieth + ETH_HEADER_LEN; + out[iip] = 0x45; + out[iip + 1] = 0x00; + out[iip + 2..iip + 4].copy_from_slice(&inner_ip_total.to_be_bytes()); + out[iip + 4..iip + 6].copy_from_slice(&[0x00, 0x00]); + out[iip + 6..iip + 8].copy_from_slice(&[0x40, 0x00]); + out[iip + 8] = 64; + out[iip + 9] = IP_PROTO_UDP; + out[iip + 10..iip + 12].copy_from_slice(&[0x00, 0x00]); + out[iip + 12..iip + 16].copy_from_slice(&inner_src_bytes); + out[iip + 16..iip + 20].copy_from_slice(&inner_dst_bytes); + let iip_cksum = ipv4_checksum(&out[iip..iip + IPV4_HEADER_LEN]); + out[iip + 10..iip + 12].copy_from_slice(&iip_cksum.to_be_bytes()); + + // === Inner UDP Header (8 bytes) === + let iudp = iip + IPV4_HEADER_LEN; + out[iudp..iudp + 2].copy_from_slice(&inner_src_port.to_be_bytes()); + out[iudp + 2..iudp + 4].copy_from_slice(&inner_dst_port.to_be_bytes()); + out[iudp + 4..iudp + 6].copy_from_slice(&inner_udp_len.to_be_bytes()); + out[iudp + 6..iudp + 8].copy_from_slice(&[0x00, 0x00]); + + // === Payload === + let poff = iudp + UDP_HEADER_LEN; + out[poff..poff + payload.len()].copy_from_slice(payload); + + // Inner UDP checksum (IPv4) + let inner_udp_cksum = udp_checksum( + &inner_src_bytes, + &inner_dst_bytes, + &out[iudp..iudp + UDP_HEADER_LEN], + payload, + ); + out[iudp + 6..iudp + 8].copy_from_slice(&inner_udp_cksum.to_be_bytes()); + + // Outer UDP checksum (IPv6 — mandatory) + let outer_udp_cksum = udp6_checksum( + &outer_src_ip, + &outer_dst_ip, + &out[oudp..oudp + UDP_HEADER_LEN], + &out[oudp + UDP_HEADER_LEN..total_len], + ); + out[oudp + 6..oudp + 8].copy_from_slice(&outer_udp_cksum.to_be_bytes()); + + Ok(total_len) +} + +/// Result of decapsulating a VXLAN frame with IPv6 outer. +#[derive(Debug)] +pub struct VxlanDecapResult6<'a> { + pub inner_src_ip: Ipv4Addr, + pub inner_dst_ip: Ipv4Addr, + pub inner_src_port: u16, + pub inner_dst_port: u16, + pub payload: &'a [u8], + pub outer_src_ip: Ipv6Addr, + pub vxlan_header: VxlanHeader, + pub inner_src_mac: [u8; 6], + pub inner_dst_mac: [u8; 6], +} + +/// Try to decapsulate a VXLAN frame with IPv6 outer. +pub fn try_decap_vxlan_v6<'a>( + frame: &'a [u8], + l3_offset: usize, + vxlan_local_port: u16, + expected_vni: Option, +) -> Option> { + if frame.len() < l3_offset + IPV6_HEADER_LEN { + return None; + } + let oip = &frame[l3_offset..]; + if (oip[0] >> 4) != 6 { + return None; + } + if oip[6] != IP_PROTO_UDP { + return None; + } + let outer_src_ip = Ipv6Addr::from(<[u8; 16]>::try_from(&oip[8..24]).unwrap()); + + // Outer UDP header + let oudp_off = l3_offset + IPV6_HEADER_LEN; + if frame.len() < oudp_off + UDP_HEADER_LEN { + return None; + } + let outer_dst_port = u16::from_be_bytes([frame[oudp_off + 2], frame[oudp_off + 3]]); + if outer_dst_port != vxlan_local_port { + return None; + } + + // VXLAN header + let vxlan_off = oudp_off + UDP_HEADER_LEN; + let vxlan_hdr = VxlanHeader::parse(&frame[vxlan_off..])?; + + if let Some(expected) = expected_vni { + if vxlan_hdr.vni != expected { + return None; + } + } + + // Inner Ethernet header + let ieth = vxlan_off + VXLAN_HEADER_LEN; + if frame.len() < ieth + ETH_HEADER_LEN { + return None; + } + let inner_dst_mac: [u8; 6] = frame[ieth..ieth + 6].try_into().ok()?; + let inner_src_mac: [u8; 6] = frame[ieth + 6..ieth + 12].try_into().ok()?; + let inner_ethertype = u16::from_be_bytes([frame[ieth + 12], frame[ieth + 13]]); + if inner_ethertype != ETH_TYPE_IPV4 { + return None; + } + + // Inner IPv4 header + let iip_off = ieth + ETH_HEADER_LEN; + if frame.len() < iip_off + IPV4_HEADER_LEN { + return None; + } + let iip = &frame[iip_off..]; + if (iip[0] >> 4) != 4 { + return None; + } + let iip_ihl = (iip[0] & 0x0F) as usize * 4; + if iip_ihl < 20 || frame.len() < iip_off + iip_ihl { + return None; + } + if iip[9] != IP_PROTO_UDP { + return None; + } + let inner_src_ip = Ipv4Addr::new(iip[12], iip[13], iip[14], iip[15]); + let inner_dst_ip = Ipv4Addr::new(iip[16], iip[17], iip[18], iip[19]); + + // Inner UDP header + let iudp_off = iip_off + iip_ihl; + if frame.len() < iudp_off + UDP_HEADER_LEN { + return None; + } + let inner_src_port = u16::from_be_bytes([frame[iudp_off], frame[iudp_off + 1]]); + let inner_dst_port = u16::from_be_bytes([frame[iudp_off + 2], frame[iudp_off + 3]]); + let inner_udp_len = u16::from_be_bytes([frame[iudp_off + 4], frame[iudp_off + 5]]) as usize; + + if inner_udp_len < UDP_HEADER_LEN || frame.len() < iudp_off + inner_udp_len { + return None; + } + + let payload_start = iudp_off + UDP_HEADER_LEN; + let payload_len = inner_udp_len - UDP_HEADER_LEN; + + Some(VxlanDecapResult6 { + inner_src_ip, + inner_dst_ip, + inner_src_port, + inner_dst_port, + payload: &frame[payload_start..payload_start + payload_len], + outer_src_ip, + vxlan_header: vxlan_hdr, + inner_src_mac, + inner_dst_mac, + }) +} + // ============================================================================ // Tests // ============================================================================ @@ -755,4 +1047,203 @@ mod tests { let base_frame = ETH_HEADER_LEN + IPV4_HEADER_LEN + UDP_HEADER_LEN; // plain UDP frame = 42 assert_eq!(frame.len(), base_frame + VXLAN_ENCAP_OVERHEAD); } + + // ========================================================================= + // IPv6 outer tests + // ========================================================================= + + mod ipv6_outer { + use super::*; + use std::net::Ipv6Addr; + use crate::ipv6::{ETH_TYPE_IPV6, IPV6_HEADER_LEN}; + + const OUTER_SRC_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x01]; + const OUTER_DST_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x00, 0x02]; + const INNER_SRC_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x01, 0x01]; + const INNER_DST_MAC: [u8; 6] = [0x02, 0x00, 0x00, 0x00, 0x01, 0x02]; + + fn outer_src() -> Ipv6Addr { "2001:db8::1".parse().unwrap() } + fn outer_dst() -> Ipv6Addr { "2001:db8::2".parse().unwrap() } + fn inner_src() -> Ipv4Addr { Ipv4Addr::new(192, 168, 1, 10) } + fn inner_dst() -> Ipv4Addr { Ipv4Addr::new(192, 168, 1, 20) } + + #[test] + fn config6_defaults() { + let cfg = VxlanConfig6::new(outer_dst(), 100); + assert_eq!(cfg.remote_ip, outer_dst()); + assert_eq!(cfg.vni, 100); + assert_eq!(cfg.remote_port, VXLAN_DEFAULT_PORT); + assert_eq!(cfg.local_port, VXLAN_DEFAULT_PORT); + } + + #[test] + fn config6_builder() { + let cfg = VxlanConfig6::new(outer_dst(), 200) + .with_remote_port(5000) + .with_local_port(5001) + .with_inner_src_mac([0xAA; 6]) + .with_inner_dst_mac([0xBB; 6]); + assert_eq!(cfg.remote_port, 5000); + assert_eq!(cfg.local_port, 5001); + assert_eq!(cfg.inner_src_mac, [0xAA; 6]); + assert_eq!(cfg.inner_dst_mac, [0xBB; 6]); + } + + #[test] + #[should_panic(expected = "VNI must be 24-bit")] + fn config6_rejects_oversized_vni() { + VxlanConfig6::new(outer_dst(), VXLAN_VNI_MAX + 1); + } + + #[test] + fn build_and_decap_roundtrip() { + let payload = b"hello VXLAN IPv6 tunnel"; + let mut frame = Vec::new(); + let len = build_vxlan_frame_into_v6( + &mut frame, + &OUTER_SRC_MAC, &OUTER_DST_MAC, + outer_src(), outer_dst(), + 4789, 4789, 100, + &INNER_SRC_MAC, &INNER_DST_MAC, + inner_src(), inner_dst(), + 9000, 9001, payload, 64, + ).unwrap(); + + assert_eq!(frame.len(), len); + // 14 (eth) + 40 (IPv6) + 8 (UDP) + 8 (VXLAN) + 14 (inner eth) + 20 (inner IPv4) + 8 (inner UDP) + payload + let expected = 14 + 40 + 8 + 8 + 14 + 20 + 8 + payload.len(); + assert_eq!(len, expected); + + let decap = try_decap_vxlan_v6(&frame, ETH_HEADER_LEN, 4789, Some(100)).unwrap(); + assert_eq!(decap.inner_src_ip, inner_src()); + assert_eq!(decap.inner_dst_ip, inner_dst()); + assert_eq!(decap.inner_src_port, 9000); + assert_eq!(decap.inner_dst_port, 9001); + assert_eq!(decap.payload, payload); + assert_eq!(decap.outer_src_ip, outer_src()); + assert_eq!(decap.vxlan_header.vni, 100); + assert_eq!(decap.inner_src_mac, INNER_SRC_MAC); + assert_eq!(decap.inner_dst_mac, INNER_DST_MAC); + } + + #[test] + fn wire_format_ethertype_is_ipv6() { + let mut frame = Vec::new(); + build_vxlan_frame_into_v6( + &mut frame, &OUTER_SRC_MAC, &OUTER_DST_MAC, + outer_src(), outer_dst(), 4789, 4789, 100, + &INNER_SRC_MAC, &INNER_DST_MAC, + inner_src(), inner_dst(), 1, 2, b"x", 64, + ).unwrap(); + let ethertype = u16::from_be_bytes([frame[12], frame[13]]); + assert_eq!(ethertype, ETH_TYPE_IPV6); + } + + #[test] + fn wire_format_ipv6_version() { + let mut frame = Vec::new(); + build_vxlan_frame_into_v6( + &mut frame, &OUTER_SRC_MAC, &OUTER_DST_MAC, + outer_src(), outer_dst(), 4789, 4789, 100, + &INNER_SRC_MAC, &INNER_DST_MAC, + inner_src(), inner_dst(), 1, 2, b"x", 64, + ).unwrap(); + assert_eq!(frame[ETH_HEADER_LEN] >> 4, 6); + } + + #[test] + fn wire_format_outer_udp_checksum_valid() { + let mut frame = Vec::new(); + build_vxlan_frame_into_v6( + &mut frame, &OUTER_SRC_MAC, &OUTER_DST_MAC, + outer_src(), outer_dst(), 4789, 4789, 100, + &INNER_SRC_MAC, &INNER_DST_MAC, + inner_src(), inner_dst(), 9000, 9001, b"checksum test", 64, + ).unwrap(); + assert!(crate::verify_udp6_checksum(&frame)); + } + + #[test] + fn decap_rejects_wrong_port() { + let mut frame = Vec::new(); + build_vxlan_frame_into_v6( + &mut frame, &OUTER_SRC_MAC, &OUTER_DST_MAC, + outer_src(), outer_dst(), 4789, 4789, 100, + &INNER_SRC_MAC, &INNER_DST_MAC, + inner_src(), inner_dst(), 9000, 9001, b"x", 64, + ).unwrap(); + assert!(try_decap_vxlan_v6(&frame, ETH_HEADER_LEN, 5000, Some(100)).is_none()); + } + + #[test] + fn decap_rejects_wrong_vni() { + let mut frame = Vec::new(); + build_vxlan_frame_into_v6( + &mut frame, &OUTER_SRC_MAC, &OUTER_DST_MAC, + outer_src(), outer_dst(), 4789, 4789, 100, + &INNER_SRC_MAC, &INNER_DST_MAC, + inner_src(), inner_dst(), 9000, 9001, b"x", 64, + ).unwrap(); + assert!(try_decap_vxlan_v6(&frame, ETH_HEADER_LEN, 4789, Some(200)).is_none()); + } + + #[test] + fn decap_accepts_any_vni_when_none() { + let mut frame = Vec::new(); + build_vxlan_frame_into_v6( + &mut frame, &OUTER_SRC_MAC, &OUTER_DST_MAC, + outer_src(), outer_dst(), 4789, 4789, 999, + &INNER_SRC_MAC, &INNER_DST_MAC, + inner_src(), inner_dst(), 9000, 9001, b"x", 64, + ).unwrap(); + let decap = try_decap_vxlan_v6(&frame, ETH_HEADER_LEN, 4789, None).unwrap(); + assert_eq!(decap.vxlan_header.vni, 999); + } + + #[test] + fn build_empty_payload() { + let mut frame = Vec::new(); + let len = build_vxlan_frame_into_v6( + &mut frame, &OUTER_SRC_MAC, &OUTER_DST_MAC, + outer_src(), outer_dst(), 4789, 4789, 100, + &INNER_SRC_MAC, &INNER_DST_MAC, + inner_src(), inner_dst(), 9000, 9001, &[], 64, + ).unwrap(); + // 14 + 40 + 8 + 8 + 14 + 20 + 8 = 112 + assert_eq!(len, 112); + let decap = try_decap_vxlan_v6(&frame, ETH_HEADER_LEN, 4789, Some(100)).unwrap(); + assert!(decap.payload.is_empty()); + } + + #[test] + fn encap_overhead_v6_is_correct() { + // outer IPv6(40) + outer UDP(8) + VXLAN(8) + inner Eth(14) = 70 + assert_eq!(VXLAN_ENCAP_OVERHEAD_V6, 70); + } + + #[test] + fn perf_build_decap_cycle_v6() { + let payload = vec![0xAA; 64]; + let mut buf = Vec::with_capacity(1500); + let iterations = 10_000; + + let start = std::time::Instant::now(); + for _ in 0..iterations { + build_vxlan_frame_into_v6( + &mut buf, &OUTER_SRC_MAC, &OUTER_DST_MAC, + outer_src(), outer_dst(), 4789, 4789, 100, + &INNER_SRC_MAC, &INNER_DST_MAC, + inner_src(), inner_dst(), 12345, 9000, &payload, 64, + ).unwrap(); + let _ = try_decap_vxlan_v6(&buf, ETH_HEADER_LEN, 4789, Some(100)).unwrap(); + } + let elapsed = start.elapsed(); + let ns_per_op = elapsed.as_nanos() / iterations as u128; + eprintln!( + "[PERF] VXLAN IPv6-outer build+decap: {} iterations in {:?} ({} ns/op)", + iterations, elapsed, ns_per_op + ); + assert!(ns_per_op < 10_000, "build+decap too slow: {} ns/op", ns_per_op); + } + } } From c84e410ec69c243b63de7653c10b767202b96575 Mon Sep 17 00:00:00 2001 From: Agent Router Date: Thu, 21 May 2026 00:59:44 -0400 Subject: [PATCH 2/2] docs: append perf Run #25 results, mark Encap: IPv6 outer done in roadmap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Performance test results from GH Actions run 26204734648 show no regression from the IPv6 outer encap feature: - rust-dpdk 64B/700K: 699,000 RX (0.1% drop) — identical to Run #18 - rust-dpdk 512B/700K: 699,000 RX (0.1% drop) — identical to Run #18 - NIC instrumentation self-check: OK (zero drift) README roadmap updated to move 'Encap: IPv6 outer' from Planned to Done. --- README.md | 4 +- docs/perf-test-log.md | 89 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6d725ac..229482e 100644 --- a/README.md +++ b/README.md @@ -451,6 +451,8 @@ Integration testing runs on **AWS EC2 with VPC networking**, which has specific **GENEVE endpoint (RFC 8926)** — Modern overlay tunnel: outer Ethernet + outer IPv4 + outer UDP (dst port 6081) + variable-length GENEVE header (24-bit VNI + TLV options up to 252 bytes) + inner Ethernet frame. Same frame shape as VXLAN plus extensible metadata — used by OVN, NSX-T, and AWS Gateway Load Balancer. Configurable per-socket via `set_geneve(Some(GeneveConfig::new(remote_ip, vni)))` or through `NetworkConfig::with_geneve()` on the builder. TX encapsulates transparently — the application calls `send_to(payload, inner_dst)` and the library wraps in the GENEVE tunnel automatically. RX decapsulates matching frames (VNI filter) and returns the inner source address to the application. TLV options are parsed on RX and available via the `GeneveHeader` in decap results. Ships with IPv4 outer; IPv6 outer is added by the "Encap: IPv6 outer" roadmap item. 43 tests (36 unit + 7 integration) including a synthetic PPS benchmark measuring GENEVE build+decap overhead. +**Encap: IPv6 outer** — IPv6 outer support for all three encapsulation protocols (VXLAN, GENEVE, GUE). Each protocol gains `build_*_frame_into_v6()` and `try_decap_*_v6()` functions using outer IPv6 headers with mandatory UDP6 checksum (RFC 8200 §8.1). New `*Config6` structs with `Ipv6Addr`, `*DecapResult6` types, and `*_ENCAP_OVERHEAD_V6` constants. Wire format: `[Outer Eth 14B][Outer IPv6 40B][Outer UDP 8B][Protocol Header][Inner frame]`. 41 unit tests including synthetic PPS benchmarks. *(PR [#60](https://github.com/gspivey/dpdk-stdlib-rust/pull/60))* + ### Planned Each bullet below is a standalone, one-PR-sized deliverable unless noted otherwise. IPv6 is a multi-PR feature with a sub-task checklist; it only moves to Done when every box is ticked and a final performance run shows no regression vs the IPv4 baseline. @@ -467,8 +469,6 @@ Each bullet below is a standalone, one-PR-sized deliverable unless noted otherwi - [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. - ### Not Currently Planned These are features the Linux kernel provides that we intentionally defer to the network infrastructure or consider out of scope: diff --git a/docs/perf-test-log.md b/docs/perf-test-log.md index b4bd7c5..84b97b7 100644 --- a/docs/perf-test-log.md +++ b/docs/perf-test-log.md @@ -5,6 +5,95 @@ Each entry captures the git context, test configuration, results, and analysis. **Standard benchmarks** (include in every run entry): 1. **Hardware PPS** — TRex on c6in.xlarge (measures NIC + DPDK + application stack) + +## Run #25: Encap IPv6 Outer (GUE, VXLAN, GENEVE) — No Regression + +| Field | Value | +|-------|-------| +| **Date** | 2026-05-21 | +| **Git Hash** | `28febfd` | +| **Branch** | `agent/encap-ipv6-outer` | +| **PR** | [#60](https://github.com/gspivey/dpdk-stdlib-rust/pull/60) | +| **GH Actions Run** | [26204734648](https://github.com/gspivey/dpdk-stdlib-rust/actions/runs/26204734648) | +| **Instance Type** | c6in.xlarge (4 vCPU, 6.25 Gbps baseline / 30 Gbps burst) | +| **Traffic Generator** | TRex | + +### Changes Since Run #18 + +1. **`28febfd` — IPv6 outer support for GUE, VXLAN, GENEVE.** Adds `build_*_frame_into_v6()` and `try_decap_*_v6()` functions for all three encap protocols, using outer IPv6 headers with mandatory UDP6 checksum (RFC 8200 §8.1). New `*Config6`, `*DecapResult6`, and `*_ENCAP_OVERHEAD_V6` types/constants. 41 new unit tests including synthetic PPS benchmarks. This is purely additive — zero changes to the existing IPv4 encap or plain UDP hot paths. + +### Results: Hardware (TRex) + +#### 64-byte packets + +| Target PPS | rust-dpdk RX | Drop | Kernel RX | Drop | native-dpdk RX | Drop | +|-----------|-------------|------|----------|------|---------------|------| +| 70,000 | 69,000 | 1.4% | 69,000 | 1.4% | 70,000 | 0.0% | +| 140,000 | 139,000 | 0.7% | 139,000 | 0.7% | 140,000 | 0.0% | +| 350,000 | 348,996 | 0.3% | 348,912 | 0.3% | 350,000 | 0.0% | +| 700,000 | 699,000 | 0.1% | 400,193 | 42.8% | 698,643 | 0.2% | + +#### 512-byte packets + +| Target PPS | rust-dpdk RX | Drop | Kernel RX | Drop | native-dpdk RX | Drop | +|-----------|-------------|------|----------|------|---------------|------| +| 70,000 | 69,000 | 1.4% | 69,000 | 1.4% | 70,000 | 0.0% | +| 140,000 | 139,000 | 0.7% | 139,000 | 0.7% | 140,000 | 0.0% | +| 350,000 | 349,000 | 0.3% | 348,883 | 0.3% | 350,000 | 0.0% | +| 700,000 | 699,000 | 0.1% | 582,543 | 16.8% | 698,348 | 0.2% | + +#### 1400-byte packets (near MTU) + +| Target PPS | rust-dpdk RX | Drop | Kernel RX | Drop | native-dpdk RX | Drop | +|-----------|-------------|------|----------|------|---------------|------| +| 70,000 | 69,000 | 1.4% | 69,000 | 1.4% | 70,000 | 0.0% | +| 140,000 | 139,000 | 0.7% | 139,000 | 0.7% | 140,000 | 0.0% | +| 350,000 | 349,000 | 0.3% | 348,957 | 0.3% | 350,000 | 0.0% | +| 700,000 | 569,926 | 18.6% | 583,762 | 16.6% | 674,637 | 3.6% | + +#### 8500-byte packets (jumbo) + +| Target PPS | rust-dpdk RX | Drop | Kernel RX | Drop | native-dpdk RX | Drop | +|-----------|-------------|------|----------|------|---------------|------| +| 70,000 | 69,000 | 1.4% | 33,908 | 51.6% | 70,000 | 0.0% | +| 140,000 | 124,350 | 0.7% | 124,329 | 0.7% | 125,301 | 0.0% | +| 350,000 | 123,085 | 1.7% | 124,013 | 1.0% | 120,300 | 3.9% | + +#### tokio-dpdk (async compat layer) + +| Target PPS | tokio-dpdk RX | Drop | +|-----------|--------------|------| +| 70,000 | 69,000 | 1.4% | +| 140,000 | 139,000 | 0.7% | +| 350,000 | 307,647 | 12.1% | +| 700,000 | 307,850 | 56.0% | + +### NIC Drops Instrumentation Self-Check + +| Config | Status | imissed (expected / actual / Δ) | ierrors (expected / actual / Δ) | rx_nombuf (expected / actual / Δ) | +|--------|--------|--------------------------------|----------------------------------|-----------------------------------| +| native-dpdk | no instrumentation | — | — | — | +| rust-dpdk | **OK** | 0 / 0 / 0 | 422,439 / 422,439 / 0 | 0 / 0 / 0 | +| tokio-dpdk | **OK** | 0 / 0 / 0 | 263,721 / 263,721 / 0 | 0 / 0 / 0 | +| plain-rust | no instrumentation | — | — | — | + +### Analysis + +**No performance regression from IPv6 outer encap.** The feature adds new `build_*_frame_into_v6()` and `try_decap_*_v6()` functions alongside the existing IPv4 encap code. Zero changes to the existing hot path — no new branches, no new Option checks in `send_to_addr()` or `process_frame_zerocopy()`. + +**rust-dpdk at 700K PPS, 64B**: 699,000 RX (0.1% drop) — matches Run #18's 699,000 exactly. + +**rust-dpdk at 700K PPS, 512B**: 699,000 RX (0.1% drop) — matches Run #18's 699,000 exactly. + +**rust-dpdk at 700K PPS, 1400B**: 569,926 RX (18.6% drop) — identical to Run #18's 569,926. + +**rust-dpdk vs native-dpdk parity**: At 700K PPS with 64B packets, Rust delivers 699,000 vs native C's 698,643 — Rust is marginally ahead (within measurement noise). At 350K PPS, both deliver ~349K with <0.3% drops. + +**tokio-dpdk**: Caps at ~307K PPS at 350K+ target — consistent with Run #18's 307,647, confirming the async compat layer ceiling is unchanged. + +**Conclusion**: IPv6 outer encap is performance-neutral. The new code paths are only invoked when the IPv6 outer build/decap functions are explicitly called — they do not affect the existing IPv4 encap or plain UDP paths. + +--- 2. **Synthetic PPS** — `cargo test -- --nocapture vlan_pps_benchmark` (measures pure CPU overhead of RX processing pipeline, independent of NIC speed; ~5s to run) 3. **HW VLAN Strip** — `cargo test -- --nocapture hw_vlan_strip_benchmark` (measures cost of frame reconstruction vs direct hw_vlan_tci passthrough; regression guard for the RX VLAN offload path)