Skip to main content

yip_io/
lib.rs

1//! Kernel-bypass-ready packet I/O. M4 adds the io_uring backend (single ring
2//! servicing UDP + TUN/TAP), then AF_XDP. This is the only crate permitted to
3//! contain `unsafe`; every `unsafe` block must carry a `// SAFETY:` comment.
4
5pub mod addr;
6pub mod af_xdp;
7pub mod batch;
8pub mod bpf;
9pub mod epoll;
10pub(crate) mod gso;
11pub mod nonce;
12pub mod poll;
13pub mod spsc;
14pub mod tun_offload;
15pub mod uring;
16
17#[cfg(target_env = "musl")]
18pub(crate) type MsgFlags = libc::c_uint;
19#[cfg(not(target_env = "musl"))]
20pub(crate) type MsgFlags = libc::c_int;
21
22#[cfg(target_env = "musl")]
23pub(crate) type CmsgLen = libc::socklen_t;
24#[cfg(not(target_env = "musl"))]
25pub(crate) type CmsgLen = usize;
26
27pub use addr::{sockaddr_to_std, std_to_sockaddr};
28pub use bpf::{BpfFilterStatus, XdpRedirectFilter};
29
30#[inline]
31pub(crate) fn empty_msghdr() -> libc::msghdr {
32    // SAFETY: msghdr contains only pointers, lengths, and integer flags, which
33    // are valid when all bytes are zeroed.
34    unsafe { std::mem::zeroed() }
35}
36
37#[inline]
38pub(crate) fn empty_mmsghdr() -> libc::mmsghdr {
39    // SAFETY: mmsghdr contains msghdr and msg_len, which are valid when zeroed.
40    unsafe { std::mem::zeroed() }
41}
42
43/// Maximum number of datagrams in a single batched send/recv call.
44pub const MAX_DATAGRAM_BATCH: usize = 64;
45
46/// Maximum size (bytes) of a single wire datagram. 2 KiB is ample for any
47/// encapsulated MTU-1500 payload plus yip-wire framing overhead.
48pub const MAX_WIRE_DATAGRAM: usize = 2048;
49
50/// Selected I/O backend, in fallback-preference order.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum Backend {
53    /// Single io_uring ring for UDP + TUN/TAP (built first in M4).
54    IoUring,
55    /// AF_XDP zero-copy (bare-metal accelerant, later).
56    AfXdpZeroCopy,
57    /// AF_XDP copy mode (cloud-VM fallback).
58    AfXdpCopy,
59    /// Portable recvmmsg/sendmmsg fallback rung.
60    Mmsg,
61}
62
63use io_uring::{opcode, types, IoUring};
64use std::io;
65use std::net::UdpSocket;
66use std::os::fd::AsRawFd;
67
68/// Sends and receives wire datagrams via the selected backend. Implemented in M4.
69pub trait DataPlaneIo {
70    /// The backend actually selected at startup (after probing/fallback).
71    fn backend(&self) -> Backend;
72    /// Send one datagram.
73    fn send(&mut self, datagram: &[u8]) -> std::io::Result<()>;
74    /// Receive one datagram into `buf`, returning its length.
75    fn recv(&mut self, buf: &mut [u8]) -> std::io::Result<usize>;
76
77    /// Send up to `datagrams.len()` datagrams in one syscall where supported.
78    ///
79    /// Returns the number of datagrams actually sent. The default implementation
80    /// loops over [`DataPlaneIo::send`]; override for real batching (e.g. `sendmmsg`).
81    fn send_batch(&mut self, datagrams: &[&[u8]]) -> io::Result<usize> {
82        let mut count = 0usize;
83        for dg in datagrams {
84            self.send(dg)?;
85            count += 1;
86        }
87        Ok(count)
88    }
89
90    /// Receive up to `bufs.len()` datagrams in one syscall where supported.
91    ///
92    /// Fills `lens[i]` with the byte count of the i-th received datagram and
93    /// returns the number of datagrams received. The default implementation does
94    /// a single [`DataPlaneIo::recv`]; override for real batching (e.g. `recvmmsg`).
95    fn recv_batch(
96        &mut self,
97        bufs: &mut [[u8; MAX_WIRE_DATAGRAM]],
98        lens: &mut [usize],
99    ) -> io::Result<usize> {
100        if bufs.is_empty() || lens.is_empty() {
101            return Ok(0);
102        }
103        let n = self.recv(&mut bufs[0])?;
104        lens[0] = n;
105        Ok(1)
106    }
107}
108
109/// A `DataPlaneIo` backend that submits Read/Write ops on a connected UDP
110/// socket through an `io_uring` ring.
111pub struct IoUringIo {
112    ring: IoUring,
113    socket: UdpSocket,
114}
115
116impl IoUringIo {
117    /// Wrap a (connected) UDP socket with an io_uring ring.
118    pub fn new(socket: UdpSocket) -> io::Result<IoUringIo> {
119        let ring = IoUring::new(8)?;
120        Ok(IoUringIo { ring, socket })
121    }
122
123    fn submit_and_reap(&mut self, entry: &io_uring::squeue::Entry) -> io::Result<usize> {
124        // SAFETY: the buffer referenced by `entry` is owned by the caller and outlives
125        // this call — we submit and wait for completion before returning, so the kernel
126        // is done with the buffer by the time we hand control back.
127        unsafe {
128            self.ring
129                .submission()
130                .push(entry)
131                .map_err(|_| io::Error::other("submission queue full"))?;
132        }
133        self.ring.submit_and_wait(1)?;
134        let cqe = self
135            .ring
136            .completion()
137            .next()
138            .ok_or_else(|| io::Error::other("missing completion"))?;
139        let res = cqe.result();
140        if res < 0 {
141            return Err(io::Error::from_raw_os_error(-res));
142        }
143        Ok(usize::try_from(res).expect("non-negative result fits usize"))
144    }
145}
146
147impl DataPlaneIo for IoUringIo {
148    fn backend(&self) -> Backend {
149        Backend::IoUring
150    }
151
152    fn send(&mut self, datagram: &[u8]) -> io::Result<()> {
153        let len =
154            u32::try_from(datagram.len()).map_err(|_| io::Error::other("datagram too large"))?;
155        let entry = opcode::Write::new(types::Fd(self.socket.as_raw_fd()), datagram.as_ptr(), len)
156            .build()
157            .user_data(0);
158        self.submit_and_reap(&entry)?;
159        Ok(())
160    }
161
162    fn recv(&mut self, buf: &mut [u8]) -> io::Result<usize> {
163        let len = u32::try_from(buf.len()).map_err(|_| io::Error::other("buffer too large"))?;
164        let entry = opcode::Read::new(types::Fd(self.socket.as_raw_fd()), buf.as_mut_ptr(), len)
165            .build()
166            .user_data(1);
167        self.submit_and_reap(&entry)
168    }
169}
170
171/// A portable fallback backend over a plain (connected) UDP socket.
172pub struct PlainIo {
173    socket: UdpSocket,
174}
175
176impl PlainIo {
177    /// Wrap a connected UDP socket.
178    pub fn new(socket: UdpSocket) -> PlainIo {
179        PlainIo { socket }
180    }
181}
182
183impl DataPlaneIo for PlainIo {
184    fn backend(&self) -> Backend {
185        Backend::Mmsg
186    }
187
188    fn send(&mut self, datagram: &[u8]) -> io::Result<()> {
189        self.socket.send(datagram).map(|_| ())
190    }
191
192    fn recv(&mut self, buf: &mut [u8]) -> io::Result<usize> {
193        self.socket.recv(buf)
194    }
195
196    /// Send up to `datagrams.len()` datagrams in one `sendmmsg(2)` syscall.
197    ///
198    /// Returns the number of datagrams accepted by the kernel (≤ input length).
199    fn send_batch(&mut self, datagrams: &[&[u8]]) -> io::Result<usize> {
200        if datagrams.is_empty() {
201            return Ok(0);
202        }
203
204        // Build parallel arrays of iovec + mmsghdr.  We cap at MAX_DATAGRAM_BATCH
205        // to keep these stack arrays reasonably sized (64 × small structs).
206        let count = datagrams.len().min(MAX_DATAGRAM_BATCH);
207
208        // iovec[i] points into the caller-owned slice datagrams[i]; the slices
209        // must outlive the sendmmsg call, which they do — both arrays are on our
210        // stack and we block until sendmmsg returns.
211        let mut iovecs = [libc::iovec {
212            iov_base: std::ptr::null_mut(),
213            iov_len: 0,
214        }; MAX_DATAGRAM_BATCH];
215        let mut msgs = [empty_mmsghdr(); MAX_DATAGRAM_BATCH];
216
217        for (i, dg) in datagrams[..count].iter().enumerate() {
218            // SAFETY: We cast a shared slice to a *mut c_void as required by the
219            // iovec ABI.  sendmmsg does not write through iov_base for send
220            // operations; the kernel only reads from it.  The slice outlives the
221            // syscall because datagrams is borrowed for the duration of this fn.
222            iovecs[i].iov_base = dg.as_ptr().cast_mut().cast::<libc::c_void>();
223            iovecs[i].iov_len = dg.len();
224            msgs[i].msg_hdr.msg_iov = &raw mut iovecs[i];
225            msgs[i].msg_hdr.msg_iovlen = 1;
226        }
227
228        // SAFETY: `msgs[..count]` is a valid slice of initialised mmsghdr structs.
229        // Each msg_hdr.msg_iov points into `iovecs[..count]`, which are fully
230        // initialised above and live on this stack frame until sendmmsg returns.
231        // The socket fd is valid for the lifetime of PlainIo (owned UdpSocket).
232        // We pass MSG_NOSIGNAL so a peer disconnect raises an error rather than
233        // delivering SIGPIPE.
234        let ret = unsafe {
235            libc::sendmmsg(
236                self.socket.as_raw_fd(),
237                msgs.as_mut_ptr(),
238                u32::try_from(count).expect("count ≤ MAX_DATAGRAM_BATCH ≤ 64 fits u32"),
239                libc::MSG_NOSIGNAL as MsgFlags,
240            )
241        };
242
243        if ret < 0 {
244            return Err(io::Error::last_os_error());
245        }
246        Ok(usize::try_from(ret).expect("non-negative sendmmsg return fits usize"))
247    }
248
249    /// Receive up to `bufs.len()` datagrams in one `recvmmsg(2)` syscall.
250    ///
251    /// Writes per-datagram byte counts into `lens` and returns the number of
252    /// datagrams received (may be less than `bufs.len()`).
253    fn recv_batch(
254        &mut self,
255        bufs: &mut [[u8; MAX_WIRE_DATAGRAM]],
256        lens: &mut [usize],
257    ) -> io::Result<usize> {
258        if bufs.is_empty() {
259            return Ok(0);
260        }
261
262        let count = bufs.len().min(lens.len()).min(MAX_DATAGRAM_BATCH);
263
264        let mut iovecs = [libc::iovec {
265            iov_base: std::ptr::null_mut(),
266            iov_len: 0,
267        }; MAX_DATAGRAM_BATCH];
268        let mut msgs = [empty_mmsghdr(); MAX_DATAGRAM_BATCH];
269
270        for i in 0..count {
271            // SAFETY: bufs[i] is a mutable slice owned by the caller and lives
272            // for the duration of this function.  recvmmsg writes received bytes
273            // into iov_base; we ensure each iovec points to a distinct buffer
274            // element so there is no aliasing.
275            iovecs[i].iov_base = bufs[i].as_mut_ptr().cast::<libc::c_void>();
276            iovecs[i].iov_len = MAX_WIRE_DATAGRAM;
277            msgs[i].msg_hdr.msg_iov = &raw mut iovecs[i];
278            msgs[i].msg_hdr.msg_iovlen = 1;
279        }
280
281        // SAFETY: `msgs[..count]` is fully initialised above.  Each msg_iov
282        // points into a distinct element of `bufs`, so there is no aliasing.
283        // MSG_WAITFORONE causes recvmmsg to block until at least one datagram
284        // arrives and then return however many are immediately available, without
285        // waiting for the full `count` — matching the blocking semantics of recv
286        // while still harvesting bursts.  The socket fd is valid for the lifetime
287        // of PlainIo.  We pass a null timeout (block indefinitely).
288        let ret = unsafe {
289            libc::recvmmsg(
290                self.socket.as_raw_fd(),
291                msgs.as_mut_ptr(),
292                u32::try_from(count).expect("count ≤ MAX_DATAGRAM_BATCH ≤ 64 fits u32"),
293                libc::MSG_WAITFORONE as MsgFlags,
294                std::ptr::null_mut(),
295            )
296        };
297
298        if ret < 0 {
299            return Err(io::Error::last_os_error());
300        }
301        let received = usize::try_from(ret).expect("non-negative recvmmsg return fits usize");
302        for i in 0..received {
303            lens[i] = usize::try_from(msgs[i].msg_len).expect("msg_len fits usize");
304        }
305        Ok(received)
306    }
307}
308
309/// Set the OS send and receive socket buffer sizes to `bytes` bytes on `sock`.
310///
311/// Raises `SO_SNDBUF` and `SO_RCVBUF` via `setsockopt(2)`.  The kernel may
312/// silently clamp or double the requested value (Linux doubles it to account for
313/// bookkeeping overhead), so callers should not assume the returned kernel value
314/// matches `bytes` exactly — only that the call succeeded.
315///
316/// This function lives in yip-io (the only crate where `unsafe` is permitted)
317/// so that the yipd daemon can stay `#![forbid(unsafe_code)]`.
318pub fn set_socket_buffers(sock: &UdpSocket, bytes: usize) -> io::Result<()> {
319    let size = i32::try_from(bytes)
320        .map_err(|_| io::Error::other("buffer size too large for setsockopt"))?;
321    let fd = sock.as_raw_fd();
322
323    // SAFETY: `fd` is a valid file descriptor owned by `sock`, which outlives
324    // this call.  `size` is a correctly-typed `i32` matching `SO_SNDBUF`/
325    // `SO_RCVBUF`'s expected `optval` type.  We pass its address and exact
326    // `size_of::<i32>()` as `optlen`, which is what the kernel expects for
327    // these two socket options.  No aliasing or memory-safety issues arise
328    // because `size` is a stack-local that we only read (never write) in the
329    // unsafe block.
330    let ret_snd = unsafe {
331        libc::setsockopt(
332            fd,
333            libc::SOL_SOCKET,
334            libc::SO_SNDBUF,
335            std::ptr::addr_of!(size).cast::<libc::c_void>(),
336            libc::socklen_t::try_from(std::mem::size_of::<i32>())
337                .expect("size_of::<i32>() fits socklen_t"),
338        )
339    };
340    if ret_snd != 0 {
341        return Err(io::Error::last_os_error());
342    }
343
344    // SAFETY: same rationale as above; only the option name differs.
345    let ret_rcv = unsafe {
346        libc::setsockopt(
347            fd,
348            libc::SOL_SOCKET,
349            libc::SO_RCVBUF,
350            std::ptr::addr_of!(size).cast::<libc::c_void>(),
351            libc::socklen_t::try_from(std::mem::size_of::<i32>())
352                .expect("size_of::<i32>() fits socklen_t"),
353        )
354    };
355    if ret_rcv != 0 {
356        return Err(io::Error::last_os_error());
357    }
358
359    Ok(())
360}
361
362/// Pin the calling thread to `core` via `libc::sched_setaffinity`.
363///
364/// Returns `Ok(())` on success, or an `io::Error` if affinity could not be set.
365pub fn pin_current_thread(core: usize) -> io::Result<()> {
366    // SAFETY: `set` is a valid, fully-initialized `cpu_set_t` local; `CPU_SET`
367    // and `sched_setaffinity` only read/write within its bounds, and
368    // `size_of::<cpu_set_t>()` matches the buffer we pass.
369    let rc = unsafe {
370        let mut set: libc::cpu_set_t = std::mem::zeroed();
371        libc::CPU_ZERO(&mut set);
372        libc::CPU_SET(core, &mut set);
373        libc::sched_setaffinity(0, std::mem::size_of::<libc::cpu_set_t>(), &raw const set)
374    };
375    if rc != 0 {
376        Err(io::Error::last_os_error())
377    } else {
378        Ok(())
379    }
380}
381
382/// Choose the lowest-latency backend that initializes: io_uring if its ring
383/// builds on this kernel, else the portable plain-socket fallback.
384pub fn select_backend(socket: UdpSocket) -> Box<dyn DataPlaneIo> {
385    let clone = socket.try_clone().expect("clone udp socket");
386    match IoUringIo::new(clone) {
387        Ok(io) => Box::new(io),
388        Err(_) => Box::new(PlainIo::new(socket)),
389    }
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395
396    // io_uring setup and teardown is asynchronous in the kernel: the ring's locked-memory
397    // accounting is not fully released until some time after the fd is closed.  On kernels
398    // with a tight RLIMIT_MEMLOCK (this dev machine has 1 MiB), creating a third ring in
399    // the same process fails with ENOMEM even though the previous two have been dropped.
400    //
401    // Serialising every io_uring-bearing test through this mutex, combined with a short
402    // post-drop sleep, keeps the total concurrent ring count at one and gives the kernel
403    // time to reclaim its accounting between tests.
404    static URING_SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(());
405
406    #[test]
407    fn backends_are_ordered_by_preference() {
408        // io_uring is the first backend we build (M4); fallback rungs follow.
409        assert_ne!(Backend::IoUring, Backend::Mmsg);
410    }
411
412    #[test]
413    fn iouring_sends_and_receives_over_udp() {
414        let _guard = URING_SERIAL.lock().unwrap();
415        use std::net::UdpSocket;
416        let rx = UdpSocket::bind("127.0.0.1:0").unwrap();
417        let tx = UdpSocket::bind("127.0.0.1:0").unwrap();
418        tx.connect(rx.local_addr().unwrap()).unwrap();
419        rx.connect(tx.local_addr().unwrap()).unwrap();
420
421        // If ring creation fails (e.g. RLIMIT_MEMLOCK too low), skip gracefully.
422        let mut tx_io = match IoUringIo::new(tx) {
423            Ok(io) => io,
424            Err(_) => {
425                eprintln!(
426                    "SKIP iouring_sends_and_receives_over_udp: \
427                     io_uring ring unavailable (RLIMIT_MEMLOCK)"
428                );
429                return;
430            }
431        };
432        let mut rx_io = match IoUringIo::new(rx) {
433            Ok(io) => io,
434            Err(_) => {
435                eprintln!(
436                    "SKIP iouring_sends_and_receives_over_udp: \
437                     io_uring ring unavailable (RLIMIT_MEMLOCK)"
438                );
439                return;
440            }
441        };
442        assert_eq!(tx_io.backend(), Backend::IoUring);
443
444        tx_io.send(b"datagram via uring").unwrap();
445        let mut buf = [0u8; 64];
446        let n = rx_io.recv(&mut buf).unwrap();
447        assert_eq!(&buf[..n], b"datagram via uring");
448        // Explicit drop + sleep: give the kernel time to reclaim ring accounting before
449        // the next io_uring test (kernel cleanup is asynchronous).
450        drop(tx_io);
451        drop(rx_io);
452        std::thread::sleep(std::time::Duration::from_millis(200));
453    }
454
455    #[test]
456    fn plain_io_sends_and_receives() {
457        use std::net::UdpSocket;
458        let rx = UdpSocket::bind("127.0.0.1:0").unwrap();
459        let tx = UdpSocket::bind("127.0.0.1:0").unwrap();
460        tx.connect(rx.local_addr().unwrap()).unwrap();
461        rx.connect(tx.local_addr().unwrap()).unwrap();
462        let mut t = PlainIo::new(tx);
463        let mut r = PlainIo::new(rx);
464        assert_eq!(t.backend(), Backend::Mmsg);
465        t.send(b"plain path").unwrap();
466        let mut buf = [0u8; 32];
467        let n = r.recv(&mut buf).unwrap();
468        assert_eq!(&buf[..n], b"plain path");
469    }
470
471    #[test]
472    fn select_backend_prefers_io_uring_or_falls_back() {
473        let _guard = URING_SERIAL.lock().unwrap();
474        use std::net::UdpSocket;
475
476        // Probe: can we build a ring right now?  Use a throwaway connected socket
477        // so the probe mirrors exactly the conditions select_backend will face.
478        let probe_sock = UdpSocket::bind("127.0.0.1:0").unwrap();
479        let probe_peer = UdpSocket::bind("127.0.0.1:0").unwrap();
480        probe_sock
481            .connect(probe_peer.local_addr().unwrap())
482            .unwrap();
483        let uring_ok = IoUringIo::new(probe_sock).is_ok();
484        // Drop the probe ring; give the kernel a moment to reclaim accounting.
485        drop(probe_peer);
486        std::thread::sleep(std::time::Duration::from_millis(50));
487
488        let s = UdpSocket::bind("127.0.0.1:0").unwrap();
489        let io = select_backend(s);
490
491        // Contract: io_uring is preferred WHEN available, else falls back to Mmsg.
492        if uring_ok {
493            assert_eq!(io.backend(), Backend::IoUring);
494        } else {
495            assert_eq!(io.backend(), Backend::Mmsg);
496        }
497    }
498
499    #[test]
500    fn set_socket_buffers_succeeds_on_loopback() {
501        use std::net::UdpSocket;
502        let sock = UdpSocket::bind("127.0.0.1:0").unwrap();
503        // Request 4 MiB buffers; the kernel may clamp or double the value, so
504        // we only assert that the call itself succeeds.
505        let result = set_socket_buffers(&sock, 4 * 1024 * 1024);
506        assert!(
507            result.is_ok(),
508            "set_socket_buffers returned error: {:?}",
509            result
510        );
511    }
512
513    #[test]
514    fn plainio_send_and_recv_batch_roundtrip() {
515        use std::net::UdpSocket;
516        let rx = UdpSocket::bind("127.0.0.1:0").unwrap();
517        let tx = UdpSocket::bind("127.0.0.1:0").unwrap();
518        tx.connect(rx.local_addr().unwrap()).unwrap();
519        rx.connect(tx.local_addr().unwrap()).unwrap();
520        let mut tx_io = PlainIo::new(tx);
521        let mut rx_io = PlainIo::new(rx);
522
523        let a = b"first-datagram".as_slice();
524        let b = b"second".as_slice();
525        let sent = tx_io.send_batch(&[a, b]).unwrap();
526        assert_eq!(sent, 2);
527
528        let mut bufs = vec![[0u8; MAX_WIRE_DATAGRAM]; 8];
529        let mut lens = [0usize; 8];
530        // recvmmsg may return fewer than sent per call; loop until we have 2.
531        let mut got: Vec<Vec<u8>> = Vec::new();
532        while got.len() < 2 {
533            let n = rx_io.recv_batch(&mut bufs, &mut lens).unwrap();
534            for i in 0..n {
535                got.push(bufs[i][..lens[i]].to_vec());
536            }
537        }
538        assert!(got.contains(&a.to_vec()));
539        assert!(got.contains(&b.to_vec()));
540    }
541
542    #[test]
543    fn test_pin_current_thread_does_not_crash() {
544        // Core 0 is present on standard systems. Call pin_current_thread and verify
545        // it returns Ok or an OS error without crashing or corrupting memory.
546        let _ = pin_current_thread(0);
547    }
548
549    #[test]
550    fn test_plain_io_single_send_recv_and_defaults() {
551        use std::net::UdpSocket;
552        let rx = UdpSocket::bind("127.0.0.1:0").unwrap();
553        let tx = UdpSocket::bind("127.0.0.1:0").unwrap();
554        tx.connect(rx.local_addr().unwrap()).unwrap();
555        rx.connect(tx.local_addr().unwrap()).unwrap();
556        let mut tx_io = PlainIo::new(tx);
557        let mut rx_io = PlainIo::new(rx);
558
559        // Test single send and recv
560        tx_io.send(b"single-packet").unwrap();
561        let mut buf = [0u8; 64];
562        let n = rx_io.recv(&mut buf).unwrap();
563        assert_eq!(&buf[..n], b"single-packet");
564
565        // Test empty send_batch and recv_batch
566        assert_eq!(tx_io.send_batch(&[]).unwrap(), 0);
567        let mut empty_lens = [0usize; 1];
568        assert_eq!(rx_io.recv_batch(&mut [], &mut empty_lens).unwrap(), 0);
569
570        // Test Backend variants
571        assert_ne!(Backend::AfXdpZeroCopy, Backend::AfXdpCopy);
572
573        // Test default DataPlaneIo trait implementation
574        struct DefaultDataPlane {
575            payload: Vec<u8>,
576        }
577        impl DataPlaneIo for DefaultDataPlane {
578            fn backend(&self) -> Backend {
579                Backend::Mmsg
580            }
581            fn send(&mut self, datagram: &[u8]) -> std::io::Result<()> {
582                self.payload = datagram.to_vec();
583                Ok(())
584            }
585            fn recv(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
586                let n = self.payload.len().min(buf.len());
587                buf[..n].copy_from_slice(&self.payload[..n]);
588                Ok(n)
589            }
590        }
591
592        let mut ddp = DefaultDataPlane {
593            payload: Vec::new(),
594        };
595        let sent = ddp.send_batch(&[b"test-1", b"test-2"]).unwrap();
596        assert_eq!(sent, 2);
597        let mut bufs = vec![[0u8; MAX_WIRE_DATAGRAM]; 2];
598        let mut lens = [0usize; 2];
599        let recvd = ddp.recv_batch(&mut bufs, &mut lens).unwrap();
600        assert_eq!(recvd, 1);
601        assert_eq!(&bufs[0][..lens[0]], b"test-2");
602        assert_eq!(ddp.recv_batch(&mut [], &mut lens).unwrap(), 0);
603    }
604}