Skip to main content

subetha_cxc/
dgram.rs

1//! `dgram`: a pluggable datagram backend for the RLC transport.
2//!
3//! The transport's wire I/O normally runs on a plain `std::net::UdpSocket`.
4//! `DgramSock` wraps that socket and, on Linux, transparently upgrades it to
5//! an **io_uring**-backed datagram socket - the kernel async submission /
6//! completion ring drives batched `recvmsg` / `sendmsg`, cutting the
7//! syscall-per-packet overhead of the hot loop. The backend is:
8//!
9//! - **auto-detected** at runtime: `DgramSock::wrap` tries the io_uring
10//!   backend and silently falls back to the plain `UdpSocket` when io_uring
11//!   is unavailable (old kernel, container, or any non-Linux target), so the
12//!   transport works everywhere and uses the ring only where it exists; and
13//! - **overridable** via the `SUBETHA_DGRAM` env var (`iouring` forces the
14//!   ring - and warns if it is unavailable rather than silently degrading -
15//!   `udp` forces the plain socket), so a test can prove the ring path is
16//!   actually exercised; and
17//! - **link-speed-gated** for the NIC-bypass Wire backend (AF_XDP on Linux,
18//!   netmap on FreeBSD, BPF on macOS): kernel-bypass only beats plain UDP once the link is
19//!   fast enough that the per-packet syscall path - not the link - is the
20//!   bottleneck, so the auto path engages Wire only when the configured
21//!   `SUBETHA_WIRE_IFNAME`'s detected link speed is at or above the gate
22//!   (`SUBETHA_WIRE_MIN_GBPS`, default 10 Gbit/s) and falls back to io_uring
23//!   / UDP below it. `SUBETHA_DGRAM=wire` forces Wire regardless (warned
24//!   when the link is below the gate, since it is net-negative there).
25//!
26//! The io_uring backend preserves every semantic the transport relies on:
27//! `(n, src_addr)` from `recv_from`, the `SO_TIMESTAMPNS` kernel arrival
28//! timestamp from `recv_with_kts`, non-blocking `WouldBlock`, and per-peer
29//! `send_to`.
30
31use std::io;
32use std::net::{SocketAddr, UdpSocket};
33
34/// Which datagram backend a [`DgramSock`] resolved to.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum DgramBackend {
37    /// Plain `std::net::UdpSocket` (the universal fallback).
38    Udp,
39    /// io_uring-backed batched `recvmsg`/`sendmsg` (Linux, when available).
40    IoUring,
41    /// NIC-bypass via a `WireSocket` (AF_XDP on Linux, netmap on FreeBSD, BPF on macOS):
42    /// the transport's datagrams ride raw Ethernet+IPv4+UDP frames, bypassing
43    /// the kernel networking stack. Engaged only above the link-speed gate.
44    Wire,
45    /// Inbound stream is an in-process [`DemuxQueue`] fed by a demux reader;
46    /// sends forward to a shared real socket. Used by the unified Sens-O-Matic
47    /// endpoint to fan one socket out to its per-code receivers.
48    Demux,
49}
50
51// The backends are inherently different sizes (a plain UdpSocket vs an io_uring
52// ring). There is exactly ONE Inner per DgramSock, alive for the socket's whole
53// life - never a collection - so the per-variant size gap the lint warns about
54// (wasted slots in a Vec) does not apply; boxing would only add a hot-path deref.
55#[allow(clippy::large_enum_variant)]
56enum Inner {
57    Udp(UdpSocket),
58    #[cfg(target_os = "linux")]
59    IoUring(linux_iou::IoUringDgram),
60    #[cfg(all(feature = "wire-locale", any(target_os = "linux", target_os = "freebsd", target_os = "macos")))]
61    Wire(wire_backend::WireDgram),
62    /// Inbound side is an in-process queue fed by a demux reader; outbound side
63    /// forwards to a shared real socket. See [`DemuxDgram`].
64    Demux(DemuxDgram),
65}
66
67/// Inbound datagram queue for a [`DgramSock::demux`] socket. The unified
68/// endpoint's demux reader classifies each datagram by its first wire byte and
69/// pushes `(bytes, from, kernel_ts)` onto the matching code's queue; that
70/// code's receiver pops it through the normal `recv_*` surface, unmodified.
71pub type DemuxQueue =
72    std::sync::Arc<std::sync::Mutex<std::collections::VecDeque<(Vec<u8>, SocketAddr, Option<i128>)>>>;
73
74/// A fresh, empty [`DemuxQueue`].
75pub fn new_demux_queue() -> DemuxQueue {
76    std::sync::Arc::new(std::sync::Mutex::new(std::collections::VecDeque::new()))
77}
78
79/// Datagram backend whose inbound stream is an in-process [`DemuxQueue`] and
80/// whose sends forward to a shared real socket. Lets one real socket fan a
81/// demultiplexed inbound stream out to several independent per-code receivers
82/// that each believe they own a socket, while they all transmit through the one
83/// real socket.
84struct DemuxDgram {
85    real: std::sync::Arc<UdpSocket>,
86    queue: DemuxQueue,
87    /// Peer set by `connect`; the connected `send` carries it, since the shared
88    /// real socket is not itself connected to one peer.
89    peer: std::sync::Mutex<Option<SocketAddr>>,
90    /// Optional shared counter of datagrams sent through this socket, the
91    /// unified endpoint's raw-channel-loss numerator (sent vs received).
92    sent: Option<std::sync::Arc<std::sync::atomic::AtomicU64>>,
93    /// Queue pops attempted and pops that yielded a datagram.
94    pop_attempts: std::sync::atomic::AtomicU64,
95    pop_yields: std::sync::atomic::AtomicU64,
96}
97
98impl DemuxDgram {
99    fn recv_with_kts(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr, Option<i128>)> {
100        self.pop_attempts.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
101        match self.queue.lock().unwrap().pop_front() {
102            Some((data, from, kts)) => {
103                self.pop_yields.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
104                let n = data.len().min(buf.len());
105                buf[..n].copy_from_slice(&data[..n]);
106                Ok((n, from, kts))
107            }
108            // An empty queue reads as a non-blocking socket would with no
109            // datagram ready, so the receivers' idle-backoff loop is unchanged.
110            None => Err(io::Error::new(io::ErrorKind::WouldBlock, "demux queue empty")),
111        }
112    }
113
114    fn connect(&self, addr: SocketAddr) {
115        *self.peer.lock().unwrap() = Some(addr);
116    }
117
118    fn count_fwd(&self, buf: &[u8]) {
119        // Count only forward data/repair (RS data 1, RLC data 10 / repair 11),
120        // exactly what the receiver tallies, so the raw-loss ratio is unbiased
121        // by control frames (heartbeats, probes) the receiver does not count.
122        if let Some(c) = &self.sent
123            && matches!(buf.first(), Some(&(1 | 10 | 11)))
124        {
125            c.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
126        }
127    }
128
129    fn send_to(&self, buf: &[u8], addr: SocketAddr) -> io::Result<usize> {
130        self.count_fwd(buf);
131        self.real.send_to(buf, addr)
132    }
133
134    fn send(&self, buf: &[u8]) -> io::Result<usize> {
135        self.count_fwd(buf);
136        match *self.peer.lock().unwrap() {
137            Some(p) => self.real.send_to(buf, p),
138            None => Err(io::Error::new(io::ErrorKind::NotConnected, "demux send before connect")),
139        }
140    }
141}
142
143/// A datagram socket whose backend is chosen at runtime: io_uring where
144/// available, plain UDP otherwise. Same surface either way.
145pub struct DgramSock {
146    inner: Inner,
147}
148
149impl DgramSock {
150    /// Wrap a bound `UdpSocket`, auto-detecting the io_uring backend. Honors
151    /// `SUBETHA_DGRAM` (`iouring` / `udp`); otherwise prefers io_uring on
152    /// Linux and falls back to plain UDP when the ring cannot be created.
153    pub fn wrap(sock: UdpSocket) -> Self {
154        enable_rx_timestamp(&sock);
155        let forced = std::env::var("SUBETHA_DGRAM").ok();
156        if forced.as_deref() == Some("udp") {
157            return Self { inner: Inner::Udp(sock) };
158        }
159
160        // NIC-bypass backend (AF_XDP on Linux, netmap on FreeBSD, BPF on macOS): the
161        // transport's datagrams ride raw Ethernet+IPv4+UDP frames with the
162        // kernel stack bypassed. It only wins when the link is fast enough
163        // that the per-packet syscall path - not the link - is the
164        // bottleneck, so the AUTO path engages it ONLY above the link-speed
165        // gate (`SUBETHA_WIRE_MIN_GBPS`, default 10 Gbit/s) and otherwise
166        // falls through to io_uring / UDP. `SUBETHA_DGRAM=wire` forces it
167        // regardless (warned when below the gate). Either way it needs the
168        // `SUBETHA_WIRE_*` interface / address / peer-MAC config.
169        #[cfg(all(feature = "wire-locale", any(target_os = "linux", target_os = "freebsd", target_os = "macos")))]
170        {
171            let wire_if = std::env::var("SUBETHA_WIRE_IFNAME").ok();
172            let force_wire = forced.as_deref() == Some("wire");
173            let auto_wire = forced.is_none()
174                && wire_if.as_deref().map(wire_link_fast_enough).unwrap_or(false);
175            if force_wire || auto_wire {
176                let port = sock.local_addr().map(|a| a.port()).unwrap_or(0);
177                match wire_backend::WireDgram::from_env(port) {
178                    Ok(w) => {
179                        if force_wire
180                            && !wire_if.as_deref().map(wire_link_fast_enough).unwrap_or(false)
181                        {
182                            eprintln!(
183                                "SUBETHA_DGRAM=wire forced on a link below the {:.0} Gbit/s \
184                                 gate; the kernel-bypass path is net-negative below line rate",
185                                wire_min_link_bps() as f64 / 1e9
186                            );
187                        }
188                        return Self { inner: Inner::Wire(w) };
189                    }
190                    Err(e) => eprintln!(
191                        "Wire backend requested but unavailable ({e}); falling back"
192                    ),
193                }
194            }
195        }
196
197        #[cfg(target_os = "linux")]
198        {
199            let force_ring = forced.as_deref() == Some("iouring");
200            match linux_iou::IoUringDgram::new(sock) {
201                Ok(d) => Self { inner: Inner::IoUring(d) },
202                Err((sock, e)) => {
203                    if force_ring {
204                        eprintln!(
205                            "SUBETHA_DGRAM=iouring requested but the ring is unavailable \
206                             ({e}); using plain UDP"
207                        );
208                    }
209                    Self { inner: Inner::Udp(sock) }
210                }
211            }
212        }
213
214        #[cfg(not(target_os = "linux"))]
215        {
216            drop(forced);
217            Self { inner: Inner::Udp(sock) }
218        }
219    }
220
221    /// Build a demux-backed socket: inbound datagrams are popped from `queue`
222    /// (fed by a demux reader that classifies one real socket's datagrams by
223    /// first wire byte), outbound sends forward to `real`. The unified
224    /// Sens-O-Matic endpoint uses this to fan one socket out to its per-code
225    /// RLC and RS receivers without modifying either.
226    pub fn demux(real: std::sync::Arc<UdpSocket>, queue: DemuxQueue) -> Self {
227        Self::demux_inner(real, queue, None)
228    }
229
230    /// Like [`demux`](Self::demux) but tallies every datagram sent through it
231    /// into `sent` (the unified endpoint's raw-loss numerator).
232    pub fn demux_counted(
233        real: std::sync::Arc<UdpSocket>,
234        queue: DemuxQueue,
235        sent: std::sync::Arc<std::sync::atomic::AtomicU64>,
236    ) -> Self {
237        Self::demux_inner(real, queue, Some(sent))
238    }
239
240    fn demux_inner(
241        real: std::sync::Arc<UdpSocket>,
242        queue: DemuxQueue,
243        sent: Option<std::sync::Arc<std::sync::atomic::AtomicU64>>,
244    ) -> Self {
245        Self {
246            inner: Inner::Demux(DemuxDgram {
247                real,
248                queue,
249                peer: std::sync::Mutex::new(None),
250                sent,
251                pop_attempts: std::sync::atomic::AtomicU64::new(0),
252                pop_yields: std::sync::atomic::AtomicU64::new(0),
253            }),
254        }
255    }
256
257    /// Demux-backend queue probe: `(pop_attempts, pop_yields,
258    /// queue_ptr, queue_len)`, or `None` for a non-demux backend.
259    /// `queue_ptr` is the shared queue's `Arc` address, comparable
260    /// against the pushing reader's record of the same.
261    pub fn demux_probe(&self) -> Option<(u64, u64, u64, u64)> {
262        match &self.inner {
263            Inner::Demux(d) => Some((
264                d.pop_attempts.load(std::sync::atomic::Ordering::Relaxed),
265                d.pop_yields.load(std::sync::atomic::Ordering::Relaxed),
266                std::sync::Arc::as_ptr(&d.queue) as usize as u64,
267                d.queue.lock().unwrap().len() as u64,
268            )),
269            _ => None,
270        }
271    }
272
273    /// Wrap a bound `UdpSocket` as a plain-UDP `DgramSock` WITHOUT the io_uring
274    /// auto-upgrade. The Reed-Solomon transport drives the raw fd directly for
275    /// GRO / TTL / ECN / connected-send / Windows USO, so it needs the `Udp`
276    /// backend (reachable via [`as_udp`](Self::as_udp)); `wrap`'s io_uring
277    /// upgrade would hide the fd. Sets no sockopts of its own - that transport
278    /// manages its own recvmsg cmsgs and control-buffer sizing, so adding the
279    /// RX-timestamp cmsg here could overflow its control buffer.
280    pub fn from_udp(sock: UdpSocket) -> Self {
281        Self { inner: Inner::Udp(sock) }
282    }
283
284    /// The underlying `UdpSocket` when this is a plain-UDP backend (the only
285    /// backend with a directly-usable fd), else `None`. Lets a transport that
286    /// needs raw-fd socket features keep them on the standalone path and fall
287    /// back cleanly on the demux / io_uring / wire paths.
288    pub fn as_udp(&self) -> Option<&UdpSocket> {
289        match &self.inner {
290            Inner::Udp(s) => Some(s),
291            _ => None,
292        }
293    }
294
295    /// Connect the socket to `addr` so [`send`](Self::send) can omit it. Udp
296    /// connects the kernel socket; Demux records the peer for its forwarded
297    /// send. io_uring / wire are not used by the connected-send transport.
298    pub fn connect(&self, addr: SocketAddr) -> io::Result<()> {
299        match &self.inner {
300            Inner::Udp(s) => s.connect(addr),
301            Inner::Demux(d) => {
302                d.connect(addr);
303                Ok(())
304            }
305            #[allow(unreachable_patterns)]
306            _ => Err(io::Error::new(
307                io::ErrorKind::Unsupported,
308                "connect on a non-Udp/Demux backend",
309            )),
310        }
311    }
312
313    /// Send on the connected peer (see [`connect`](Self::connect)).
314    pub fn send(&self, buf: &[u8]) -> io::Result<usize> {
315        match &self.inner {
316            Inner::Udp(s) => s.send(buf),
317            Inner::Demux(d) => d.send(buf),
318            #[allow(unreachable_patterns)]
319            _ => Err(io::Error::new(
320                io::ErrorKind::Unsupported,
321                "send on a non-Udp/Demux backend",
322            )),
323        }
324    }
325
326    /// Receive on the connected socket (see [`connect`](Self::connect)). Udp
327    /// uses the kernel connected recv; Demux pops its demux queue.
328    pub fn recv(&self, buf: &mut [u8]) -> io::Result<usize> {
329        match &self.inner {
330            Inner::Udp(s) => s.recv(buf),
331            Inner::Demux(d) => d.recv_with_kts(buf).map(|(n, _, _)| n),
332            #[allow(unreachable_patterns)]
333            _ => Err(io::Error::new(
334                io::ErrorKind::Unsupported,
335                "recv on a non-Udp/Demux backend",
336            )),
337        }
338    }
339
340    /// Which backend was selected.
341    pub fn backend(&self) -> DgramBackend {
342        match &self.inner {
343            Inner::Udp(_) => DgramBackend::Udp,
344            #[cfg(target_os = "linux")]
345            Inner::IoUring(_) => DgramBackend::IoUring,
346            #[cfg(all(feature = "wire-locale", any(target_os = "linux", target_os = "freebsd", target_os = "macos")))]
347            Inner::Wire(_) => DgramBackend::Wire,
348            Inner::Demux(_) => DgramBackend::Demux,
349        }
350    }
351
352    pub fn send_to(&self, buf: &[u8], addr: SocketAddr) -> io::Result<usize> {
353        match &self.inner {
354            Inner::Udp(s) => s.send_to(buf, addr),
355            #[cfg(target_os = "linux")]
356            Inner::IoUring(d) => d.send_to(buf, addr),
357            #[cfg(all(feature = "wire-locale", any(target_os = "linux", target_os = "freebsd", target_os = "macos")))]
358            Inner::Wire(d) => d.send_to(buf, addr),
359            Inner::Demux(d) => d.send_to(buf, addr),
360        }
361    }
362
363    /// The underlying UDP socket fd, when the backend has one (Udp / io_uring).
364    /// The Wire (AF_XDP / netmap / BPF) backend has no kernel UDP fd and returns
365    /// `None`. Used by the GSO batch path, which does a direct `sendmsg`.
366    #[cfg(target_os = "linux")]
367    fn raw_fd(&self) -> Option<i32> {
368        use std::os::unix::io::AsRawFd;
369        match &self.inner {
370            Inner::Udp(s) => Some(s.as_raw_fd()),
371            Inner::IoUring(d) => Some(d.raw_fd()),
372            #[cfg(all(feature = "wire-locale", any(target_os = "linux", target_os = "freebsd", target_os = "macos")))]
373            Inner::Wire(_) => None,
374            Inner::Demux(_) => None,
375        }
376    }
377
378    /// Ship `batch` (an integer number of `seg_size`-byte datagrams concatenated)
379    /// to `addr` in ONE `sendmsg` via UDP GSO (`UDP_SEGMENT`) - the kernel slices
380    /// it into `batch.len() / seg_size` wire datagrams, replicating IP+UDP
381    /// headers. Collapses the per-datagram syscall + stack-traversal cost (~62x
382    /// fewer syscalls at MTU). Falls back to one `send_to` per segment on
383    /// backends without a UDP fd (Wire) or non-Linux. `batch.len()` MUST be a
384    /// multiple of `seg_size`, and `seg_size * n_segs` must fit a single IP
385    /// datagram (<= 65535) - the caller caps the batch.
386    pub fn send_gso(&self, batch: &[u8], seg_size: u16, addr: SocketAddr) -> io::Result<()> {
387        #[cfg(target_os = "linux")]
388        if let Some(fd) = self.raw_fd() {
389            return linux_gso_send(fd, batch, seg_size, addr);
390        }
391        let n = (seg_size as usize).max(1);
392        let mut off = 0;
393        while off < batch.len() {
394            let end = (off + n).min(batch.len());
395            self.send_to(&batch[off..end], addr)?;
396            off = end;
397        }
398        Ok(())
399    }
400
401    pub fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr)> {
402        match &self.inner {
403            Inner::Udp(s) => s.recv_from(buf),
404            #[cfg(target_os = "linux")]
405            Inner::IoUring(d) => d.recv_with_kts(buf).map(|(n, a, _)| (n, a)),
406            #[cfg(all(feature = "wire-locale", any(target_os = "linux", target_os = "freebsd", target_os = "macos")))]
407            Inner::Wire(d) => d.recv_with_kts(buf).map(|(n, a, _)| (n, a)),
408            Inner::Demux(d) => d.recv_with_kts(buf).map(|(n, a, _)| (n, a)),
409        }
410    }
411
412    /// Receive one datagram with the kernel arrival timestamp when available.
413    pub fn recv_with_kts(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr, Option<i128>)> {
414        match &self.inner {
415            Inner::Udp(s) => udp_recv_with_kts(s, buf),
416            #[cfg(target_os = "linux")]
417            Inner::IoUring(d) => d.recv_with_kts(buf),
418            #[cfg(all(feature = "wire-locale", any(target_os = "linux", target_os = "freebsd", target_os = "macos")))]
419            Inner::Wire(d) => d.recv_with_kts(buf),
420            Inner::Demux(d) => d.recv_with_kts(buf),
421        }
422    }
423
424    pub fn local_addr(&self) -> io::Result<SocketAddr> {
425        match &self.inner {
426            Inner::Udp(s) => s.local_addr(),
427            #[cfg(target_os = "linux")]
428            Inner::IoUring(d) => d.local_addr(),
429            #[cfg(all(feature = "wire-locale", any(target_os = "linux", target_os = "freebsd", target_os = "macos")))]
430            Inner::Wire(d) => d.local_addr(),
431            Inner::Demux(d) => d.real.local_addr(),
432        }
433    }
434
435    pub fn set_nonblocking(&self, nb: bool) -> io::Result<()> {
436        match &self.inner {
437            Inner::Udp(s) => s.set_nonblocking(nb),
438            #[cfg(target_os = "linux")]
439            Inner::IoUring(d) => d.set_nonblocking(nb),
440            #[cfg(all(feature = "wire-locale", any(target_os = "linux", target_os = "freebsd", target_os = "macos")))]
441            Inner::Wire(d) => d.set_nonblocking(nb),
442            // The demux queue's pop is inherently non-blocking (empty -> WouldBlock),
443            // so blocking mode is a no-op; the per-code receivers run non-blocking.
444            Inner::Demux(_) => Ok(()),
445        }
446    }
447}
448
449// ---------------------------------------------------------------------
450// Shared UDP datagram helpers (also the universal fallback path).
451// ---------------------------------------------------------------------
452
453/// Best-effort kernel RX timestamps (`SO_TIMESTAMPNS`).
454#[cfg(target_os = "linux")]
455fn enable_rx_timestamp(sock: &UdpSocket) {
456    use std::os::fd::AsRawFd;
457    let on: libc::c_int = 1;
458    unsafe {
459        libc::setsockopt(
460            sock.as_raw_fd(),
461            libc::SOL_SOCKET,
462            libc::SO_TIMESTAMPNS,
463            &on as *const libc::c_int as *const libc::c_void,
464            std::mem::size_of::<libc::c_int>() as libc::socklen_t,
465        );
466    }
467}
468
469#[cfg(not(target_os = "linux"))]
470fn enable_rx_timestamp(_sock: &UdpSocket) {}
471
472/// `recvmsg`-based receive that extracts the kernel arrival timestamp.
473#[cfg(target_os = "linux")]
474pub(crate) fn udp_recv_with_kts(sock: &UdpSocket, buf: &mut [u8]) -> io::Result<(usize, SocketAddr, Option<i128>)> {
475    use std::os::fd::AsRawFd;
476    let mut iov = libc::iovec {
477        iov_base: buf.as_mut_ptr() as *mut libc::c_void,
478        iov_len: buf.len(),
479    };
480    let mut name: libc::sockaddr_storage = unsafe { std::mem::zeroed() };
481    let mut control = [0u8; 64];
482    let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
483    msg.msg_name = &mut name as *mut _ as *mut libc::c_void;
484    msg.msg_namelen = std::mem::size_of::<libc::sockaddr_storage>() as libc::socklen_t;
485    msg.msg_iov = &mut iov;
486    msg.msg_iovlen = 1;
487    msg.msg_control = control.as_mut_ptr() as *mut libc::c_void;
488    msg.msg_controllen = control.len();
489    let n = unsafe { libc::recvmsg(sock.as_raw_fd(), &mut msg, 0) };
490    if n < 0 {
491        return Err(io::Error::last_os_error());
492    }
493    let kts = parse_timestamp(&msg);
494    let from = unsafe { sockaddr_to_socketaddr(&name) }
495        .ok_or_else(|| io::Error::other("non-IP source"))?;
496    Ok((n as usize, from, kts))
497}
498
499#[cfg(not(target_os = "linux"))]
500pub(crate) fn udp_recv_with_kts(sock: &UdpSocket, buf: &mut [u8]) -> io::Result<(usize, SocketAddr, Option<i128>)> {
501    let (n, from) = sock.recv_from(buf)?;
502    Ok((n, from, None))
503}
504
505#[cfg(target_os = "linux")]
506fn parse_timestamp(msg: &libc::msghdr) -> Option<i128> {
507    unsafe {
508        let mut cmsg = libc::CMSG_FIRSTHDR(msg);
509        while !cmsg.is_null() {
510            if (*cmsg).cmsg_level == libc::SOL_SOCKET && (*cmsg).cmsg_type == libc::SCM_TIMESTAMPNS {
511                let ts = (libc::CMSG_DATA(cmsg) as *const libc::timespec).read_unaligned();
512                return Some(ts.tv_sec as i128 * 1_000_000_000 + ts.tv_nsec as i128);
513            }
514            cmsg = libc::CMSG_NXTHDR(msg, cmsg);
515        }
516    }
517    None
518}
519
520#[cfg(target_os = "linux")]
521unsafe fn sockaddr_to_socketaddr(name: &libc::sockaddr_storage) -> Option<SocketAddr> {
522    use std::net::{Ipv4Addr, Ipv6Addr};
523    match name.ss_family as libc::c_int {
524        libc::AF_INET => {
525            let sin = unsafe { &*(name as *const libc::sockaddr_storage as *const libc::sockaddr_in) };
526            let ip = Ipv4Addr::from(sin.sin_addr.s_addr.to_ne_bytes());
527            Some(SocketAddr::new(ip.into(), u16::from_be(sin.sin_port)))
528        }
529        libc::AF_INET6 => {
530            let sin6 = unsafe { &*(name as *const libc::sockaddr_storage as *const libc::sockaddr_in6) };
531            let ip = Ipv6Addr::from(sin6.sin6_addr.s6_addr);
532            Some(SocketAddr::new(ip.into(), u16::from_be(sin6.sin6_port)))
533        }
534        _ => None,
535    }
536}
537
538/// Build a kernel `sockaddr_storage` (+ length) from a [`SocketAddr`] for
539/// `sendmsg`.
540#[cfg(target_os = "linux")]
541fn socketaddr_to_sockaddr(addr: SocketAddr) -> (libc::sockaddr_storage, libc::socklen_t) {
542    let mut storage: libc::sockaddr_storage = unsafe { std::mem::zeroed() };
543    match addr {
544        SocketAddr::V4(a) => {
545            let sin = unsafe { &mut *(&mut storage as *mut _ as *mut libc::sockaddr_in) };
546            sin.sin_family = libc::AF_INET as libc::sa_family_t;
547            sin.sin_port = a.port().to_be();
548            sin.sin_addr.s_addr = u32::from_ne_bytes(a.ip().octets());
549            (storage, std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t)
550        }
551        SocketAddr::V6(a) => {
552            let sin6 = unsafe { &mut *(&mut storage as *mut _ as *mut libc::sockaddr_in6) };
553            sin6.sin6_family = libc::AF_INET6 as libc::sa_family_t;
554            sin6.sin6_port = a.port().to_be();
555            sin6.sin6_addr.s6_addr = a.ip().octets();
556            (storage, std::mem::size_of::<libc::sockaddr_in6>() as libc::socklen_t)
557        }
558    }
559}
560
561/// One `sendmsg` shipping `batch` (k * `seg_size` bytes) to `addr` as k wire
562/// datagrams of `seg_size` via a `UDP_SEGMENT` control message; the kernel
563/// (or the NIC, with `tx-udp-segmentation`) slices and replicates the headers.
564#[cfg(target_os = "linux")]
565fn linux_gso_send(fd: i32, batch: &[u8], seg_size: u16, addr: SocketAddr) -> io::Result<()> {
566    /// `UDP_SEGMENT` cmsg type; `SOL_UDP` = 17.
567    const UDP_SEGMENT: libc::c_int = 103;
568    const SOL_UDP: libc::c_int = 17;
569    let (storage, addrlen) = socketaddr_to_sockaddr(addr);
570    let mut iov = libc::iovec {
571        iov_base: batch.as_ptr() as *mut libc::c_void,
572        iov_len: batch.len(),
573    };
574    let cmsg_space = unsafe { libc::CMSG_SPACE(2) } as usize;
575    let mut cbuf = vec![0u8; cmsg_space];
576    let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
577    msg.msg_name = &storage as *const _ as *mut libc::c_void;
578    msg.msg_namelen = addrlen;
579    msg.msg_iov = &mut iov;
580    msg.msg_iovlen = 1;
581    msg.msg_control = cbuf.as_mut_ptr() as *mut libc::c_void;
582    msg.msg_controllen = cmsg_space as _;
583    // SAFETY: msg + storage + iov + cbuf all outlive the sendmsg call; the
584    // cmsg is sized by CMSG_SPACE(2) for a single u16 UDP_SEGMENT value.
585    unsafe {
586        let c = libc::CMSG_FIRSTHDR(&msg);
587        (*c).cmsg_level = SOL_UDP;
588        (*c).cmsg_type = UDP_SEGMENT;
589        (*c).cmsg_len = libc::CMSG_LEN(2) as _;
590        std::ptr::copy_nonoverlapping(&seg_size as *const u16 as *const u8, libc::CMSG_DATA(c), 2);
591        loop {
592            let r = libc::sendmsg(fd, &msg, 0);
593            if r >= 0 {
594                return Ok(());
595            }
596            let e = io::Error::last_os_error();
597            if e.kind() == io::ErrorKind::Interrupted {
598                continue;
599            }
600            return Err(e);
601        }
602    }
603}
604
605// ---------------------------------------------------------------------
606// Linux io_uring datagram backend.
607// ---------------------------------------------------------------------
608
609#[cfg(target_os = "linux")]
610mod linux_iou {
611    use super::{parse_timestamp, sockaddr_to_socketaddr, socketaddr_to_sockaddr};
612    use std::cell::RefCell;
613    use std::collections::VecDeque;
614    use std::io;
615    use std::net::{SocketAddr, UdpSocket};
616    use std::os::fd::AsRawFd;
617
618    use io_uring::{opcode, types, IoUring};
619
620    const RECV_DEPTH: usize = 32;
621    const SEND_DEPTH: usize = 32;
622    const FRAME_CAP: usize = 2048;
623    const CONTROL_CAP: usize = 64;
624    const SEND_TAG: u64 = 1 << 32; // user_data >= SEND_TAG marks send completions
625
626    /// A self-referential `recvmsg` context: the `msghdr` points at the addr
627    /// / iov / control / buf fields of the same heap box, so the box must
628    /// never move while an SQE referencing it is in flight (it lives in the
629    /// `recv` Vec for the socket's lifetime).
630    struct RecvCtx {
631        addr: libc::sockaddr_storage,
632        iov: libc::iovec,
633        control: [u8; CONTROL_CAP],
634        msghdr: libc::msghdr,
635        buf: [u8; FRAME_CAP],
636    }
637
638    impl RecvCtx {
639        fn boxed() -> Box<Self> {
640            let mut b: Box<Self> = Box::new(unsafe { std::mem::zeroed() });
641            b.refresh();
642            b
643        }
644        fn refresh(&mut self) {
645            self.iov.iov_base = self.buf.as_mut_ptr() as *mut libc::c_void;
646            self.iov.iov_len = FRAME_CAP;
647            self.msghdr.msg_name = std::ptr::addr_of_mut!(self.addr) as *mut libc::c_void;
648            self.msghdr.msg_namelen = std::mem::size_of::<libc::sockaddr_storage>() as u32;
649            self.msghdr.msg_iov = std::ptr::addr_of_mut!(self.iov);
650            self.msghdr.msg_iovlen = 1;
651            self.msghdr.msg_control = self.control.as_mut_ptr() as *mut libc::c_void;
652            self.msghdr.msg_controllen = CONTROL_CAP;
653        }
654    }
655
656    struct SendCtx {
657        addr: libc::sockaddr_storage,
658        iov: libc::iovec,
659        msghdr: libc::msghdr,
660        buf: [u8; FRAME_CAP],
661    }
662
663    struct State {
664        ring: IoUring,
665        // Box per element is load-bearing, not redundant: each ctx's msghdr
666        // points at its own addr/iov/control fields, so every element needs a
667        // stable heap address independent of the Vec's buffer (clippy's
668        // vec_box lint assumes the boxing is unnecessary - it is not here).
669        #[allow(clippy::vec_box)]
670        recv: Vec<Box<RecvCtx>>,
671        #[allow(clippy::vec_box)]
672        send: Vec<Box<SendCtx>>,
673        ready: VecDeque<usize>,
674        ready_len: Vec<usize>,
675        free_send: Vec<usize>,
676    }
677
678    pub struct IoUringDgram {
679        sock: UdpSocket,
680        fd: i32,
681        st: RefCell<State>,
682    }
683
684    unsafe impl Send for IoUringDgram {}
685
686    impl IoUringDgram {
687        pub fn new(sock: UdpSocket) -> Result<Self, (UdpSocket, io::Error)> {
688            let entries = ((RECV_DEPTH + SEND_DEPTH) * 2).next_power_of_two() as u32;
689            let ring = match IoUring::new(entries) {
690                Ok(r) => r,
691                Err(e) => return Err((sock, e)),
692            };
693            let fd = sock.as_raw_fd();
694            let recv: Vec<Box<RecvCtx>> = (0..RECV_DEPTH).map(|_| RecvCtx::boxed()).collect();
695            let send: Vec<Box<SendCtx>> =
696                (0..SEND_DEPTH).map(|_| Box::new(unsafe { std::mem::zeroed() })).collect();
697            let st = State {
698                ring,
699                recv,
700                send,
701                ready: VecDeque::new(),
702                ready_len: vec![0usize; RECV_DEPTH],
703                free_send: (0..SEND_DEPTH).collect(),
704            };
705            let me = Self { sock, fd, st: RefCell::new(st) };
706            if let Err(e) = me.submit_all_recv() {
707                return Err((me.sock, e));
708            }
709            Ok(me)
710        }
711
712        pub fn local_addr(&self) -> io::Result<SocketAddr> {
713            self.sock.local_addr()
714        }
715
716        /// The underlying UDP socket fd - a normal socket the io_uring ring
717        /// submits ops against. A direct `sendmsg` on it (e.g. the GSO batch
718        /// path) is independent of the ring's recv SQEs.
719        pub fn raw_fd(&self) -> i32 {
720            self.fd
721        }
722
723        pub fn set_nonblocking(&self, nb: bool) -> io::Result<()> {
724            self.sock.set_nonblocking(nb)
725        }
726
727        fn submit_all_recv(&self) -> io::Result<()> {
728            let mut st = self.st.borrow_mut();
729            for i in 0..st.recv.len() {
730                self.push_recv(&mut st, i)?;
731            }
732            st.ring.submit()?;
733            Ok(())
734        }
735
736        fn push_recv(&self, st: &mut State, idx: usize) -> io::Result<()> {
737            st.recv[idx].refresh();
738            let msg: *mut libc::msghdr = std::ptr::addr_of_mut!(st.recv[idx].msghdr);
739            let e = opcode::RecvMsg::new(types::Fd(self.fd), msg)
740                .build()
741                .user_data(idx as u64);
742            // SAFETY: the msghdr + buffers live in the boxed ctx for the
743            // socket's lifetime; the ctx is not reused until this op completes.
744            unsafe {
745                st.ring
746                    .submission()
747                    .push(&e)
748                    .map_err(|_| io::Error::other("io_uring SQ full (recv)"))?;
749            }
750            Ok(())
751        }
752
753        fn reap(&self, st: &mut State) {
754            let mut completed: Vec<(u64, i32)> = Vec::new();
755            for cqe in st.ring.completion() {
756                completed.push((cqe.user_data(), cqe.result()));
757            }
758            for (ud, res) in completed {
759                if ud >= SEND_TAG {
760                    st.free_send.push((ud - SEND_TAG) as usize);
761                } else {
762                    let idx = ud as usize;
763                    if res >= 0 {
764                        st.ready_len[idx] = res as usize;
765                        st.ready.push_back(idx);
766                    } else {
767                        self.push_recv(st, idx).ok();
768                    }
769                }
770            }
771        }
772
773        pub fn recv_with_kts(&self, buf: &mut [u8]) -> io::Result<(usize, SocketAddr, Option<i128>)> {
774            let mut st = self.st.borrow_mut();
775            self.reap(&mut st);
776            if st.ready.is_empty() {
777                st.ring.submit()?;
778                self.reap(&mut st);
779                if st.ready.is_empty() {
780                    return Err(io::Error::from(io::ErrorKind::WouldBlock));
781                }
782            }
783            let idx = st.ready.pop_front().unwrap();
784            let n = st.ready_len[idx];
785            let copy = n.min(buf.len());
786            let from;
787            let kts;
788            {
789                let ctx = &st.recv[idx];
790                buf[..copy].copy_from_slice(&ctx.buf[..copy]);
791                from = unsafe { sockaddr_to_socketaddr(&ctx.addr) }
792                    .ok_or_else(|| io::Error::other("non-IP source"))?;
793                kts = parse_timestamp(&ctx.msghdr);
794            }
795            self.push_recv(&mut st, idx)?;
796            st.ring.submit()?;
797            Ok((copy, from, kts))
798        }
799
800        pub fn send_to(&self, data: &[u8], addr: SocketAddr) -> io::Result<usize> {
801            if data.len() > FRAME_CAP {
802                return Err(io::Error::other("datagram exceeds frame cap"));
803            }
804            let mut st = self.st.borrow_mut();
805            self.reap(&mut st);
806            if st.free_send.is_empty() {
807                st.ring.submit()?;
808                self.reap(&mut st);
809                if st.free_send.is_empty() {
810                    return self.sock.send_to(data, addr);
811                }
812            }
813            let idx = st.free_send.pop().unwrap();
814            let (sa, sa_len) = socketaddr_to_sockaddr(addr);
815            {
816                let ctx = &mut st.send[idx];
817                ctx.buf[..data.len()].copy_from_slice(data);
818                ctx.addr = sa;
819                ctx.iov.iov_base = ctx.buf.as_mut_ptr() as *mut libc::c_void;
820                ctx.iov.iov_len = data.len();
821                ctx.msghdr.msg_name = std::ptr::addr_of_mut!(ctx.addr) as *mut libc::c_void;
822                ctx.msghdr.msg_namelen = sa_len;
823                ctx.msghdr.msg_iov = std::ptr::addr_of_mut!(ctx.iov);
824                ctx.msghdr.msg_iovlen = 1;
825            }
826            let msg: *const libc::msghdr = std::ptr::addr_of!(st.send[idx].msghdr);
827            let e = opcode::SendMsg::new(types::Fd(self.fd), msg)
828                .build()
829                .user_data(SEND_TAG | idx as u64);
830            // SAFETY: the msghdr + buffers live in the boxed send ctx; the ctx
831            // is not reused until this op completes (it left free_send).
832            unsafe {
833                st.ring
834                    .submission()
835                    .push(&e)
836                    .map_err(|_| io::Error::other("io_uring SQ full (send)"))?;
837            }
838            st.ring.submit()?;
839            Ok(data.len())
840        }
841    }
842}
843
844// ---------------------------------------------------------------------
845// Wire (AF_XDP NIC-bypass) datagram backend.
846//
847// The RLC datagram rides a hand-built Ethernet+IPv4+UDP frame through a
848// `WireSocket`, bypassing the kernel networking stack. Point-to-point: the
849// single peer's MAC is configured (no general ARP needed for a sender <->
850// receiver link). Linux-only for now (AF_XDP); behind the wire-locale
851// feature. Config via SUBETHA_WIRE_{IFNAME,LOCAL_IP,LOCAL_MAC,PEER_MAC}.
852// ---------------------------------------------------------------------
853
854// ---------------------------------------------------------------------
855// Link-speed gate: the kernel-bypass Wire backend only beats plain UDP
856// once the link is fast enough that the per-packet syscall path - not the
857// link - is the bottleneck. The auto path consults the detected link
858// speed of the configured Wire interface against SUBETHA_WIRE_MIN_GBPS.
859// ---------------------------------------------------------------------
860
861/// Detected link speed (bits/sec) of `ifname`, or `None` when it cannot be
862/// determined (unknown speed, or a pure-software netmap port - `vale*` /
863/// pipe - with no physical link). Linux reads `/sys/class/net/<if>/speed`;
864/// FreeBSD reads the AF_LINK `if_data.ifi_baudrate`. Exposed so callers can
865/// see why the gate chose a backend.
866#[cfg(all(feature = "wire-locale", any(target_os = "linux", target_os = "freebsd", target_os = "macos")))]
867pub fn link_speed_bps(ifname: &str) -> Option<u64> {
868    // A netmap spec like "netmap:em0" gauges the underlying NIC; a VALE
869    // switch / pipe ("vale0:a") has no physical link to measure.
870    let phys = ifname.strip_prefix("netmap:").unwrap_or(ifname);
871    if phys.starts_with("vale") || phys.contains(':') || phys.contains('{') {
872        return None;
873    }
874    #[cfg(target_os = "linux")]
875    {
876        // /sys/class/net/<if>/speed: negotiated link speed in Mbit/s, or -1.
877        let mbps: i64 = std::fs::read_to_string(format!("/sys/class/net/{phys}/speed"))
878            .ok()?
879            .trim()
880            .parse()
881            .ok()?;
882        (mbps > 0).then_some(mbps as u64 * 1_000_000)
883    }
884    #[cfg(any(target_os = "freebsd", target_os = "macos"))]
885    {
886        link_speed_baudrate(phys)
887    }
888}
889
890/// FreeBSD / macOS link speed via the AF_LINK `if_data.ifi_baudrate` (the
891/// link's negotiated line rate in bits/sec), read from `getifaddrs`.
892#[cfg(all(feature = "wire-locale", any(target_os = "freebsd", target_os = "macos")))]
893fn link_speed_baudrate(ifname: &str) -> Option<u64> {
894    use std::ffi::CStr;
895    let mut ifap: *mut libc::ifaddrs = std::ptr::null_mut();
896    if unsafe { libc::getifaddrs(&mut ifap) } != 0 {
897        return None;
898    }
899    let mut speed = None;
900    let mut cur = ifap;
901    while !cur.is_null() {
902        // SAFETY: getifaddrs returned a valid NUL-terminated linked list; we
903        // only read each node's name / addr / data through live pointers and
904        // stop at the null terminator.
905        let ifa = unsafe { &*cur };
906        if !ifa.ifa_name.is_null() && !ifa.ifa_addr.is_null() && !ifa.ifa_data.is_null() {
907            let name = unsafe { CStr::from_ptr(ifa.ifa_name) }.to_string_lossy();
908            let family = unsafe { (*ifa.ifa_addr).sa_family } as i32;
909            if name == ifname && family == libc::AF_LINK {
910                let baud =
911                    unsafe { (*(ifa.ifa_data as *const libc::if_data)).ifi_baudrate };
912                if baud > 0 {
913                    // `ifi_baudrate` is u64 on FreeBSD but u32 on Darwin.
914                    #[cfg(target_os = "freebsd")]
915                    {
916                        speed = Some(baud);
917                    }
918                    #[cfg(target_os = "macos")]
919                    {
920                        speed = Some(u64::from(baud));
921                    }
922                }
923            }
924        }
925        cur = ifa.ifa_next;
926    }
927    unsafe { libc::freeifaddrs(ifap) };
928    speed
929}
930
931/// The link-speed gate threshold (bits/sec). Default 10 Gbit/s - the rough
932/// crossover where the per-packet syscall / copy path, not the link, is the
933/// bottleneck, so kernel-bypass starts to pay. `SUBETHA_WIRE_MIN_GBPS`
934/// overrides it; `0` disables the gate (always engage Wire when configured -
935/// e.g. for a software VALE switch, which has no physical link to gauge).
936#[cfg(all(feature = "wire-locale", any(target_os = "linux", target_os = "freebsd", target_os = "macos")))]
937fn wire_min_link_bps() -> u64 {
938    std::env::var("SUBETHA_WIRE_MIN_GBPS")
939        .ok()
940        .and_then(|s| s.trim().parse::<f64>().ok())
941        .map(|g| (g * 1e9) as u64)
942        .unwrap_or(10_000_000_000)
943}
944
945/// Whether `ifname`'s detected link is fast enough for the Wire backend to
946/// win. Unknown speed -> `false` (conservative: don't pay the bypass
947/// overhead on a link we cannot confirm is line-rate). Threshold `0` ->
948/// always `true`. Exposed so callers can introspect the gate decision.
949#[cfg(all(feature = "wire-locale", any(target_os = "linux", target_os = "freebsd", target_os = "macos")))]
950pub fn wire_gate_admits(ifname: &str) -> bool {
951    wire_link_fast_enough(ifname)
952}
953
954#[cfg(all(feature = "wire-locale", any(target_os = "linux", target_os = "freebsd", target_os = "macos")))]
955fn wire_link_fast_enough(ifname: &str) -> bool {
956    let threshold = wire_min_link_bps();
957    if threshold == 0 {
958        return true;
959    }
960    link_speed_bps(ifname).map(|bps| bps >= threshold).unwrap_or(false)
961}
962
963#[cfg(all(feature = "wire-locale", any(target_os = "linux", target_os = "freebsd", target_os = "macos")))]
964mod wire_backend {
965    use super::*;
966    use std::cell::RefCell;
967    use std::net::{IpAddr, Ipv4Addr};
968
969    use crate::locale_wire::WireSocket;
970
971    const ETH_HDR: usize = 14;
972    const IP_HDR: usize = 20;
973    const UDP_HDR: usize = 8;
974    const HDRS: usize = ETH_HDR + IP_HDR + UDP_HDR; // 42
975
976    fn ipv4_csum(h: &[u8]) -> u16 {
977        let mut sum = 0u32;
978        let mut i = 0;
979        while i + 1 < h.len() {
980            sum += u16::from_be_bytes([h[i], h[i + 1]]) as u32;
981            i += 2;
982        }
983        while (sum >> 16) != 0 {
984            sum = (sum & 0xffff) + (sum >> 16);
985        }
986        !(sum as u16)
987    }
988
989    /// Build Eth+IPv4+UDP around `payload` into `out`. The args are the frame
990    /// fields themselves, so grouping them into a struct would not aid clarity.
991    #[allow(clippy::too_many_arguments)]
992    fn build_frame(
993        dst_mac: [u8; 6],
994        src_mac: [u8; 6],
995        src_ip: [u8; 4],
996        dst_ip: [u8; 4],
997        src_port: u16,
998        dst_port: u16,
999        payload: &[u8],
1000        out: &mut Vec<u8>,
1001    ) {
1002        out.clear();
1003        out.resize(HDRS + payload.len(), 0);
1004        out[0..6].copy_from_slice(&dst_mac);
1005        out[6..12].copy_from_slice(&src_mac);
1006        out[12..14].copy_from_slice(&0x0800u16.to_be_bytes());
1007        out[14] = 0x45;
1008        out[16..18].copy_from_slice(&((IP_HDR + UDP_HDR + payload.len()) as u16).to_be_bytes());
1009        out[22] = 64;
1010        out[23] = 17;
1011        out[26..30].copy_from_slice(&src_ip);
1012        out[30..34].copy_from_slice(&dst_ip);
1013        let c = ipv4_csum(&out[14..34]);
1014        out[24..26].copy_from_slice(&c.to_be_bytes());
1015        out[34..36].copy_from_slice(&src_port.to_be_bytes());
1016        out[36..38].copy_from_slice(&dst_port.to_be_bytes());
1017        out[38..40].copy_from_slice(&((UDP_HDR + payload.len()) as u16).to_be_bytes());
1018        out[42..].copy_from_slice(payload);
1019    }
1020
1021    /// Parse an Eth+IPv4+UDP frame -> (src_ip, src_port, dst_port, payload
1022    /// offset, payload len). None if it is not IPv4/UDP.
1023    fn parse_frame(f: &[u8]) -> Option<([u8; 4], u16, u16, usize, usize)> {
1024        if f.len() < HDRS || f[12..14] != 0x0800u16.to_be_bytes() {
1025            return None;
1026        }
1027        let ihl = (f[14] & 0x0f) as usize * 4;
1028        if ihl < IP_HDR || f[23] != 17 {
1029            return None;
1030        }
1031        let l4 = ETH_HDR + ihl;
1032        if f.len() < l4 + UDP_HDR {
1033            return None;
1034        }
1035        let src_ip = [f[26], f[27], f[28], f[29]];
1036        let src_port = u16::from_be_bytes([f[l4], f[l4 + 1]]);
1037        let dst_port = u16::from_be_bytes([f[l4 + 2], f[l4 + 3]]);
1038        let udp_len = u16::from_be_bytes([f[l4 + 4], f[l4 + 5]]) as usize;
1039        let pstart = l4 + UDP_HDR;
1040        let plen = udp_len.saturating_sub(UDP_HDR).min(f.len() - pstart);
1041        Some((src_ip, src_port, dst_port, pstart, plen))
1042    }
1043
1044    fn parse_mac(s: &str) -> Option<[u8; 6]> {
1045        let mut mac = [0u8; 6];
1046        let parts: Vec<&str> = s.split(':').collect();
1047        if parts.len() != 6 {
1048            return None;
1049        }
1050        for (i, p) in parts.iter().enumerate() {
1051            mac[i] = u8::from_str_radix(p, 16).ok()?;
1052        }
1053        Some(mac)
1054    }
1055
1056    fn parse_ipv4(s: &str) -> Option<[u8; 4]> {
1057        s.parse::<Ipv4Addr>().ok().map(|a| a.octets())
1058    }
1059
1060    /// A datagram socket whose wire is a `WireSocket` (AF_XDP on Linux,
1061    /// netmap on FreeBSD, BPF on macOS). Same surface as the UDP / io_uring backends; the
1062    /// kernel stack is bypassed. The interface in `SUBETHA_WIRE_IFNAME` is a
1063    /// NIC name on Linux and a netmap port spec (e.g. `vale0:a`,
1064    /// `netmap:em0`) on FreeBSD.
1065    pub struct WireDgram {
1066        wire: RefCell<WireSocket>,
1067        scratch: RefCell<Vec<u8>>,
1068        local_ip: [u8; 4],
1069        local_mac: [u8; 6],
1070        peer_mac: [u8; 6],
1071        local_port: u16,
1072    }
1073
1074    impl WireDgram {
1075        pub fn from_env(local_port: u16) -> io::Result<Self> {
1076            let getv = |k: &str| -> io::Result<String> {
1077                std::env::var(k).map_err(|_| io::Error::other(format!("{k} unset")))
1078            };
1079            let ifname = getv("SUBETHA_WIRE_IFNAME")?;
1080            let local_ip = parse_ipv4(&getv("SUBETHA_WIRE_LOCAL_IP")?)
1081                .ok_or_else(|| io::Error::other("bad SUBETHA_WIRE_LOCAL_IP"))?;
1082            let local_mac = parse_mac(&getv("SUBETHA_WIRE_LOCAL_MAC")?)
1083                .ok_or_else(|| io::Error::other("bad SUBETHA_WIRE_LOCAL_MAC"))?;
1084            let peer_mac = parse_mac(&getv("SUBETHA_WIRE_PEER_MAC")?)
1085                .ok_or_else(|| io::Error::other("bad SUBETHA_WIRE_PEER_MAC"))?;
1086            let wire = WireSocket::bind(&ifname, 0)?;
1087            Ok(Self {
1088                wire: RefCell::new(wire),
1089                scratch: RefCell::new(Vec::with_capacity(HDRS + 2048)),
1090                local_ip,
1091                local_mac,
1092                peer_mac,
1093                local_port,
1094            })
1095        }
1096
1097        pub fn local_addr(&self) -> io::Result<SocketAddr> {
1098            Ok(SocketAddr::new(IpAddr::V4(Ipv4Addr::from(self.local_ip)), self.local_port))
1099        }
1100
1101        pub fn set_nonblocking(&self, _nb: bool) -> io::Result<()> {
1102            // The wire is polled with a timeout; it is always non-blocking.
1103            Ok(())
1104        }
1105
1106        pub fn send_to(&self, buf: &[u8], addr: SocketAddr) -> io::Result<usize> {
1107            let dst_ip = match addr.ip() {
1108                IpAddr::V4(v) => v.octets(),
1109                IpAddr::V6(_) => return Err(io::Error::other("wire backend is IPv4-only")),
1110            };
1111            let mut scratch = self.scratch.borrow_mut();
1112            build_frame(
1113                self.peer_mac,
1114                self.local_mac,
1115                self.local_ip,
1116                dst_ip,
1117                self.local_port,
1118                addr.port(),
1119                buf,
1120                &mut scratch,
1121            );
1122            self.wire.borrow_mut().send_frame(&scratch)?;
1123            Ok(buf.len())
1124        }
1125
1126        pub fn recv_with_kts(
1127            &self,
1128            buf: &mut [u8],
1129        ) -> io::Result<(usize, SocketAddr, Option<i128>)> {
1130            let mut fb = [0u8; 2048];
1131            let mut wire = self.wire.borrow_mut();
1132            loop {
1133                // Non-blocking poll of the RX ring (0 ms timeout).
1134                let n = wire.recv_frame(&mut fb, 0)?;
1135                if n == 0 {
1136                    return Err(io::Error::from(io::ErrorKind::WouldBlock));
1137                }
1138                // The XSK sees all queue traffic; keep only IPv4/UDP frames
1139                // addressed to our port.
1140                if let Some((src_ip, src_port, dst_port, pstart, plen)) = parse_frame(&fb[..n])
1141                    && dst_port == self.local_port
1142                {
1143                    let copy = plen.min(buf.len());
1144                    buf[..copy].copy_from_slice(&fb[pstart..pstart + copy]);
1145                    let from = SocketAddr::new(IpAddr::V4(Ipv4Addr::from(src_ip)), src_port);
1146                    return Ok((copy, from, None));
1147                }
1148                // Not ours; drain the next frame.
1149            }
1150        }
1151    }
1152}