From 21bf72d3d15460811376ee4afd34f2bb0d9f1de2 Mon Sep 17 00:00:00 2001 From: Lee Smet Date: Tue, 3 Mar 2026 11:01:13 +0100 Subject: [PATCH] Only attempt UDP coalescing in tun interface if USO is enabled Signed-off-by: Lee Smet --- mycelium-tun/src/linux.rs | 14 ++++++++++++-- mycelium-tun/src/offload.rs | 27 +++++++++++++++++++-------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/mycelium-tun/src/linux.rs b/mycelium-tun/src/linux.rs index 896a4ce..2afd151 100644 --- a/mycelium-tun/src/linux.rs +++ b/mycelium-tun/src/linux.rs @@ -92,6 +92,7 @@ nix::ioctl_write_ptr_bad!( pub struct Tun { fd: OwnedFd, name: String, + uso_enabled: bool, } impl Tun { @@ -140,7 +141,8 @@ impl Tun { let uso_flags = offload_flags | TUN_F_USO4 | TUN_F_USO6; // SAFETY: file is a valid TUN fd. let ret = unsafe { libc::ioctl(file.as_raw_fd(), TUNSETOFFLOAD, uso_flags) }; - if ret == 0 { + let uso_enabled = ret == 0; + if uso_enabled { debug!(name = %actual_name, "enabled USO offload"); } @@ -155,6 +157,7 @@ impl Tun { Ok(Tun { fd: owned_fd, name: actual_name, + uso_enabled, }) } @@ -260,6 +263,7 @@ impl Tun { let write_half = WriteHalf { fd: AsyncFd::new(write_fd)?, write_buf: vec![0u8; READ_BUF_SIZE], + uso_enabled: self.uso_enabled, }; Ok((read_half, write_half)) @@ -351,6 +355,8 @@ pub struct WriteHalf { fd: AsyncFd, /// Internal buffer for building coalesced packets (virtio_net_hdr + payload). write_buf: Vec, + /// Whether USO (UDP Segmentation Offload) is available on this device. + uso_enabled: bool, } impl WriteHalf { @@ -366,7 +372,11 @@ impl WriteHalf { let mut remaining = pkts; while remaining.len() > 1 { - match offload::gro_coalesce(remaining, &mut self.write_buf[VIRTIO_NET_HDR_LEN..]) { + match offload::gro_coalesce( + remaining, + &mut self.write_buf[VIRTIO_NET_HDR_LEN..], + self.uso_enabled, + ) { Ok((len, vhdr, consumed)) => { vhdr.encode(&mut self.write_buf[..VIRTIO_NET_HDR_LEN]); self.write_raw(&self.write_buf[..VIRTIO_NET_HDR_LEN + len]) diff --git a/mycelium-tun/src/offload.rs b/mycelium-tun/src/offload.rs index 6f70528..e62dcef 100644 --- a/mycelium-tun/src/offload.rs +++ b/mycelium-tun/src/offload.rs @@ -433,7 +433,11 @@ fn pseudo_header_acc(pkt: &[u8], info: &HeaderInfo) -> u64 { /// If even the first two packets cannot be coalesced (incompatible flows, single packet, etc.), /// returns an error. The caller should then fall back to writing the first packet individually /// with a `GSO_NONE` header. -pub fn gro_coalesce(pkts: &[&[u8]], out: &mut [u8]) -> io::Result<(usize, VirtioNetHdr, usize)> { +pub fn gro_coalesce( + pkts: &[&[u8]], + out: &mut [u8], + can_uso: bool, +) -> io::Result<(usize, VirtioNetHdr, usize)> { if pkts.len() < 2 { return Err(io::Error::new( io::ErrorKind::InvalidInput, @@ -446,6 +450,13 @@ pub fn gro_coalesce(pkts: &[&[u8]], out: &mut [u8]) -> io::Result<(usize, Virtio let is_tcp = info.protocol == IPPROTO_TCP; + if !is_tcp && !can_uso { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "USO not available for UDP coalescing", + )); + } + // Extract flow key from first packet. let flow = FlowKey::from_packet(first, &info)?; @@ -876,7 +887,7 @@ mod tests { let pkts: Vec<&[u8]> = vec![&pkt0, &pkt1]; let mut out = vec![0u8; 65536]; - let (len, vhdr, consumed) = gro_coalesce(&pkts, &mut out).unwrap(); + let (len, vhdr, consumed) = gro_coalesce(&pkts, &mut out, true).unwrap(); assert_eq!(consumed, 2); assert_eq!(vhdr.gso_type, VIRTIO_NET_HDR_GSO_TCPV4); @@ -909,7 +920,7 @@ mod tests { let pkts: Vec<&[u8]> = vec![&pkt0, &pkt1]; let mut out = vec![0u8; 65536]; - assert!(gro_coalesce(&pkts, &mut out).is_err()); + assert!(gro_coalesce(&pkts, &mut out, true).is_err()); } #[test] @@ -936,7 +947,7 @@ mod tests { let pkts: Vec<&[u8]> = vec![&pkt0, &pkt1]; let mut out = vec![0u8; 65536]; - assert!(gro_coalesce(&pkts, &mut out).is_err()); + assert!(gro_coalesce(&pkts, &mut out, true).is_err()); } #[test] @@ -974,7 +985,7 @@ mod tests { // Coalesce. let pkts: Vec<&[u8]> = vec![&pkt0, &pkt1, &pkt2]; let mut coalesced = vec![0u8; 65536]; - let (len, vhdr, consumed) = gro_coalesce(&pkts, &mut coalesced).unwrap(); + let (len, vhdr, consumed) = gro_coalesce(&pkts, &mut coalesced, true).unwrap(); assert_eq!(consumed, 3); // Build raw buffer as if read from kernel. @@ -1049,7 +1060,7 @@ mod tests { let pkts: Vec<&[u8]> = vec![&pkt0, &pkt1]; let mut out = vec![0u8; 65536]; - let (len, vhdr, consumed) = gro_coalesce(&pkts, &mut out).unwrap(); + let (len, vhdr, consumed) = gro_coalesce(&pkts, &mut out, true).unwrap(); assert_eq!(consumed, 2); assert_eq!(vhdr.gso_type, VIRTIO_NET_HDR_GSO_UDP_L4); @@ -1102,7 +1113,7 @@ mod tests { let out_size = header_len + PAYLOAD_SIZE * 2 + 10; // just enough for 2 let mut out = vec![0u8; out_size]; - let (len, vhdr, consumed) = gro_coalesce(&pkts, &mut out).unwrap(); + let (len, vhdr, consumed) = gro_coalesce(&pkts, &mut out, true).unwrap(); assert_eq!(consumed, 2); assert_eq!(vhdr.gso_type, VIRTIO_NET_HDR_GSO_UDP_L4); @@ -1145,7 +1156,7 @@ mod tests { let pkts: Vec<&[u8]> = vec![&pkt0, &pkt1, &pkt2]; let mut out = vec![0u8; 65536]; - let (len, vhdr, consumed) = gro_coalesce(&pkts, &mut out).unwrap(); + let (len, vhdr, consumed) = gro_coalesce(&pkts, &mut out, true).unwrap(); // Should coalesce only the first 2 packets. assert_eq!(consumed, 2);