1use std::io;
13use std::net::SocketAddr;
14use std::os::fd::RawFd;
15use std::time::Instant;
16
17use io_uring::{cqueue, opcode, squeue, types, IoUring};
18
19use crate::poll::{Dispatch, DispatchOut, EgressDatagram};
20use crate::{sockaddr_to_std, std_to_sockaddr, MAX_DATAGRAM_BATCH, MAX_WIRE_DATAGRAM};
21
22const RING_ENTRIES: u32 = 1024;
28const RING_BUFS: usize = 256;
29const TUN_READ_DEPTH: usize = 16;
30const UDP_RECV_DEPTH: usize = 256;
50const BUF_GROUP: u16 = 17;
51const SEND_SLOTS: usize = 256;
52const TAG_SHIFT: u32 = 56;
53const TAG_UDP_RECV: u64 = 1_u64 << TAG_SHIFT;
54const TAG_TUN_RECV: u64 = 2_u64 << TAG_SHIFT;
55const TAG_REPROVIDE: u64 = 3_u64 << TAG_SHIFT;
56const TAG_SEND_SLOT: u64 = 4_u64 << TAG_SHIFT;
57const TAG_KIND_MASK: u64 = 0xff_u64 << TAG_SHIFT;
58const TAG_PAYLOAD_MASK: u64 = !TAG_KIND_MASK;
59const SQE_FLAGS_BUFFER_SELECT: squeue::Flags = squeue::Flags::BUFFER_SELECT;
60const MAX_WIRE_DATAGRAM_I32: i32 = 2048;
61const MAX_WIRE_DATAGRAM_U32: u32 = 2048;
62const RING_BUFS_U16: u16 = 256;
63const MAX_GSO_DATAGRAMS: usize = MAX_DATAGRAM_BATCH;
64const MAX_GSO_PAYLOAD: usize = MAX_WIRE_DATAGRAM * MAX_GSO_DATAGRAMS;
65const MAX_PENDING_GSO_DATAGRAMS: usize = 512;
68const CQ_SPIN_BUDGET: u32 = 2_000_000;
77const TICK_TIMEOUT_NS: u32 = 10_000_000;
81const GSO_CONTROL_PAYLOAD_LEN_U32: u32 = 2;
82const GSO_CONTROL_SPACE: usize = 64;
83
84#[derive(Clone, Copy)]
85struct GsoMeta {
86 segment_size: u16,
87 datagram_count: usize,
88 dst: SocketAddr,
93}
94
95#[derive(Clone, Copy, PartialEq, Eq)]
99enum SendKind {
100 Udp,
101 Tun,
102}
103
104struct GsoSendContext {
110 iov: libc::iovec,
111 msg: libc::msghdr,
112 control: [u8; GSO_CONTROL_SPACE],
113 name: libc::sockaddr_storage,
114}
115
116impl GsoSendContext {
117 fn new() -> Self {
118 Self {
119 iov: libc::iovec {
120 iov_base: std::ptr::null_mut(),
121 iov_len: 0,
122 },
123 msg: crate::empty_msghdr(),
124 control: [0_u8; GSO_CONTROL_SPACE],
125 name: unsafe { std::mem::zeroed() },
129 }
130 }
131
132 fn set_destination(&mut self, dst: SocketAddr) {
134 let (storage, len) = std_to_sockaddr(dst);
135 self.name = storage;
136 self.msg.msg_name = std::ptr::addr_of_mut!(self.name).cast::<libc::c_void>();
137 self.msg.msg_namelen = len;
138 }
139
140 fn prepare_plain(&mut self, payload_ptr: *mut u8, payload_len: usize, dst: SocketAddr) {
142 self.iov.iov_base = payload_ptr.cast::<libc::c_void>();
143 self.iov.iov_len = payload_len;
144 self.msg.msg_iov = std::ptr::addr_of_mut!(self.iov);
145 self.msg.msg_iovlen = 1;
146 self.msg.msg_control = std::ptr::null_mut();
147 self.msg.msg_controllen = 0;
148 self.msg.msg_flags = 0;
149 self.set_destination(dst);
150 }
151
152 fn prepare_gso(
154 &mut self,
155 payload_ptr: *mut u8,
156 payload_len: usize,
157 segment_size: u16,
158 dst: SocketAddr,
159 ) -> io::Result<()> {
160 self.iov.iov_base = payload_ptr.cast::<libc::c_void>();
161 self.iov.iov_len = payload_len;
162 self.msg.msg_iov = std::ptr::addr_of_mut!(self.iov);
163 self.msg.msg_iovlen = 1;
164 self.msg.msg_control = self.control.as_mut_ptr().cast::<libc::c_void>();
165 let cmsg_space = usize::try_from(unsafe { libc::CMSG_SPACE(GSO_CONTROL_PAYLOAD_LEN_U32) })
168 .map_err(|_| io::Error::other("cmsg space does not fit usize"))?;
169 if cmsg_space > self.control.len() {
170 return Err(io::Error::other("GSO control buffer too small"));
171 }
172 self.msg.msg_controllen = cmsg_space as crate::CmsgLen;
173 self.msg.msg_flags = 0;
174 self.set_destination(dst);
175
176 unsafe {
180 let cmsg = libc::CMSG_FIRSTHDR(std::ptr::addr_of!(self.msg));
181 if cmsg.is_null() {
182 return Err(io::Error::other("missing first cmsg header"));
183 }
184 (*cmsg).cmsg_level = libc::SOL_UDP;
185 (*cmsg).cmsg_type = libc::UDP_SEGMENT;
186 let cmsg_len: usize = usize::try_from(libc::CMSG_LEN(GSO_CONTROL_PAYLOAD_LEN_U32))
187 .map_err(|_| io::Error::other("cmsg len does not fit usize"))?;
188 (*cmsg).cmsg_len = cmsg_len as crate::CmsgLen;
189 let segment_ptr = libc::CMSG_DATA(cmsg).cast::<u16>();
190 *segment_ptr = segment_size;
191 }
192 Ok(())
193 }
194}
195
196struct UdpRecvSlot {
205 buf: [u8; MAX_WIRE_DATAGRAM],
206 name: libc::sockaddr_storage,
207 iov: libc::iovec,
208 msg: libc::msghdr,
209}
210
211impl UdpRecvSlot {
212 fn zeroed() -> Self {
217 unsafe { std::mem::zeroed() }
222 }
223}
224
225pub struct UringDriver {
227 ring: IoUring,
228 udp_fd: RawFd,
229 tun_fd: RawFd,
230 recv_pool: Box<[[u8; MAX_WIRE_DATAGRAM]; RING_BUFS]>,
231 udp_recv_slots: Box<[UdpRecvSlot]>,
234 in_flight: Vec<Option<Vec<u8>>>,
235 gso_meta: Vec<Option<GsoMeta>>,
236 gso_ctx: Vec<Option<GsoSendContext>>,
237 send_kind: Vec<Option<SendKind>>,
238 free_bufs: Vec<Vec<u8>>,
241 recv_scratch: Vec<u8>,
244 cqe_buf: Vec<(u64, i32, u32)>,
247 busy_poll: bool,
250 active: bool,
254 pending_gso: Vec<EgressDatagram>,
258 ext_arg: bool,
261 gso_enabled: bool,
262 started: Instant,
263 dropped_sends: u64,
267 #[cfg(test)]
268 gso_submission_count: usize,
269 #[cfg(test)]
270 force_gso_submit_failure: bool,
271}
272
273impl UringDriver {
274 pub fn new(udp_fd: RawFd, tun_fd: RawFd) -> io::Result<Self> {
276 let ring = IoUring::new(RING_ENTRIES)?;
277 let ext_arg = ring.params().is_feature_ext_arg();
278 let recv_pool = Box::new([[0_u8; MAX_WIRE_DATAGRAM]; RING_BUFS]);
279
280 let mut udp_recv_slots: Box<[UdpRecvSlot]> = (0..UDP_RECV_DEPTH)
284 .map(|_| UdpRecvSlot::zeroed())
285 .collect::<Vec<_>>()
286 .into_boxed_slice();
287 let namelen = libc::socklen_t::try_from(std::mem::size_of::<libc::sockaddr_storage>())
288 .expect("size_of::<sockaddr_storage>() fits socklen_t");
289 for slot in udp_recv_slots.iter_mut() {
290 slot.iov = libc::iovec {
291 iov_base: slot.buf.as_mut_ptr().cast::<libc::c_void>(),
292 iov_len: slot.buf.len(),
293 };
294 slot.msg = crate::empty_msghdr();
295 slot.msg.msg_name = std::ptr::addr_of_mut!(slot.name).cast::<libc::c_void>();
296 slot.msg.msg_namelen = namelen;
297 slot.msg.msg_iov = std::ptr::addr_of_mut!(slot.iov);
298 slot.msg.msg_iovlen = 1;
299 }
300
301 let mut in_flight = Vec::with_capacity(SEND_SLOTS);
302 let mut gso_meta = Vec::with_capacity(SEND_SLOTS);
303 let mut gso_ctx = Vec::with_capacity(SEND_SLOTS);
304 let mut send_kind = Vec::with_capacity(SEND_SLOTS);
305 for _ in 0..SEND_SLOTS {
306 in_flight.push(None);
307 gso_meta.push(None);
308 gso_ctx.push(None);
309 send_kind.push(None);
310 }
311
312 let mut driver = Self {
313 ring,
314 udp_fd,
315 tun_fd,
316 recv_pool,
317 udp_recv_slots,
318 in_flight,
319 gso_meta,
320 gso_ctx,
321 send_kind,
322 free_bufs: Vec::with_capacity(SEND_SLOTS),
323 recv_scratch: Vec::with_capacity(MAX_WIRE_DATAGRAM),
324 cqe_buf: Vec::with_capacity(
325 usize::try_from(RING_ENTRIES).expect("RING_ENTRIES fits usize"),
326 ),
327 busy_poll: std::env::var_os("YIP_URING_BUSYPOLL").is_some(),
328 active: false,
329 pending_gso: Vec::with_capacity(MAX_PENDING_GSO_DATAGRAMS),
330 ext_arg,
331 gso_enabled: true,
332 started: Instant::now(),
333 dropped_sends: 0,
334 #[cfg(test)]
335 gso_submission_count: 0,
336 #[cfg(test)]
337 force_gso_submit_failure: false,
338 };
339
340 driver.provide_all_buffers()?;
341 for i in 0..UDP_RECV_DEPTH {
342 driver.arm_udp_recv_slot(i)?;
343 }
344 for _ in 0..TUN_READ_DEPTH {
345 driver.arm_tun_read()?;
346 }
347 driver.ring.submit()?;
348 Ok(driver)
349 }
350
351 fn push_entry(&mut self, entry: io_uring::squeue::Entry) -> io::Result<()> {
352 let first_try = unsafe { self.ring.submission().push(&entry) };
355 if first_try.is_ok() {
356 return Ok(());
357 }
358
359 self.ring.submit()?;
360 unsafe {
362 self.ring
363 .submission()
364 .push(&entry)
365 .map_err(|_| io::Error::other("uring submission queue full"))?;
366 }
367 Ok(())
368 }
369
370 fn provide_all_buffers(&mut self) -> io::Result<()> {
371 let base_ptr = self.recv_pool.as_mut_ptr().cast::<u8>();
372 let entry = opcode::ProvideBuffers::new(
373 base_ptr,
374 MAX_WIRE_DATAGRAM_I32,
375 RING_BUFS_U16,
376 BUF_GROUP,
377 0,
378 )
379 .build()
380 .user_data(TAG_REPROVIDE);
381 self.push_entry(entry)?;
382 self.submit_and_wait_1()?;
383 let cqe = self
384 .ring
385 .completion()
386 .next()
387 .ok_or_else(|| io::Error::other("missing provide-buffer completion"))?;
388 if cqe.result() < 0 {
389 return Err(io::Error::from_raw_os_error(-cqe.result()));
390 }
391 Ok(())
392 }
393
394 fn reprovide_buffer(&mut self, bid: u16) -> io::Result<()> {
395 let idx = usize::from(bid);
396 if idx >= RING_BUFS {
397 return Err(io::Error::other("provided buffer id out of range"));
398 }
399 let ptr = self.recv_pool[idx].as_mut_ptr();
400 let entry = opcode::ProvideBuffers::new(ptr, MAX_WIRE_DATAGRAM_I32, 1, BUF_GROUP, bid)
401 .build()
402 .user_data(TAG_REPROVIDE | u64::from(bid));
403 self.push_entry(entry)
404 }
405
406 fn arm_udp_recv_slot(&mut self, idx: usize) -> io::Result<()> {
413 let namelen = libc::socklen_t::try_from(std::mem::size_of::<libc::sockaddr_storage>())
420 .expect("size_of::<sockaddr_storage>() fits socklen_t");
421 self.udp_recv_slots[idx].msg.msg_namelen = namelen;
422 let msg_ptr = std::ptr::addr_of_mut!(self.udp_recv_slots[idx].msg);
423 let tag = TAG_UDP_RECV | u64::try_from(idx).expect("udp recv slot index fits u64");
424 let entry = opcode::RecvMsg::new(types::Fd(self.udp_fd), msg_ptr)
425 .build()
426 .user_data(tag);
427 self.push_entry(entry)
428 }
429
430 fn arm_tun_read(&mut self) -> io::Result<()> {
431 let entry = opcode::Read::new(
434 types::Fd(self.tun_fd),
435 std::ptr::null_mut(),
436 MAX_WIRE_DATAGRAM_U32,
437 )
438 .buf_group(BUF_GROUP)
439 .build()
440 .flags(SQE_FLAGS_BUFFER_SELECT)
441 .user_data(TAG_TUN_RECV);
442 self.push_entry(entry)?;
443 Ok(())
444 }
445
446 fn now_ms(&self) -> u64 {
447 u64::try_from(self.started.elapsed().as_millis()).unwrap_or(u64::MAX)
448 }
449
450 #[cfg(test)]
451 fn gso_submission_count(&self) -> usize {
452 self.gso_submission_count
453 }
454
455 #[cfg(test)]
456 fn in_flight_used_count(&self) -> usize {
457 self.in_flight.iter().filter(|slot| slot.is_some()).count()
458 }
459
460 #[cfg(test)]
461 fn dropped_sends(&self) -> u64 {
462 self.dropped_sends
463 }
464
465 #[cfg(test)]
466 fn set_force_gso_submit_failure(&mut self, force: bool) {
467 self.force_gso_submit_failure = force;
468 }
469
470 fn store_in_flight(&mut self, buf: Vec<u8>) -> io::Result<usize> {
473 let Some((idx, slot)) = self
474 .in_flight
475 .iter_mut()
476 .enumerate()
477 .find(|(_, slot)| slot.is_none())
478 else {
479 self.recycle_buf(buf);
480 return Err(io::Error::new(
481 io::ErrorKind::WouldBlock,
482 "no free in-flight send slots available",
483 ));
484 };
485 *slot = Some(buf);
486 Ok(idx)
487 }
488
489 fn alloc_in_flight_slot(
492 &mut self,
493 payload: Vec<u8>,
494 payload_limit: usize,
495 ) -> io::Result<usize> {
496 if payload.len() > payload_limit {
497 self.recycle_buf(payload);
498 return Err(io::Error::other("payload exceeds in-flight slot limit"));
499 }
500 self.store_in_flight(payload)
501 }
502
503 fn alloc_in_flight_slot_copy(
506 &mut self,
507 data: &[u8],
508 payload_limit: usize,
509 ) -> io::Result<usize> {
510 if data.len() > payload_limit {
511 return Err(io::Error::other("payload exceeds in-flight slot limit"));
512 }
513 let mut buf = self.free_bufs.pop().unwrap_or_default();
514 buf.clear();
515 buf.extend_from_slice(data);
516 self.store_in_flight(buf)
517 }
518
519 fn recycle_buf(&mut self, buf: Vec<u8>) {
522 if self.free_bufs.len() < SEND_SLOTS {
523 self.free_bufs.push(buf);
524 }
525 }
526
527 fn queue_udp_send(&mut self, dg: &EgressDatagram) -> io::Result<()> {
530 let slot_id = self.alloc_in_flight_slot_copy(&dg.bytes, MAX_WIRE_DATAGRAM)?;
531 self.send_kind[slot_id] = Some(SendKind::Udp);
532 let (payload_ptr, payload_len) = {
533 let slot_buf = self.in_flight[slot_id]
534 .as_mut()
535 .ok_or_else(|| io::Error::other("missing in-flight buffer for udp send"))?;
536 (slot_buf.as_mut_ptr(), slot_buf.len())
537 };
538 let ctx = self.gso_ctx[slot_id].get_or_insert_with(GsoSendContext::new);
539 ctx.prepare_plain(payload_ptr, payload_len, dg.dst);
540 let tag = TAG_SEND_SLOT | u64::try_from(slot_id).expect("slot id fits u64");
541 let msg_ptr = std::ptr::from_ref(&ctx.msg);
542 let entry = opcode::SendMsg::new(types::Fd(self.udp_fd), msg_ptr)
543 .flags(u32::try_from(libc::MSG_NOSIGNAL).expect("MSG_NOSIGNAL fits u32"))
544 .build()
545 .user_data(tag);
546 if let Err(e) = self.push_entry(entry) {
547 self.release_in_flight_slot(slot_id);
548 return Err(e);
549 }
550 Ok(())
551 }
552
553 fn queue_tun_write(&mut self, frame: &[u8]) -> io::Result<()> {
554 let slot_id = self.alloc_in_flight_slot_copy(frame, MAX_WIRE_DATAGRAM)?;
555 self.send_kind[slot_id] = Some(SendKind::Tun);
556 let (ptr, len_u32) = {
557 let slot_buf = self.in_flight[slot_id]
558 .as_ref()
559 .ok_or_else(|| io::Error::other("missing in-flight buffer for tun write"))?;
560 let len_u32 = u32::try_from(slot_buf.len())
561 .map_err(|_| io::Error::other("tun write buffer too large"))?;
562 (slot_buf.as_ptr(), len_u32)
563 };
564 let tag = TAG_SEND_SLOT | u64::try_from(slot_id).expect("slot id fits u64");
565 let entry = opcode::Write::new(types::Fd(self.tun_fd), ptr, len_u32)
566 .build()
567 .user_data(tag);
568 if let Err(e) = self.push_entry(entry) {
569 self.release_in_flight_slot(slot_id);
570 return Err(e);
571 }
572 Ok(())
573 }
574
575 fn release_in_flight_slot(&mut self, slot_id: usize) {
576 if slot_id >= self.in_flight.len() {
577 return;
578 }
579 if let Some(buf) = self.in_flight[slot_id].take() {
580 self.recycle_buf(buf);
581 }
582 self.gso_meta[slot_id] = None;
583 self.gso_ctx[slot_id] = None;
584 self.send_kind[slot_id] = None;
585 }
586
587 fn can_coalesce_gso_tagged(datagrams: &[EgressDatagram]) -> Option<u16> {
595 crate::gso::can_coalesce(datagrams)
596 }
597
598 fn queue_udp_batch_tagged(
604 &mut self,
605 datagrams: &[EgressDatagram],
606 allow_gso: bool,
607 ) -> io::Result<()> {
608 if datagrams.is_empty() {
609 return Ok(());
610 }
611 if allow_gso && self.gso_enabled {
612 if let Some(segment_size) = Self::can_coalesce_gso_tagged(datagrams) {
613 let max_chunk = Self::max_gso_datagrams_for_segment(segment_size);
614 for chunk in datagrams.chunks(max_chunk) {
615 let dst = chunk[0].dst;
619 if self.queue_udp_gso(chunk, segment_size, dst)? {
620 continue;
621 }
622 eprintln!("uring: GSO submit failed, trying per-datagram sends");
623 for dg in chunk {
624 self.queue_udp_send(dg)?;
625 }
626 }
627 return Ok(());
628 }
629 }
630 for dg in datagrams {
631 self.queue_udp_send(dg)?;
632 }
633 Ok(())
634 }
635
636 fn flush_pending_gso(&mut self) {
651 while !self.pending_gso.is_empty() {
652 let mut chunk: Vec<EgressDatagram> = Vec::with_capacity(self.pending_gso.len());
653 let mut deferred: Vec<EgressDatagram> = Vec::with_capacity(self.pending_gso.len());
654 for dg in self.pending_gso.drain(..) {
655 let dst_conflict = chunk
656 .first()
657 .is_some_and(|c: &EgressDatagram| c.dst != dg.dst);
658 let fate_conflict = chunk.iter().any(|c| c.fate == dg.fate);
659 if dst_conflict || fate_conflict {
660 deferred.push(dg);
661 } else {
662 chunk.push(dg);
663 }
664 }
665 self.pending_gso = deferred;
666 if let Err(e) = self.queue_udp_batch_tagged(&chunk, true) {
667 self.dropped_sends += 1;
668 eprintln!(
669 "uring: drop udp send batch from tun: {e} (dropped_sends={})",
670 self.dropped_sends
671 );
672 }
673 }
674 }
675
676 fn max_gso_datagrams_for_segment(segment_size: u16) -> usize {
677 crate::gso::max_gso_run_len(segment_size, MAX_GSO_DATAGRAMS)
678 }
679
680 fn queue_udp_gso<T: AsRef<[u8]>>(
681 &mut self,
682 datagrams: &[T],
683 segment_size: u16,
684 dst: SocketAddr,
685 ) -> io::Result<bool> {
686 if datagrams.len() > MAX_GSO_DATAGRAMS {
687 return Ok(false);
688 }
689 let payload_len = usize::from(segment_size);
690 let total_len = payload_len
691 .checked_mul(datagrams.len())
692 .ok_or_else(|| io::Error::other("GSO payload size overflow"))?;
693 if total_len > MAX_GSO_PAYLOAD {
694 return Ok(false);
695 }
696 let mut coalesced = self.free_bufs.pop().unwrap_or_default();
697 coalesced.clear();
698 coalesced.reserve(total_len);
699 for datagram in datagrams {
700 coalesced.extend_from_slice(datagram.as_ref());
701 }
702 let slot_id = match self.alloc_in_flight_slot(coalesced, MAX_GSO_PAYLOAD) {
703 Ok(slot_id) => slot_id,
704 Err(_) => return Ok(false),
705 };
706 self.send_kind[slot_id] = Some(SendKind::Udp);
707 #[cfg(test)]
708 if self.force_gso_submit_failure {
709 self.release_in_flight_slot(slot_id);
710 return Ok(false);
711 }
712 let payload_ptr = {
713 let payload = self.in_flight[slot_id]
714 .as_mut()
715 .ok_or_else(|| io::Error::other("missing in-flight buffer for GSO send"))?;
716 payload.as_mut_ptr()
717 };
718 let payload_len_now = self.in_flight[slot_id]
719 .as_ref()
720 .ok_or_else(|| io::Error::other("missing in-flight GSO payload"))?
721 .len();
722 let ctx = self.gso_ctx[slot_id].get_or_insert_with(GsoSendContext::new);
723 if let Err(e) = ctx.prepare_gso(payload_ptr, payload_len_now, segment_size, dst) {
724 self.release_in_flight_slot(slot_id);
725 return Err(e);
726 }
727 self.gso_meta[slot_id] = Some(GsoMeta {
728 segment_size,
729 datagram_count: datagrams.len(),
730 dst,
731 });
732 let tag = TAG_SEND_SLOT | u64::try_from(slot_id).expect("slot id fits u64");
733 let msg_ptr = std::ptr::from_ref(&ctx.msg);
734 let entry = opcode::SendMsg::new(types::Fd(self.udp_fd), msg_ptr)
735 .flags(u32::try_from(libc::MSG_NOSIGNAL).expect("MSG_NOSIGNAL fits u32"))
736 .build()
737 .user_data(tag);
738 if self.push_entry(entry).is_err() {
739 self.release_in_flight_slot(slot_id);
740 return Ok(false);
741 }
742 #[cfg(test)]
743 {
744 self.gso_submission_count = self.gso_submission_count.saturating_add(1);
745 }
746 Ok(true)
747 }
748
749 fn recover_gso_fallback_datagrams(&self, slot_id: usize) -> Vec<EgressDatagram> {
756 let Some(meta) = self.gso_meta.get(slot_id).and_then(|meta| *meta) else {
757 return Vec::new();
758 };
759 let Some(payload) = self.in_flight.get(slot_id).and_then(|slot| slot.as_ref()) else {
760 return Vec::new();
761 };
762 let segment_len = usize::from(meta.segment_size);
763 let mut datagrams = Vec::with_capacity(meta.datagram_count);
764 for i in 0..meta.datagram_count {
765 let start = i
766 .checked_mul(segment_len)
767 .expect("segment index multiplication should not overflow");
768 let end = start
769 .checked_add(segment_len)
770 .expect("segment end computation should not overflow");
771 if end > payload.len() {
772 break;
773 }
774 datagrams.push(EgressDatagram {
775 fate: 0,
776 dst: meta.dst,
777 bytes: payload[start..end].to_vec(),
778 });
779 }
780 datagrams
781 }
782
783 fn recover_gso_unsent_datagrams(
786 &self,
787 slot_id: usize,
788 bytes_sent: usize,
789 ) -> Vec<EgressDatagram> {
790 let Some(meta) = self.gso_meta.get(slot_id).and_then(|meta| *meta) else {
791 return Vec::new();
792 };
793 let Some(payload) = self.in_flight.get(slot_id).and_then(|slot| slot.as_ref()) else {
794 return Vec::new();
795 };
796 let segment_len = usize::from(meta.segment_size);
797 if segment_len == 0 || bytes_sent >= payload.len() {
798 return Vec::new();
799 }
800 let sent_segments = bytes_sent / segment_len;
801 if sent_segments >= meta.datagram_count {
802 return Vec::new();
803 }
804
805 let mut datagrams = Vec::with_capacity(meta.datagram_count - sent_segments);
806 for i in sent_segments..meta.datagram_count {
807 let start = i
808 .checked_mul(segment_len)
809 .expect("segment index multiplication should not overflow");
810 let end = start
811 .checked_add(segment_len)
812 .expect("segment end computation should not overflow");
813 if end > payload.len() {
814 break;
815 }
816 datagrams.push(EgressDatagram {
817 fate: 0,
818 dst: meta.dst,
819 bytes: payload[start..end].to_vec(),
820 });
821 }
822 datagrams
823 }
824
825 fn is_gso_unsupported_errno(errno: i32) -> bool {
826 errno == libc::EIO
827 || errno == libc::EINVAL
828 || errno == libc::ENOPROTOOPT
829 || errno == libc::EOPNOTSUPP
830 || errno == libc::ENOTSUP
831 }
832
833 fn is_transient_send_errno(errno: i32) -> bool {
837 errno == libc::EAGAIN || errno == libc::EWOULDBLOCK || errno == libc::ENOBUFS
838 }
839
840 fn submit_and_wait_1(&self) -> io::Result<()> {
848 loop {
849 match self.ring.submit_and_wait(1) {
850 Ok(_) => return Ok(()),
851 Err(e) if e.raw_os_error() == Some(libc::EINTR) => continue,
852 Err(e) => return Err(e),
853 }
854 }
855 }
856
857 fn wait_for_completions(&self) -> io::Result<bool> {
864 if !self.ext_arg {
865 self.submit_and_wait_1()?;
866 return Ok(false);
867 }
868 let ts = types::Timespec::new().nsec(TICK_TIMEOUT_NS);
869 let args = types::SubmitArgs::new().timespec(&ts);
870 loop {
871 match self.ring.submitter().submit_with_args(1, &args) {
872 Ok(_) => return Ok(false),
873 Err(e) if e.raw_os_error() == Some(libc::ETIME) => return Ok(true),
874 Err(e) if e.raw_os_error() == Some(libc::EINTR) => continue,
875 Err(e) => return Err(e),
876 }
877 }
878 }
879
880 fn handle_dispatch_udp(
881 &mut self,
882 d: &mut impl Dispatch,
883 src: SocketAddr,
884 datagram: &[u8],
885 now_ms: u64,
886 ) {
887 match d.on_udp(src, datagram, now_ms) {
888 DispatchOut::None => {}
889 DispatchOut::Tun(inner) => {
890 if let Err(e) = self.queue_tun_write(inner) {
891 self.dropped_sends += 1;
892 eprintln!(
893 "uring: drop tun write: {e} (dropped_sends={})",
894 self.dropped_sends
895 );
896 }
897 }
898 DispatchOut::Udp(pkts) => {
899 if let Err(e) = self.queue_udp_batch_tagged(pkts, false) {
902 self.dropped_sends += 1;
903 eprintln!(
904 "uring: drop udp send batch: {e} (dropped_sends={})",
905 self.dropped_sends
906 );
907 }
908 }
909 DispatchOut::Both(inner, pkts) => {
910 if let Err(e) = self.queue_tun_write(inner) {
911 self.dropped_sends += 1;
912 eprintln!(
913 "uring: drop tun write: {e} (dropped_sends={})",
914 self.dropped_sends
915 );
916 }
917 if let Err(e) = self.queue_udp_batch_tagged(pkts, false) {
918 self.dropped_sends += 1;
919 eprintln!(
920 "uring: drop udp send batch: {e} (dropped_sends={})",
921 self.dropped_sends
922 );
923 }
924 }
925 }
926 }
927
928 fn handle_dispatch_tun(&mut self, d: &mut impl Dispatch, frame: &[u8], now_ms: u64) {
929 self.pending_gso.extend_from_slice(d.on_tun(frame, now_ms));
933 if self.pending_gso.len() >= MAX_PENDING_GSO_DATAGRAMS {
934 self.flush_pending_gso();
935 }
936 }
937
938 pub fn poll_once<D: Dispatch>(&mut self, d: &mut D) -> io::Result<()> {
940 self.ring.submit()?;
944 let mut cqes = std::mem::take(&mut self.cqe_buf);
948 cqes.clear();
949 let budget = if self.busy_poll && self.active {
956 CQ_SPIN_BUDGET
957 } else {
958 0
959 };
960 let mut spins = 0_u32;
961 loop {
962 for cqe in &mut self.ring.completion() {
963 cqes.push((cqe.user_data(), cqe.result(), cqe.flags()));
964 }
965 if !cqes.is_empty() {
966 self.active = true;
969 break;
970 }
971 if spins >= budget {
972 if self.wait_for_completions()? {
977 self.active = false;
978 break;
979 }
980 } else {
981 spins += 1;
982 std::hint::spin_loop();
983 }
984 }
985
986 let now_ms = self.now_ms();
987 for &(user_data, result, flags) in &cqes {
988 let kind = user_data & TAG_KIND_MASK;
989 if kind == TAG_SEND_SLOT {
990 let slot_id_u64 = user_data & TAG_PAYLOAD_MASK;
991 let slot_id =
992 usize::try_from(slot_id_u64).expect("send slot id user_data fits usize");
993 if result < 0 {
994 let errno = -result;
995 if self.gso_meta.get(slot_id).and_then(|meta| *meta).is_some() {
996 let fallback_datagrams = self.recover_gso_fallback_datagrams(slot_id);
997 self.release_in_flight_slot(slot_id);
998 if Self::is_gso_unsupported_errno(errno) {
999 self.gso_enabled = false;
1000 eprintln!(
1001 "uring: UDP_SEGMENT unsupported ({errno}); disabling GSO and retrying per datagram"
1002 );
1003 } else {
1004 eprintln!(
1005 "uring: GSO send completion error ({errno}); retrying per datagram"
1006 );
1007 }
1008 for datagram in &fallback_datagrams {
1009 if let Err(e) = self.queue_udp_send(datagram) {
1010 eprintln!("uring: drop GSO fallback datagram: {e}");
1011 }
1012 }
1013 continue;
1014 }
1015 let kind = self.send_kind.get(slot_id).and_then(|k| *k);
1023 let err = io::Error::from_raw_os_error(errno);
1024 self.release_in_flight_slot(slot_id);
1025 match kind {
1026 Some(SendKind::Tun) => {
1027 eprintln!("uring: tun write error on slot {slot_id} (dropped): {err}");
1028 continue;
1029 }
1030 _ => {
1031 if Self::is_transient_send_errno(errno) {
1032 eprintln!("uring: udp send dropped on slot {slot_id}: {err}");
1033 continue;
1034 }
1035 return Err(err);
1036 }
1037 }
1038 } else if let Some(meta) = self.gso_meta.get(slot_id).and_then(|entry| *entry) {
1039 let bytes_sent =
1040 usize::try_from(result).expect("non-negative send result fits usize");
1041 let expected_len = usize::from(meta.segment_size)
1042 .checked_mul(meta.datagram_count)
1043 .expect("GSO expected payload length should not overflow");
1044 if bytes_sent < expected_len {
1045 let unsent = self.recover_gso_unsent_datagrams(slot_id, bytes_sent);
1046 self.release_in_flight_slot(slot_id);
1047 eprintln!(
1048 "uring: partial GSO completion ({bytes_sent}/{expected_len}); retrying {} datagrams",
1049 unsent.len()
1050 );
1051 for datagram in &unsent {
1052 if let Err(e) = self.queue_udp_send(datagram) {
1053 eprintln!("uring: drop partial GSO retry datagram: {e}");
1054 }
1055 }
1056 continue;
1057 }
1058 }
1059 self.release_in_flight_slot(slot_id);
1060 continue;
1061 }
1062
1063 if kind == TAG_REPROVIDE {
1064 if result < 0 {
1065 eprintln!(
1066 "uring: provide-buffer completion error: {}",
1067 io::Error::from_raw_os_error(-result)
1068 );
1069 }
1070 continue;
1071 }
1072
1073 if kind == TAG_UDP_RECV {
1074 let slot_idx_u64 = user_data & TAG_PAYLOAD_MASK;
1075 let slot_idx =
1076 usize::try_from(slot_idx_u64).expect("udp recv slot index fits usize");
1077 if slot_idx >= self.udp_recv_slots.len() {
1078 return Err(io::Error::other(
1079 "kernel returned udp recv slot out of range",
1080 ));
1081 }
1082 if result < 0 {
1083 let errno = -result;
1084 if errno == libc::EAGAIN || errno == libc::EWOULDBLOCK || errno == libc::ENOBUFS
1090 {
1091 self.arm_udp_recv_slot(slot_idx)?;
1092 continue;
1093 }
1094 return Err(io::Error::other(format!(
1095 "udp recv completion error: {}",
1096 io::Error::from_raw_os_error(errno)
1097 )));
1098 }
1099 let n = usize::try_from(result).expect("non-negative CQE result fits usize");
1100 if n > MAX_WIRE_DATAGRAM {
1101 self.arm_udp_recv_slot(slot_idx)?;
1102 return Err(io::Error::other("kernel returned oversized datagram"));
1103 }
1104 let namelen = self.udp_recv_slots[slot_idx].msg.msg_namelen;
1108 let src = match sockaddr_to_std(&self.udp_recv_slots[slot_idx].name, namelen) {
1109 Ok(addr) => addr,
1110 Err(e) => {
1111 eprintln!(
1112 "uring: dropping udp datagram with unparseable source address: {e}"
1113 );
1114 self.arm_udp_recv_slot(slot_idx)?;
1115 continue;
1116 }
1117 };
1118 let mut scratch = std::mem::take(&mut self.recv_scratch);
1119 scratch.clear();
1120 scratch.extend_from_slice(&self.udp_recv_slots[slot_idx].buf[..n]);
1121 self.handle_dispatch_udp(d, src, &scratch, now_ms);
1122 self.recv_scratch = scratch;
1123 self.arm_udp_recv_slot(slot_idx)?;
1124 continue;
1125 }
1126
1127 let bid_opt = cqueue::buffer_select(flags);
1130 if result < 0 {
1131 let errno = -result;
1132 let err = io::Error::from_raw_os_error(errno);
1133 if let Some(bid) = bid_opt {
1134 self.reprovide_buffer(bid)?;
1135 }
1136 if (errno == libc::EAGAIN || errno == libc::EWOULDBLOCK || errno == libc::ENOBUFS)
1143 && kind == TAG_TUN_RECV
1144 {
1145 self.arm_tun_read()?;
1146 continue;
1147 }
1148 if kind == TAG_TUN_RECV {
1149 return Err(io::Error::other(format!(
1150 "tun recv completion error: {err}"
1151 )));
1152 }
1153 return Err(io::Error::other(format!(
1154 "unexpected completion kind 0x{kind:016x} with error {err}"
1155 )));
1156 }
1157
1158 let bid = if kind == TAG_TUN_RECV {
1159 bid_opt.ok_or_else(|| {
1160 io::Error::other(
1161 "recv completion missing buffer_select; aborting to avoid pool-slot leak",
1162 )
1163 })?
1164 } else {
1165 if let Some(bid) = bid_opt {
1166 self.reprovide_buffer(bid)?;
1167 }
1168 return Err(io::Error::other(format!(
1169 "unexpected completion kind 0x{kind:016x}"
1170 )));
1171 };
1172
1173 let idx = usize::from(bid);
1174 if idx >= RING_BUFS {
1175 return Err(io::Error::other("kernel returned buffer id out of range"));
1176 }
1177 let n = usize::try_from(result).expect("non-negative CQE result fits usize");
1178 if n > MAX_WIRE_DATAGRAM {
1179 self.reprovide_buffer(bid)?;
1180 return Err(io::Error::other("kernel returned oversized datagram"));
1181 }
1182
1183 let mut scratch = std::mem::take(&mut self.recv_scratch);
1184 scratch.clear();
1185 scratch.extend_from_slice(&self.recv_pool[idx][..n]);
1186 self.handle_dispatch_tun(d, &scratch, now_ms);
1187 self.recv_scratch = scratch;
1188 self.reprovide_buffer(bid)?;
1189 self.arm_tun_read()?;
1190 }
1191
1192 self.flush_pending_gso();
1194
1195 if let Some(pkts) = d.tick(now_ms) {
1196 for pkt in pkts {
1197 if let Err(e) = self.queue_udp_send(pkt) {
1198 eprintln!("uring: drop tick packet: {e}");
1199 }
1200 }
1201 }
1202
1203 self.cqe_buf = cqes;
1206 Ok(())
1207 }
1208}
1209
1210pub fn run_uring<D: Dispatch>(udp_fd: RawFd, tun_fd: RawFd, d: &mut D) -> io::Result<()> {
1216 let mut driver = match UringDriver::new(udp_fd, tun_fd) {
1223 Ok(driver) => driver,
1224 Err(e) => {
1225 eprintln!("uring: init failed ({e}); falling back to PollDriver");
1226 return crate::poll::run_poll(udp_fd, tun_fd, false, d);
1227 }
1228 };
1229 loop {
1230 if let Err(e) = driver.poll_once(d) {
1231 eprintln!("uring: fatal driver error ({e}); falling back to PollDriver");
1232 drop(driver);
1233 return crate::poll::run_poll(udp_fd, tun_fd, false, d);
1234 }
1235 }
1236}
1237
1238pub fn uring_available() -> bool {
1240 let mut ring = match IoUring::new(8) {
1241 Ok(r) => r,
1242 Err(_) => return false,
1243 };
1244 let mut tiny = [0_u8; 64];
1245 let entry = opcode::ProvideBuffers::new(tiny.as_mut_ptr(), 64, 1, BUF_GROUP, 0)
1246 .build()
1247 .user_data(TAG_REPROVIDE);
1248 let push_res = unsafe { ring.submission().push(&entry) };
1251 if push_res.is_err() {
1252 return false;
1253 }
1254 if ring.submit_and_wait(1).is_err() {
1255 return false;
1256 }
1257 let Some(cqe) = ring.completion().next() else {
1258 return false;
1259 };
1260 cqe.result() >= 0
1261}
1262
1263#[cfg(test)]
1264mod tests {
1265 use super::*;
1266 use std::net::UdpSocket;
1267 use std::os::fd::AsRawFd;
1268 use std::sync::Mutex;
1269 use std::time::Duration;
1270
1271 static URING_SERIAL: Mutex<()> = Mutex::new(());
1272
1273 struct EchoDispatch {
1274 scratch: Vec<EgressDatagram>,
1275 }
1276
1277 impl EchoDispatch {
1278 fn new() -> Self {
1279 Self {
1280 scratch: Vec::new(),
1281 }
1282 }
1283 }
1284
1285 impl Dispatch for EchoDispatch {
1286 fn on_udp(&mut self, src: SocketAddr, dg: &[u8], _now_ms: u64) -> DispatchOut<'_> {
1287 self.scratch = vec![EgressDatagram {
1288 fate: 0,
1289 dst: src,
1290 bytes: dg.to_vec(),
1291 }];
1292 DispatchOut::Udp(&self.scratch)
1293 }
1294
1295 fn on_tun(&mut self, _inner: &[u8], _now_ms: u64) -> &[EgressDatagram] {
1296 &[]
1297 }
1298
1299 fn tick(&mut self, _now_ms: u64) -> Option<&[EgressDatagram]> {
1300 None
1301 }
1302 }
1303
1304 struct TickCountDispatch {
1307 ticks: usize,
1308 }
1309
1310 impl Dispatch for TickCountDispatch {
1311 fn on_udp(&mut self, _src: SocketAddr, _dg: &[u8], _now_ms: u64) -> DispatchOut<'_> {
1312 DispatchOut::None
1313 }
1314
1315 fn on_tun(&mut self, _inner: &[u8], _now_ms: u64) -> &[EgressDatagram] {
1316 &[]
1317 }
1318
1319 fn tick(&mut self, _now_ms: u64) -> Option<&[EgressDatagram]> {
1320 self.ticks += 1;
1321 None
1322 }
1323 }
1324
1325 struct GsoDispatch {
1329 dst: SocketAddr,
1330 scratch: Vec<EgressDatagram>,
1331 }
1332
1333 impl GsoDispatch {
1334 fn new(dst: SocketAddr) -> Self {
1335 Self {
1336 dst,
1337 scratch: Vec::new(),
1338 }
1339 }
1340 }
1341
1342 impl Dispatch for GsoDispatch {
1343 fn on_udp(&mut self, _src: SocketAddr, _dg: &[u8], _now_ms: u64) -> DispatchOut<'_> {
1344 DispatchOut::None
1345 }
1346
1347 fn on_tun(&mut self, _inner: &[u8], _now_ms: u64) -> &[EgressDatagram] {
1348 self.scratch.clear();
1349 for i in 0_u8..5_u8 {
1350 let mut datagram = vec![b'a'; 64];
1351 datagram[0] = b'0' + i;
1352 self.scratch.push(EgressDatagram {
1353 fate: 7,
1354 dst: self.dst,
1355 bytes: datagram,
1356 });
1357 }
1358 &self.scratch
1359 }
1360
1361 fn tick(&mut self, _now_ms: u64) -> Option<&[EgressDatagram]> {
1362 None
1363 }
1364 }
1365
1366 struct GsoLargeBatchDispatch {
1371 dst: SocketAddr,
1372 scratch: Vec<EgressDatagram>,
1373 datagram_count: usize,
1374 datagram_size: usize,
1375 }
1376
1377 impl GsoLargeBatchDispatch {
1378 fn new(dst: SocketAddr, datagram_count: usize, datagram_size: usize) -> Self {
1379 Self {
1380 dst,
1381 scratch: Vec::new(),
1382 datagram_count,
1383 datagram_size,
1384 }
1385 }
1386 }
1387
1388 impl Dispatch for GsoLargeBatchDispatch {
1389 fn on_udp(&mut self, _src: SocketAddr, _dg: &[u8], _now_ms: u64) -> DispatchOut<'_> {
1390 DispatchOut::None
1391 }
1392
1393 fn on_tun(&mut self, _inner: &[u8], _now_ms: u64) -> &[EgressDatagram] {
1394 self.scratch.clear();
1395 for i in 0..self.datagram_count {
1396 let mut datagram = vec![b'z'; self.datagram_size];
1397 datagram[0] = u8::try_from(i % 251).expect("modulo bound fits u8");
1398 self.scratch.push(EgressDatagram {
1399 fate: u16::try_from(i).expect("datagram_count fits u16"),
1400 dst: self.dst,
1401 bytes: datagram,
1402 });
1403 }
1404 &self.scratch
1405 }
1406
1407 fn tick(&mut self, _now_ms: u64) -> Option<&[EgressDatagram]> {
1408 None
1409 }
1410 }
1411
1412 fn make_pipe() -> io::Result<(RawFd, RawFd)> {
1413 let mut fds = [0_i32; 2];
1414 let rc = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC) };
1416 if rc < 0 {
1417 return Err(io::Error::last_os_error());
1418 }
1419 Ok((fds[0], fds[1]))
1420 }
1421
1422 fn write_pipe_once(fd: RawFd, bytes: &[u8]) {
1423 let wrote = unsafe { libc::write(fd, bytes.as_ptr().cast::<libc::c_void>(), bytes.len()) };
1426 assert!(wrote >= 0, "pipe write should succeed");
1427 assert_eq!(
1428 usize::try_from(wrote).expect("non-negative write count fits usize"),
1429 bytes.len(),
1430 "must write full trigger frame into pipe"
1431 );
1432 }
1433
1434 fn test_dst() -> SocketAddr {
1435 "127.0.0.1:1".parse().expect("valid test address")
1436 }
1437
1438 fn other_dst() -> SocketAddr {
1439 "127.0.0.1:2".parse().expect("valid test address")
1440 }
1441
1442 fn dg(fate: u16, len: usize) -> EgressDatagram {
1443 dg_to(fate, len, test_dst())
1444 }
1445
1446 fn dg_to(fate: u16, len: usize, dst: SocketAddr) -> EgressDatagram {
1447 EgressDatagram {
1448 fate,
1449 dst,
1450 bytes: vec![b'x'; len],
1451 }
1452 }
1453
1454 #[test]
1455 fn can_coalesce_gso_tagged_rejects_duplicate_fate() {
1456 let dgs = [dg(3, 64), dg(3, 64)];
1459 assert!(UringDriver::can_coalesce_gso_tagged(&dgs).is_none());
1460 }
1461
1462 #[test]
1463 fn can_coalesce_gso_tagged_accepts_distinct_fates_same_length() {
1464 let dgs = [dg(3, 64), dg(4, 64), dg(5, 64)];
1465 assert_eq!(UringDriver::can_coalesce_gso_tagged(&dgs), Some(64));
1466 }
1467
1468 #[test]
1469 fn can_coalesce_gso_tagged_rejects_mismatched_length() {
1470 let dgs = [dg(3, 64), dg(4, 32)];
1471 assert!(UringDriver::can_coalesce_gso_tagged(&dgs).is_none());
1472 }
1473
1474 #[test]
1475 fn can_coalesce_gso_tagged_rejects_mixed_destinations() {
1476 let dgs = [dg_to(3, 64, test_dst()), dg_to(4, 64, other_dst())];
1480 assert!(UringDriver::can_coalesce_gso_tagged(&dgs).is_none());
1481 }
1482
1483 #[test]
1484 fn uring_loopback_roundtrip_recycles_recv_buffers() {
1485 let _guard = URING_SERIAL
1486 .lock()
1487 .unwrap_or_else(std::sync::PoisonError::into_inner);
1488 let a = UdpSocket::bind("127.0.0.1:0").expect("bind sender");
1489 let b = UdpSocket::bind("127.0.0.1:0").expect("bind uring peer");
1490 a.connect(b.local_addr().expect("sender local addr"))
1491 .expect("connect sender");
1492 b.connect(a.local_addr().expect("uring peer local addr"))
1493 .expect("connect uring peer");
1494 a.set_nonblocking(true).expect("set sender nonblocking");
1495
1496 let (tun_rd, tun_wr) = make_pipe().expect("make tun placeholder pipe");
1497 let mut dispatch = EchoDispatch::new();
1498
1499 let mut driver = match UringDriver::new(b.as_raw_fd(), tun_rd) {
1500 Ok(driver) => driver,
1501 Err(_) => {
1502 eprintln!(
1503 "SKIP uring_loopback_roundtrip_recycles_recv_buffers: \
1504 io_uring ring or buffer registration unavailable (RLIMIT_MEMLOCK)"
1505 );
1506 unsafe {
1508 libc::close(tun_rd);
1509 libc::close(tun_wr);
1510 }
1511 return;
1512 }
1513 };
1514
1515 let total = 384_usize;
1524 const WINDOW: usize = 64;
1525 let mut sent_payloads = Vec::with_capacity(total);
1526 let mut got = Vec::with_capacity(total);
1527 let mut recv_buf = [0_u8; MAX_WIRE_DATAGRAM];
1528 let mut next_to_send = 0_usize;
1529 let mut stall = 0_usize;
1530 for _ in 0..8000 {
1531 while next_to_send < total && next_to_send < got.len() + WINDOW {
1534 let payload = format!("uring-echo-{next_to_send}").into_bytes();
1535 a.send(&payload).expect("send test datagram");
1536 sent_payloads.push(payload);
1537 next_to_send += 1;
1538 }
1539
1540 driver.poll_once(&mut dispatch).expect("poll_once succeeds");
1541
1542 let before = got.len();
1543 loop {
1544 match a.recv(&mut recv_buf) {
1545 Ok(n) => got.push(recv_buf[..n].to_vec()),
1546 Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
1547 Err(e) => panic!("recv failed: {e}"),
1548 }
1549 }
1550 if got.len() >= total {
1551 break;
1552 }
1553 if got.len() == before {
1558 stall += 1;
1559 if stall >= 200 {
1560 break;
1561 }
1562 } else {
1563 stall = 0;
1564 }
1565 }
1566
1567 unsafe {
1569 libc::close(tun_rd);
1570 libc::close(tun_wr);
1571 }
1572
1573 assert!(
1580 got.len() > RING_BUFS,
1581 "expected more than RING_BUFS={RING_BUFS} datagrams to round-trip \
1582 (proving recv buffers were recycled), got {}",
1583 got.len()
1584 );
1585 for payload in &got {
1587 assert!(
1588 sent_payloads.contains(payload),
1589 "received a payload that was never sent"
1590 );
1591 }
1592 drop(driver);
1593 std::thread::sleep(Duration::from_millis(200));
1594 }
1595
1596 #[test]
1597 fn uring_idle_poll_fires_tick_via_timeout() {
1598 let _guard = URING_SERIAL
1599 .lock()
1600 .unwrap_or_else(std::sync::PoisonError::into_inner);
1601 let sock = UdpSocket::bind("127.0.0.1:0").expect("bind idle udp");
1604 let (tun_rd, tun_wr) = make_pipe().expect("make tun placeholder pipe");
1605 let mut driver = match UringDriver::new(sock.as_raw_fd(), tun_rd) {
1606 Ok(driver) => driver,
1607 Err(_) => {
1608 eprintln!("SKIP uring_idle_poll_fires_tick_via_timeout: io_uring unavailable");
1609 unsafe {
1611 libc::close(tun_rd);
1612 libc::close(tun_wr);
1613 }
1614 return;
1615 }
1616 };
1617
1618 let mut dispatch = TickCountDispatch { ticks: 0 };
1619 let started = std::time::Instant::now();
1622 driver
1623 .poll_once(&mut dispatch)
1624 .expect("idle poll_once returns via the tick timeout");
1625 assert!(
1626 dispatch.ticks >= 1,
1627 "tick must fire on the idle-timeout path (fired {})",
1628 dispatch.ticks
1629 );
1630 assert!(
1631 started.elapsed() < Duration::from_secs(2),
1632 "idle poll_once must return promptly via timeout, not block"
1633 );
1634
1635 drop(driver);
1636 unsafe {
1638 libc::close(tun_rd);
1639 libc::close(tun_wr);
1640 }
1641 }
1642
1643 #[test]
1644 fn uring_gso_same_object_datagrams_are_never_coalesced() {
1645 let _guard = URING_SERIAL
1646 .lock()
1647 .unwrap_or_else(std::sync::PoisonError::into_inner);
1648 let a = UdpSocket::bind("127.0.0.1:0").expect("bind sender");
1649 let b = UdpSocket::bind("127.0.0.1:0").expect("bind uring peer");
1650 a.connect(b.local_addr().expect("sender local addr"))
1651 .expect("connect sender");
1652 b.connect(a.local_addr().expect("uring peer local addr"))
1653 .expect("connect uring peer");
1654 a.set_nonblocking(true).expect("set sender nonblocking");
1655
1656 let (tun_rd, tun_wr) = make_pipe().expect("make tun placeholder pipe");
1657 let mut dispatch = GsoDispatch::new(a.local_addr().expect("sender local addr"));
1660 let mut driver = match UringDriver::new(b.as_raw_fd(), tun_rd) {
1661 Ok(driver) => driver,
1662 Err(_) => {
1663 eprintln!(
1664 "SKIP uring_gso_same_object_datagrams_are_never_coalesced: \
1665 io_uring ring or buffer registration unavailable (RLIMIT_MEMLOCK)"
1666 );
1667 unsafe {
1669 libc::close(tun_rd);
1670 libc::close(tun_wr);
1671 }
1672 return;
1673 }
1674 };
1675
1676 write_pipe_once(tun_wr, b"trigger-gso");
1677 let mut got = Vec::new();
1678 let mut recv_buf = [0_u8; MAX_WIRE_DATAGRAM];
1679 for _ in 0..1024 {
1680 driver.poll_once(&mut dispatch).expect("poll_once succeeds");
1681 loop {
1682 match a.recv(&mut recv_buf) {
1683 Ok(n) => got.push(recv_buf[..n].to_vec()),
1684 Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
1685 Err(e) => panic!("recv failed: {e}"),
1686 }
1687 }
1688 if got.len() >= 5 {
1689 break;
1690 }
1691 }
1692
1693 assert_eq!(
1697 driver.gso_submission_count(),
1698 0,
1699 "same-object (same-fate) datagrams must never be GSO-coalesced"
1700 );
1701
1702 let expected: Vec<Vec<u8>> = (0_u8..5_u8)
1703 .map(|i| {
1704 let mut datagram = vec![b'a'; 64];
1705 datagram[0] = b'0' + i;
1706 datagram
1707 })
1708 .collect();
1709 assert_eq!(got.len(), expected.len(), "must receive all datagrams");
1710 for datagram in expected {
1711 assert!(got.contains(&datagram), "missing datagram from output");
1712 }
1713
1714 unsafe {
1716 libc::close(tun_rd);
1717 libc::close(tun_wr);
1718 }
1719 drop(driver);
1720 std::thread::sleep(Duration::from_millis(200));
1721 }
1722
1723 #[test]
1724 fn uring_gso_submit_fallback_uses_per_datagram_send() {
1725 let _guard = URING_SERIAL
1726 .lock()
1727 .unwrap_or_else(std::sync::PoisonError::into_inner);
1728 let a = UdpSocket::bind("127.0.0.1:0").expect("bind sender");
1729 let b = UdpSocket::bind("127.0.0.1:0").expect("bind uring peer");
1730 a.connect(b.local_addr().expect("sender local addr"))
1731 .expect("connect sender");
1732 b.connect(a.local_addr().expect("uring peer local addr"))
1733 .expect("connect uring peer");
1734 a.set_nonblocking(true).expect("set sender nonblocking");
1735
1736 let (tun_rd, tun_wr) = make_pipe().expect("make tun placeholder pipe");
1737 let mut dispatch =
1739 GsoLargeBatchDispatch::new(a.local_addr().expect("sender local addr"), 5, 64);
1740 let mut driver = match UringDriver::new(b.as_raw_fd(), tun_rd) {
1741 Ok(driver) => driver,
1742 Err(_) => {
1743 eprintln!(
1744 "SKIP uring_gso_submit_fallback_uses_per_datagram_send: \
1745 io_uring ring or buffer registration unavailable (RLIMIT_MEMLOCK)"
1746 );
1747 unsafe {
1749 libc::close(tun_rd);
1750 libc::close(tun_wr);
1751 }
1752 return;
1753 }
1754 };
1755 driver.set_force_gso_submit_failure(true);
1756
1757 write_pipe_once(tun_wr, b"trigger-gso-fallback");
1758 let mut got = Vec::new();
1759 let mut recv_buf = [0_u8; MAX_WIRE_DATAGRAM];
1760 for _ in 0..1024 {
1761 driver.poll_once(&mut dispatch).expect("poll_once succeeds");
1762 loop {
1763 match a.recv(&mut recv_buf) {
1764 Ok(n) => got.push(recv_buf[..n].to_vec()),
1765 Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
1766 Err(e) => panic!("recv failed: {e}"),
1767 }
1768 }
1769 if got.len() >= 5 {
1770 break;
1771 }
1772 }
1773
1774 assert_eq!(
1775 driver.gso_submission_count(),
1776 0,
1777 "forced submit failure should prevent GSO send submissions"
1778 );
1779 let expected: Vec<Vec<u8>> = (0..5_usize)
1780 .map(|i| {
1781 let mut datagram = vec![b'z'; 64];
1782 datagram[0] = u8::try_from(i % 251).expect("fits u8");
1783 datagram
1784 })
1785 .collect();
1786 assert_eq!(
1787 got.len(),
1788 expected.len(),
1789 "must receive all fallback datagrams"
1790 );
1791 for datagram in expected {
1792 assert!(
1793 got.contains(&datagram),
1794 "missing datagram from fallback output"
1795 );
1796 }
1797
1798 unsafe {
1800 libc::close(tun_rd);
1801 libc::close(tun_wr);
1802 }
1803 drop(driver);
1804 std::thread::sleep(Duration::from_millis(200));
1805 }
1806
1807 #[test]
1808 fn uring_gso_large_batch_chunks_payload_to_udp_limits() {
1809 let _guard = URING_SERIAL
1810 .lock()
1811 .unwrap_or_else(std::sync::PoisonError::into_inner);
1812 let a = UdpSocket::bind("127.0.0.1:0").expect("bind sender");
1813 let b = UdpSocket::bind("127.0.0.1:0").expect("bind uring peer");
1814 a.connect(b.local_addr().expect("sender local addr"))
1815 .expect("connect sender");
1816 b.connect(a.local_addr().expect("uring peer local addr"))
1817 .expect("connect uring peer");
1818 a.set_nonblocking(true).expect("set sender nonblocking");
1819
1820 let (tun_rd, tun_wr) = make_pipe().expect("make tun placeholder pipe");
1821 let datagram_count = 64usize;
1822 let datagram_size = 1400usize;
1823 let mut dispatch = GsoLargeBatchDispatch::new(
1824 a.local_addr().expect("sender local addr"),
1825 datagram_count,
1826 datagram_size,
1827 );
1828 let mut driver = match UringDriver::new(b.as_raw_fd(), tun_rd) {
1829 Ok(driver) => driver,
1830 Err(_) => {
1831 eprintln!(
1832 "SKIP uring_gso_large_batch_chunks_payload_to_udp_limits: \
1833 io_uring ring or buffer registration unavailable (RLIMIT_MEMLOCK)"
1834 );
1835 unsafe {
1837 libc::close(tun_rd);
1838 libc::close(tun_wr);
1839 }
1840 return;
1841 }
1842 };
1843
1844 write_pipe_once(tun_wr, b"trigger-large-gso");
1845 for _ in 0..2048 {
1846 driver.poll_once(&mut dispatch).expect("poll_once succeeds");
1847 if driver.gso_submission_count() > 0 {
1848 break;
1849 }
1850 }
1851 assert!(
1852 driver.gso_submission_count() > 0,
1853 "driver must submit at least one GSO send"
1854 );
1855
1856 let mut got = Vec::new();
1857 let mut recv_buf = [0_u8; MAX_WIRE_DATAGRAM];
1858 for _ in 0..4096 {
1859 loop {
1860 match a.recv(&mut recv_buf) {
1861 Ok(n) => got.push(recv_buf[..n].to_vec()),
1862 Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
1863 Err(e) => panic!("recv failed: {e}"),
1864 }
1865 }
1866 if got.len() >= datagram_count {
1867 break;
1868 }
1869 driver.poll_once(&mut dispatch).expect("poll_once succeeds");
1870 }
1871
1872 let mtu_cap = 65_507_usize / datagram_size;
1878 let max_chunk = mtu_cap.min(32).min(64);
1879 let expected_submissions = datagram_count.div_ceil(max_chunk);
1880 assert!(expected_submissions >= 2, "test should exercise chunking");
1881 assert!(
1882 driver.gso_submission_count() >= expected_submissions,
1883 "driver must chunk large GSO batches into multiple submissions \
1884 ({} distinct-fate datagrams, expected >= {expected_submissions} sends, \
1885 got {})",
1886 datagram_count,
1887 driver.gso_submission_count()
1888 );
1889 assert_eq!(
1890 got.len(),
1891 datagram_count,
1892 "must receive all datagrams from a large GSO batch"
1893 );
1894
1895 unsafe {
1897 libc::close(tun_rd);
1898 libc::close(tun_wr);
1899 }
1900 drop(driver);
1901 std::thread::sleep(Duration::from_millis(200));
1902 }
1903
1904 #[test]
1905 fn uring_in_flight_send_table_reuses_slots_after_completions() {
1906 let _guard = URING_SERIAL
1907 .lock()
1908 .unwrap_or_else(std::sync::PoisonError::into_inner);
1909 let a = UdpSocket::bind("127.0.0.1:0").expect("bind sender");
1910 let b = UdpSocket::bind("127.0.0.1:0").expect("bind uring peer");
1911 a.connect(b.local_addr().expect("sender local addr"))
1912 .expect("connect sender");
1913 b.connect(a.local_addr().expect("uring peer local addr"))
1914 .expect("connect uring peer");
1915 a.set_nonblocking(true).expect("set sender nonblocking");
1916
1917 let (tun_rd, tun_wr) = make_pipe().expect("make tun placeholder pipe");
1918 let mut dispatch = EchoDispatch::new();
1919 let mut driver = match UringDriver::new(b.as_raw_fd(), tun_rd) {
1920 Ok(driver) => driver,
1921 Err(_) => {
1922 eprintln!(
1923 "SKIP uring_in_flight_send_table_reuses_slots_after_completions: \
1924 io_uring ring or buffer registration unavailable (RLIMIT_MEMLOCK)"
1925 );
1926 unsafe {
1928 libc::close(tun_rd);
1929 libc::close(tun_wr);
1930 }
1931 return;
1932 }
1933 };
1934
1935 let total = SEND_SLOTS * 3;
1942 const WINDOW: usize = 64;
1943 let mut recv_buf = [0_u8; MAX_WIRE_DATAGRAM];
1944 let mut received = 0_usize;
1945 let mut next_to_send = 0_usize;
1946 let mut stall = 0_usize;
1947 for _ in 0..8000 {
1948 while next_to_send < total && next_to_send < received + WINDOW {
1949 let payload = format!("slot-reuse-{next_to_send}").into_bytes();
1950 a.send(&payload).expect("send test datagram");
1951 next_to_send += 1;
1952 }
1953
1954 driver.poll_once(&mut dispatch).expect("poll_once succeeds");
1955 let before = received;
1956 loop {
1957 match a.recv(&mut recv_buf) {
1958 Ok(_) => received += 1,
1959 Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
1960 Err(e) => panic!("recv failed: {e}"),
1961 }
1962 }
1963 if received >= total {
1964 break;
1965 }
1966 if received == before {
1970 stall += 1;
1971 if stall >= 200 {
1972 break;
1973 }
1974 } else {
1975 stall = 0;
1976 }
1977 }
1978
1979 assert!(
1987 received > SEND_SLOTS,
1988 "expected more than SEND_SLOTS={SEND_SLOTS} datagrams to round-trip \
1989 (proving the send table was reused), got {received}"
1990 );
1991
1992 for _ in 0..1000 {
1999 if driver.in_flight_used_count() == 0 {
2000 break;
2001 }
2002 driver
2003 .poll_once(&mut dispatch)
2004 .expect("poll_once drains pending send completions");
2005 }
2006 assert_eq!(
2007 driver.in_flight_used_count(),
2008 0,
2009 "all in-flight slots must be released after completions"
2010 );
2011
2012 assert!(
2015 driver.dropped_sends() <= u64::try_from(total).expect("total fits u64"),
2016 "dropped_sends must not exceed total sent"
2017 );
2018
2019 unsafe {
2021 libc::close(tun_rd);
2022 libc::close(tun_wr);
2023 }
2024 drop(driver);
2025 std::thread::sleep(Duration::from_millis(200));
2026 }
2027}