Skip to main content

snap_tun/
udp_batch.rs

1// Copyright 2026 Anapaya Systems
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//   http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Helpers for batched UDP receive and transmit operations used by SNAP tunnel I/O.
16//!
17//! `UdpBatchReceiver` batches receive-side work with a fixed compile-time batch size,
18//! while `UdpBatchSender` batches same-sized datagrams to the same destination so the
19//! underlying socket can take advantage of UDP segmentation offload when available.
20//!
21//! Both helpers are intended to be created once per socket and reused. They keep
22//! batch-sized scratch state alive so repeated calls can reuse packet buffers and
23//! socket state instead of rebuilding that state for every receive or flush cycle.
24
25use std::{
26    collections::VecDeque,
27    io,
28    io::IoSliceMut,
29    net::SocketAddr,
30    time::{Duration, Instant},
31};
32
33use ana_gotatun::packet::{Packet, PacketBufPool};
34use quinn_udp::{RecvMeta, Transmit, UdpSockRef, UdpSocketState};
35use tokio::{io::Interest, net::UdpSocket};
36
37const MAX_BATCH_SIZE: usize = 64;
38
39/// Maximum payload of a single UDP datagram, i.e. 64 KiB minus the IPv4 and UDP headers.
40///
41/// The kernel rejects larger sends with `EMSGSIZE`, so this also bounds how many equally
42/// sized datagrams may be coalesced into one segmentation-offloaded transmit. IPv6 without
43/// extension headers would allow 20 bytes more, which this bound conservatively gives up.
44const MAX_UDP_PAYLOAD_SIZE: usize = u16::MAX as usize - 20 - 8;
45
46/// How often a discarded datagram is reported at warning level.
47///
48/// A peer whose datagrams the socket keeps refusing would otherwise produce one warning per
49/// datagram. Matches what `quinn-udp` does for its own send errors.
50const DISCARD_LOG_INTERVAL: Duration = Duration::from_secs(60);
51
52/// The socket was not writable, so the queued datagrams were left in place.
53///
54/// This is the only way [`UdpBatchSender::try_flush_best_effort`] can fail. Datagrams the
55/// socket refuses are discarded by the flush itself, so back pressure is all that remains for
56/// the caller to handle: retry the flush once the socket signals writability again.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub struct SocketNotWritable;
59
60impl std::fmt::Display for SocketNotWritable {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        f.write_str("socket not writable, queued datagrams were left in place")
63    }
64}
65
66impl std::error::Error for SocketNotWritable {}
67
68/// Errors returned while receiving and processing a UDP batch.
69pub enum RecvBatchError<E> {
70    /// The socket operation itself failed.
71    Io(io::Error),
72    /// The caller-provided packet handler failed.
73    Handler(E),
74}
75
76/// Errors returned while queueing packets for batched transmission.
77#[derive(Debug)]
78pub enum QueuePacketError {
79    /// The sender queue is full and cannot accept another packet right now.
80    Full {
81        /// The unsent packet.
82        packet: Packet,
83        /// The original target address of the unsent packet.
84        target: SocketAddr,
85    },
86    /// The packet is larger than the configured sender scratch budget.
87    PacketTooLarge {
88        /// The oversized packet.
89        packet: Packet,
90        /// The original target address of the oversized packet.
91        target: SocketAddr,
92        /// The packet length in bytes.
93        packet_len: usize,
94        /// The configured maximum packet size.
95        max_packet_size: usize,
96    },
97}
98
99/// Creates the `quinn-udp` socket state for `socket`.
100///
101/// On Windows, `quinn-udp` probes for segmentation offload by setting the `UDP_SEND_MSG_SIZE`
102/// socket option and does not clear it again. Left in place, the option makes the kernel
103/// split every datagram sent without a per-transmit segment size into 1500 byte datagrams
104/// instead of refusing an oversized one. The option is cleared here so that segmentation
105/// only happens when a transmit asks for it.
106fn new_socket_state(socket: &UdpSocket) -> io::Result<UdpSocketState> {
107    let state = UdpSocketState::new(UdpSockRef::from(socket))?;
108    #[cfg(windows)]
109    if state.max_gso_segments() > 1 {
110        clear_socket_segment_size(socket)?;
111    }
112    Ok(state)
113}
114
115#[cfg(windows)]
116fn clear_socket_segment_size(socket: &UdpSocket) -> io::Result<()> {
117    use std::{mem, os::windows::io::AsRawSocket};
118
119    use windows_sys::Win32::Networking::WinSock;
120
121    let value: u32 = 0;
122    let rc = unsafe {
123        WinSock::setsockopt(
124            socket.as_raw_socket() as usize,
125            WinSock::IPPROTO_UDP,
126            WinSock::UDP_SEND_MSG_SIZE,
127            &value as *const _ as *const _,
128            mem::size_of_val(&value) as _,
129        )
130    };
131
132    if rc == 0 {
133        Ok(())
134    } else {
135        Err(io::Error::last_os_error())
136    }
137}
138
139/// UdpBatchReceiver wraps a standard UDP socket and provides batched receive operations.
140///
141/// It receives up to `BATCH_SIZE` UDP datagrams in one socket read cycle and is
142/// intended to be reused for as long as that socket is active. Reusing it keeps
143/// the receive slots checked out from the pool so repeated receive calls can stay
144/// on the fast path.
145///
146/// `BUFFER_SIZE` controls the size of packet buffers drawn from the provided pool.
147/// `BATCH_SIZE * BUFFER_SIZE` bytes of memory will be reserved for the receive buffer.
148pub struct UdpBatchReceiver<const BATCH_SIZE: usize, const BUFFER_SIZE: usize = 4096> {
149    state: UdpSocketState,
150    recv_meta: [RecvMeta; BATCH_SIZE],
151    recv_slots: [Packet; BATCH_SIZE],
152}
153
154impl<const BATCH_SIZE: usize, const BUFFER_SIZE: usize> UdpBatchReceiver<BATCH_SIZE, BUFFER_SIZE> {
155    /// Creates a receiver configured for a fixed compile-time batch size.
156    ///
157    /// The receiver keeps `BATCH_SIZE` packet buffers checked out from `pool` until
158    /// it is dropped, so callers should typically create one receiver per socket and
159    /// reuse it across receive calls.
160    pub fn new(socket: &UdpSocket, pool: &PacketBufPool<BUFFER_SIZE>) -> io::Result<Self> {
161        assert!(
162            BATCH_SIZE > 0,
163            "UdpBatchReceiver BATCH_SIZE must be greater than zero"
164        );
165        assert!(
166            BATCH_SIZE <= MAX_BATCH_SIZE,
167            "UdpBatchReceiver BATCH_SIZE must not exceed MAX_BATCH_SIZE"
168        );
169        let state = new_socket_state(socket)?;
170        let recv_slots = std::array::from_fn(|_| pool.get());
171        Ok(Self {
172            state,
173            recv_meta: std::array::from_fn(|_| RecvMeta::default()),
174            recv_slots,
175        })
176    }
177
178    /// Receives a batch of packets and invokes `handler` for each decoded datagram.
179    pub async fn recv_batch<E, F>(
180        &mut self,
181        socket: &UdpSocket,
182        pool: &PacketBufPool<BUFFER_SIZE>,
183        mut handler: F,
184    ) -> Result<(), RecvBatchError<E>>
185    where
186        F: FnMut(Packet, SocketAddr) -> Result<(), E>,
187    {
188        let received = loop {
189            socket.readable().await.map_err(RecvBatchError::Io)?;
190            match socket.try_io(Interest::READABLE, || self.try_recv(socket)) {
191                Ok(count) => break count,
192                Err(err) if err.kind() == io::ErrorKind::WouldBlock => continue,
193                Err(err) => return Err(RecvBatchError::Io(err)),
194            }
195        };
196
197        for index in 0..received {
198            self.handle_received(index, pool, &mut handler)
199                .map_err(RecvBatchError::Handler)?;
200        }
201
202        Ok(())
203    }
204
205    fn handle_received<E, F>(
206        &mut self,
207        index: usize,
208        pool: &PacketBufPool<BUFFER_SIZE>,
209        handler: &mut F,
210    ) -> Result<(), E>
211    where
212        F: FnMut(Packet, SocketAddr) -> Result<(), E>,
213    {
214        // `quinn_udp` can report one large receive buffer together with a stride
215        // when the kernel coalesced multiple datagrams. Split that back into
216        // logical packets here so downstream code keeps its usual one-packet-at-a-time view.
217        let meta = self.recv_meta[index];
218        if meta.len == 0 {
219            return Ok(());
220        }
221        let stride = if meta.stride == 0 {
222            meta.len
223        } else {
224            meta.stride
225        };
226        if stride >= meta.len {
227            // Hand ownership of the filled slot to the caller and immediately put a
228            // fresh buffer back into the slot so the next batch can reuse the same layout.
229            let mut packet = std::mem::replace(&mut self.recv_slots[index], pool.get());
230            packet.truncate(meta.len);
231            handler(packet, meta.addr)?;
232            return Ok(());
233        }
234
235        // Keep the receive slots permanently populated and carve a coalesced buffer
236        // into individually owned segments only when the kernel told us multiple
237        // datagrams were packed into one receive slot.
238        let packet = std::mem::replace(&mut self.recv_slots[index], pool.get());
239        for chunk in packet[..meta.len].chunks(stride) {
240            let mut segment = pool.get();
241            segment[..chunk.len()].copy_from_slice(chunk);
242            segment.truncate(chunk.len());
243            handler(segment, meta.addr)?;
244        }
245        Ok(())
246    }
247
248    fn try_recv(&mut self, socket: &UdpSocket) -> io::Result<usize> {
249        // Keep the receive slots alive across calls and hand them directly to the
250        // socket so a steady-state receive loop does not need to re-acquire buffers
251        // from the pool on every readiness notification.
252        let mut bufs_uninit: [std::mem::MaybeUninit<IoSliceMut<'_>>; BATCH_SIZE] =
253            std::array::from_fn(|_| std::mem::MaybeUninit::uninit());
254        for (index, packet) in self.recv_slots.iter_mut().enumerate() {
255            bufs_uninit[index].write(IoSliceMut::new(packet.as_mut()));
256        }
257        // SAFETY: Every element of `bufs_uninit` was written in the loop above, so
258        // all `BATCH_SIZE` slots are fully initialised. `MaybeUninit<T>` is guaranteed
259        // to have the same size and alignment as `T`, so reinterpreting the
260        // pointer as `*mut IoSliceMut<'_>` is sound. The resulting slice covers
261        // exactly the `BATCH_SIZE` elements that were initialised, and the backing
262        // array lives for the duration of this function.
263        let bufs = unsafe {
264            std::slice::from_raw_parts_mut(
265                bufs_uninit.as_mut_ptr() as *mut IoSliceMut<'_>,
266                BATCH_SIZE,
267            )
268        };
269        self.state
270            .recv(UdpSockRef::from(socket), bufs, &mut self.recv_meta)
271    }
272}
273
274/// Queues up to `BATCH_SIZE` packets for batched UDP transmission.
275///
276/// The sender is intended to be reused for the lifetime of a socket. It keeps a
277/// reusable scratch buffer and a small transmit queue so successive flushes do not
278/// need to rebuild that state from scratch.
279///
280/// `MAX_PACKET_SIZE` determines the capacity reserved for the transmit scratch buffer.
281pub struct UdpBatchSender<const BATCH_SIZE: usize, const MAX_PACKET_SIZE: usize = 4096> {
282    state: UdpSocketState,
283    queued_packets: VecDeque<(SocketAddr, Packet)>,
284    scratch: Vec<u8>,
285    /// Datagrams discarded since [`UdpBatchSender::take_discarded_datagrams`] was last called.
286    discarded_datagrams: u64,
287    /// When a discarded datagram was last reported at warning level.
288    last_discard_log: Option<Instant>,
289    /// Datagrams discarded since the last warning, reported with the next one.
290    discards_since_log: u64,
291}
292
293impl<const BATCH_SIZE: usize, const MAX_PACKET_SIZE: usize>
294    UdpBatchSender<BATCH_SIZE, MAX_PACKET_SIZE>
295{
296    /// Creates a sender configured for a fixed compile-time batch size.
297    ///
298    /// Callers should generally create one sender per socket and reuse it across
299    /// queue/flush cycles so the queue and scratch storage stay hot.
300    pub fn new(socket: &UdpSocket) -> io::Result<Self> {
301        assert!(
302            BATCH_SIZE > 0,
303            "UdpBatchSender BATCH_SIZE must be greater than zero"
304        );
305        assert!(
306            BATCH_SIZE <= MAX_BATCH_SIZE,
307            "UdpBatchSender BATCH_SIZE must not exceed MAX_BATCH_SIZE"
308        );
309        Ok(Self {
310            state: new_socket_state(socket)?,
311            queued_packets: VecDeque::with_capacity(BATCH_SIZE),
312            scratch: Vec::with_capacity(MAX_PACKET_SIZE * BATCH_SIZE),
313            discarded_datagrams: 0,
314            last_discard_log: None,
315            discards_since_log: 0,
316        })
317    }
318
319    /// Returns whether no packets are currently queued for transmission.
320    pub fn is_empty(&self) -> bool {
321        self.queued_packets.is_empty()
322    }
323
324    /// Returns whether the sender queue has reached its configured capacity.
325    pub fn is_full(&self) -> bool {
326        self.queued_packets.len() == BATCH_SIZE
327    }
328
329    /// Queues one packet for transmission to `target`.
330    ///
331    /// Returns an error when the sender queue is full or when `packet` exceeds
332    /// `MAX_PACKET_SIZE`, which would otherwise force the scratch buffer to grow.
333    ///
334    /// Accepting a packet here does not guarantee that it can be delivered: datagrams above
335    /// the egress path MTU are rejected by the kernel and are discarded when flushing.
336    pub fn try_queue_packet(
337        &mut self,
338        packet: Packet,
339        target: SocketAddr,
340    ) -> Result<(), QueuePacketError> {
341        let packet_len = packet.len();
342        if packet.len() > MAX_PACKET_SIZE {
343            return Err(QueuePacketError::PacketTooLarge {
344                packet,
345                target,
346                packet_len,
347                max_packet_size: MAX_PACKET_SIZE,
348            });
349        }
350        if self.is_full() {
351            return Err(QueuePacketError::Full { packet, target });
352        }
353        self.queued_packets.push_back((target, packet));
354        Ok(())
355    }
356
357    /// Attempts to flush queued packets without waiting for the socket to become writable.
358    ///
359    /// A datagram the socket refuses is discarded rather than retried forever: it would
360    /// otherwise stay at the head of the queue and block every packet behind it for good.
361    /// This applies to a datagram that can never be sent, such as one above the egress MTU
362    /// which fails with `EMSGSIZE` because `quinn-udp` sets the don't-fragment bit, but also
363    /// costs a datagram on transient conditions like a full qdisc reporting `ENOBUFS`.
364    ///
365    /// Discards are counted in [`Self::take_discarded_datagrams`] and reported at warning
366    /// level at most once per minute, so callers that want to observe every discard should
367    /// consume that counter rather than rely on the log.
368    ///
369    /// Fails only with [`SocketNotWritable`], which leaves the queue intact.
370    pub fn try_flush_best_effort(&mut self, socket: &UdpSocket) -> Result<(), SocketNotWritable> {
371        // Cleared once a coalesced transmit failed so the remaining datagrams of this flush
372        // are sent one by one. That tells a datagram which is undeliverable on its own apart
373        // from a transmit that only failed because it was coalesced with others.
374        let mut coalesce = true;
375
376        while !self.is_empty() {
377            let (target, segment_size, segments) = self.fill_scratch_from_front(coalesce);
378            let result = socket.try_io(Interest::WRITABLE, || {
379                let transmit = Transmit {
380                    destination: target,
381                    ecn: None,
382                    contents: &self.scratch,
383                    segment_size: (segments > 1).then_some(segment_size),
384                    src_ip: None,
385                };
386                self.state.try_send(UdpSockRef::from(socket), &transmit)
387            });
388
389            match result {
390                Ok(()) => self.drop_prefix(segments),
391                Err(err) if err.kind() == io::ErrorKind::WouldBlock => {
392                    return Err(SocketNotWritable);
393                }
394                Err(err) if segments > 1 => {
395                    tracing::debug!(
396                        ?target,
397                        segments,
398                        segment_size,
399                        err = ?err,
400                        "coalesced transmit failed, retrying datagrams individually"
401                    );
402                    coalesce = false;
403                }
404                Err(err) => {
405                    self.drop_prefix(segments);
406                    self.discarded_datagrams += 1;
407                    self.log_discard(target, segment_size, &err);
408                }
409            }
410        }
411        Ok(())
412    }
413
414    /// Flushes queued packets, waiting asynchronously until the socket becomes writable.
415    ///
416    /// Refused datagrams are discarded as described on [`Self::try_flush_best_effort`], so the
417    /// only error this returns comes from waiting for writability.
418    pub async fn flush(&mut self, socket: &UdpSocket) -> io::Result<()> {
419        while !self.is_empty() {
420            socket.writable().await?;
421            if self.try_flush_best_effort(socket).is_ok() {
422                break;
423            }
424        }
425        Ok(())
426    }
427
428    /// Returns how many datagrams were discarded since this was last called, and resets the
429    /// count.
430    ///
431    /// Discarding keeps the queue draining but loses the datagram, so callers should export
432    /// this as a metric: a rising count means outbound traffic is being dropped.
433    pub fn take_discarded_datagrams(&mut self) -> u64 {
434        std::mem::take(&mut self.discarded_datagrams)
435    }
436
437    /// Reports a discarded datagram, at warning level at most once per
438    /// [`DISCARD_LOG_INTERVAL`] so that a peer producing undeliverable datagrams at line rate
439    /// cannot flood the log.
440    fn log_discard(&mut self, target: SocketAddr, datagram_len: usize, err: &io::Error) {
441        let now = Instant::now();
442        let due = self
443            .last_discard_log
444            .is_none_or(|last| now.duration_since(last) >= DISCARD_LOG_INTERVAL);
445
446        if !due {
447            self.discards_since_log += 1;
448            tracing::debug!(?target, datagram_len, err = ?err, "discarding refused datagram");
449            return;
450        }
451
452        tracing::warn!(
453            ?target,
454            datagram_len,
455            err = ?err,
456            suppressed_discards = self.discards_since_log,
457            "discarding refused datagram"
458        );
459        self.last_discard_log = Some(now);
460        self.discards_since_log = 0;
461    }
462
463    fn drop_prefix(&mut self, count: usize) {
464        self.queued_packets.drain(..count);
465    }
466
467    /// Copies the datagrams at the front of the queue into the scratch buffer.
468    ///
469    /// Returns their common destination, their segment size and how many of them were
470    /// copied. The datagrams stay queued until the transmit is known to have left the queue.
471    fn fill_scratch_from_front(&mut self, coalesce: bool) -> (SocketAddr, usize, usize) {
472        self.scratch.clear();
473        let (target, first_packet) = self
474            .queued_packets
475            .front()
476            .expect("filling the scratch buffer requires a non-empty queue");
477        let target = *target;
478        let segment_size = first_packet.len();
479        let mut segments = 0;
480        // A coalesced transmit is one datagram towards the kernel, so it must respect the
481        // maximum UDP payload size on top of the offload and batch limits.
482        //
483        // Zero-length datagrams are excluded from coalescing entirely: the kernel reads a
484        // segment size of zero as "no segmentation" and delivers a single datagram for the
485        // whole group, which would retire every queue entry while sending only one of them.
486        let max_segments = if coalesce && segment_size > 0 {
487            self.state
488                .max_gso_segments()
489                .min(BATCH_SIZE)
490                .min(MAX_UDP_PAYLOAD_SIZE / segment_size)
491                .max(1)
492        } else {
493            1
494        };
495
496        // Only coalesce the segments at the front with matching destination and
497        // segment size so queue order stays intact and we can drop exactly the
498        // packets that were handed to the kernel.
499        for (queued_target, packet) in self.queued_packets.iter().take(max_segments) {
500            if *queued_target != target || packet.len() != segment_size {
501                break;
502            }
503            self.scratch.extend_from_slice(&packet[..]);
504            segments += 1;
505        }
506
507        (target, segment_size, segments)
508    }
509}
510
511#[cfg(test)]
512mod tests {
513    use std::{net::SocketAddr, time::Duration};
514
515    use ana_gotatun::packet::PacketBufPool;
516    use tokio::net::UdpSocket;
517
518    use super::{MAX_BATCH_SIZE, MAX_UDP_PAYLOAD_SIZE, UdpBatchReceiver, UdpBatchSender};
519
520    const TEST_PACKET_SIZE: usize = 128;
521
522    fn packet_pool() -> PacketBufPool<TEST_PACKET_SIZE> {
523        PacketBufPool::new(MAX_BATCH_SIZE)
524    }
525
526    async fn bound_socket() -> UdpSocket {
527        UdpSocket::bind("127.0.0.1:0").await.unwrap()
528    }
529
530    fn packet_from_bytes(
531        pool: &PacketBufPool<TEST_PACKET_SIZE>,
532        bytes: &[u8],
533    ) -> ana_gotatun::packet::Packet {
534        let mut packet = pool.get();
535        packet[..bytes.len()].copy_from_slice(bytes);
536        packet.truncate(bytes.len());
537        packet
538    }
539
540    #[tokio::test]
541    async fn flushes_partially_full_sender_batch() {
542        let sender_socket = bound_socket().await;
543        let receiver_socket = bound_socket().await;
544        let pool = packet_pool();
545        let mut sender =
546            UdpBatchSender::<MAX_BATCH_SIZE, TEST_PACKET_SIZE>::new(&sender_socket).unwrap();
547
548        sender
549            .try_queue_packet(
550                packet_from_bytes(&pool, b"one"),
551                receiver_socket.local_addr().unwrap(),
552            )
553            .unwrap();
554        sender
555            .try_queue_packet(
556                packet_from_bytes(&pool, b"two"),
557                receiver_socket.local_addr().unwrap(),
558            )
559            .unwrap();
560
561        sender.flush(&sender_socket).await.unwrap();
562
563        let mut buf = [0u8; TEST_PACKET_SIZE];
564        let (n1, _) = receiver_socket.recv_from(&mut buf).await.unwrap();
565        let first = buf[..n1].to_vec();
566        let (n2, _) = receiver_socket.recv_from(&mut buf).await.unwrap();
567        let second = buf[..n2].to_vec();
568
569        assert!(sender.is_empty());
570        assert_eq!(vec![first, second], vec![b"one".to_vec(), b"two".to_vec()]);
571    }
572
573    #[tokio::test]
574    async fn flushes_sender_batch_with_mixed_targets() {
575        let sender_socket = bound_socket().await;
576        let first_target = bound_socket().await;
577        let second_target = bound_socket().await;
578        let pool = packet_pool();
579        let mut sender =
580            UdpBatchSender::<MAX_BATCH_SIZE, TEST_PACKET_SIZE>::new(&sender_socket).unwrap();
581
582        sender
583            .try_queue_packet(
584                packet_from_bytes(&pool, b"alpha"),
585                first_target.local_addr().unwrap(),
586            )
587            .unwrap();
588        sender
589            .try_queue_packet(
590                packet_from_bytes(&pool, b"beta"),
591                second_target.local_addr().unwrap(),
592            )
593            .unwrap();
594        sender
595            .try_queue_packet(
596                packet_from_bytes(&pool, b"gamma"),
597                first_target.local_addr().unwrap(),
598            )
599            .unwrap();
600
601        sender.flush(&sender_socket).await.unwrap();
602
603        let mut buf = [0u8; TEST_PACKET_SIZE];
604        let (n_first_a, _) = first_target.recv_from(&mut buf).await.unwrap();
605        let first_a = buf[..n_first_a].to_vec();
606        let (n_second, _) = second_target.recv_from(&mut buf).await.unwrap();
607        let second = buf[..n_second].to_vec();
608        let (n_first_b, _) = first_target.recv_from(&mut buf).await.unwrap();
609        let first_b = buf[..n_first_b].to_vec();
610
611        assert_eq!(first_a, b"alpha".to_vec());
612        assert_eq!(second, b"beta".to_vec());
613        assert_eq!(first_b, b"gamma".to_vec());
614    }
615
616    /// A datagram above the maximum UDP payload size can never be sent. It must not keep the
617    /// packets queued behind it from being flushed.
618    #[tokio::test]
619    async fn flush_discards_undeliverable_datagram_and_drains_the_rest() {
620        const OVERSIZED_PACKET_SIZE: usize = MAX_UDP_PAYLOAD_SIZE + 1;
621
622        let sender_socket = bound_socket().await;
623        let receiver_socket = bound_socket().await;
624        let target = receiver_socket.local_addr().unwrap();
625        let pool = PacketBufPool::<OVERSIZED_PACKET_SIZE>::new(2);
626        let mut sender =
627            UdpBatchSender::<MAX_BATCH_SIZE, OVERSIZED_PACKET_SIZE>::new(&sender_socket).unwrap();
628
629        let mut oversized = pool.get();
630        oversized.truncate(OVERSIZED_PACKET_SIZE);
631        sender.try_queue_packet(oversized, target).unwrap();
632
633        let mut deliverable = pool.get();
634        deliverable[..5].copy_from_slice(b"after");
635        deliverable.truncate(5);
636        sender.try_queue_packet(deliverable, target).unwrap();
637
638        sender.flush(&sender_socket).await.unwrap();
639
640        let mut buf = [0u8; 64];
641        let (len, _) = receiver_socket.recv_from(&mut buf).await.unwrap();
642
643        assert!(sender.is_empty(), "the queue must not retain the discard");
644        assert_eq!(&buf[..len], b"after");
645        assert_eq!(
646            sender.take_discarded_datagrams(),
647            1,
648            "the discard must be accountable, the log is only sampled"
649        );
650        assert_eq!(
651            sender.take_discarded_datagrams(),
652            0,
653            "taking the count must reset it"
654        );
655    }
656
657    /// A single datagram larger than the segment size that `quinn-udp` probes with must
658    /// arrive as one datagram. A segmentation setting left on the socket would split it.
659    #[tokio::test]
660    async fn flush_delivers_a_single_large_datagram_in_one_piece() {
661        const LARGE_PACKET_SIZE: usize = 2048;
662
663        let sender_socket = bound_socket().await;
664        let receiver_socket = bound_socket().await;
665        let target = receiver_socket.local_addr().unwrap();
666        let pool = PacketBufPool::<LARGE_PACKET_SIZE>::new(MAX_BATCH_SIZE + 1);
667        let mut sender =
668            UdpBatchSender::<MAX_BATCH_SIZE, LARGE_PACKET_SIZE>::new(&sender_socket).unwrap();
669        let _receiver =
670            UdpBatchReceiver::<MAX_BATCH_SIZE, LARGE_PACKET_SIZE>::new(&sender_socket, &pool)
671                .unwrap();
672
673        let mut packet = pool.get();
674        packet.truncate(LARGE_PACKET_SIZE);
675        sender.try_queue_packet(packet, target).unwrap();
676        sender.flush(&sender_socket).await.unwrap();
677
678        let mut buf = [0u8; 2 * LARGE_PACKET_SIZE];
679        let (len, _) = receiver_socket.recv_from(&mut buf).await.unwrap();
680
681        assert_eq!(len, LARGE_PACKET_SIZE);
682        assert!(sender.is_empty());
683        assert_eq!(sender.take_discarded_datagrams(), 0);
684    }
685
686    /// Coalescing must stay within the maximum UDP payload size, which the kernel would
687    /// otherwise reject with `EMSGSIZE`.
688    #[tokio::test]
689    async fn coalescing_respects_the_maximum_udp_payload_size() {
690        const LARGE_PACKET_SIZE: usize = 2048;
691
692        let sender_socket = bound_socket().await;
693        let target = bound_socket().await.local_addr().unwrap();
694        let pool = PacketBufPool::<LARGE_PACKET_SIZE>::new(MAX_BATCH_SIZE);
695        let mut sender =
696            UdpBatchSender::<MAX_BATCH_SIZE, LARGE_PACKET_SIZE>::new(&sender_socket).unwrap();
697
698        for _ in 0..MAX_BATCH_SIZE {
699            let mut packet = pool.get();
700            packet.truncate(LARGE_PACKET_SIZE);
701            sender.try_queue_packet(packet, target).unwrap();
702        }
703
704        let (_, segment_size, segments) = sender.fill_scratch_from_front(true);
705
706        assert_eq!(segment_size, LARGE_PACKET_SIZE);
707        assert!(segments >= 1);
708        assert!(
709            segments * segment_size <= MAX_UDP_PAYLOAD_SIZE,
710            "coalesced {segments} segments of {segment_size} bytes exceed the maximum UDP payload"
711        );
712    }
713
714    /// Zero-length datagrams are legal and must each reach the peer.
715    ///
716    /// They must not be coalesced: a segment size of zero reads as "no segmentation" to the
717    /// kernel, which accepts the transmit and delivers a single datagram for the whole group.
718    /// The queue entries would all be retired, losing every datagram but the first without
719    /// any error to account for.
720    #[tokio::test]
721    async fn flush_delivers_every_zero_length_datagram() {
722        const PACKETS: usize = 3;
723
724        let sender_socket = bound_socket().await;
725        let receiver_socket = bound_socket().await;
726        let target = receiver_socket.local_addr().unwrap();
727        let pool = packet_pool();
728        let mut sender =
729            UdpBatchSender::<MAX_BATCH_SIZE, TEST_PACKET_SIZE>::new(&sender_socket).unwrap();
730
731        for _ in 0..PACKETS {
732            sender
733                .try_queue_packet(packet_from_bytes(&pool, b""), target)
734                .unwrap();
735        }
736
737        let (_, segment_size, segments) = sender.fill_scratch_from_front(true);
738        assert_eq!(segment_size, 0);
739        assert_eq!(segments, 1, "zero-length datagrams must be sent one by one");
740
741        sender.flush(&sender_socket).await.unwrap();
742
743        assert!(sender.is_empty());
744        assert_eq!(sender.take_discarded_datagrams(), 0);
745
746        let mut buf = [0u8; TEST_PACKET_SIZE];
747        for index in 0..PACKETS {
748            let received =
749                tokio::time::timeout(Duration::from_secs(5), receiver_socket.recv_from(&mut buf))
750                    .await
751                    .unwrap_or_else(|_| panic!("zero-length datagram {index} never arrived"))
752                    .unwrap();
753
754            assert_eq!(received.0, 0);
755        }
756    }
757
758    /// All queued datagrams must reach the peer even when their total exceeds what fits into a
759    /// single coalesced transmit.
760    #[tokio::test]
761    async fn flush_delivers_more_datagrams_than_one_transmit_can_carry() {
762        const LARGE_PACKET_SIZE: usize = 2048;
763        const PACKETS: usize = MAX_BATCH_SIZE;
764
765        let sender_socket = bound_socket().await;
766        let receiver_socket = bound_socket().await;
767        let target = receiver_socket.local_addr().unwrap();
768        let pool = PacketBufPool::<LARGE_PACKET_SIZE>::new(PACKETS);
769        let mut sender =
770            UdpBatchSender::<MAX_BATCH_SIZE, LARGE_PACKET_SIZE>::new(&sender_socket).unwrap();
771
772        for index in 0..PACKETS {
773            let mut packet = pool.get();
774            packet.truncate(LARGE_PACKET_SIZE);
775            packet[0] = index as u8;
776            sender.try_queue_packet(packet, target).unwrap();
777        }
778
779        // Receive concurrently with the flush: the datagrams together exceed the receive
780        // buffer, and a lost one has to fail the test rather than block it forever.
781        let receive = tokio::spawn(async move {
782            let mut buf = [0u8; LARGE_PACKET_SIZE];
783            let mut received = Vec::with_capacity(PACKETS);
784            for _ in 0..PACKETS {
785                let (len, _) = receiver_socket.recv_from(&mut buf).await.unwrap();
786                assert_eq!(len, LARGE_PACKET_SIZE);
787                received.push(buf[0]);
788            }
789            received
790        });
791
792        sender.flush(&sender_socket).await.unwrap();
793
794        let received = tokio::time::timeout(Duration::from_secs(10), receive)
795            .await
796            .expect("all datagrams should arrive")
797            .unwrap();
798
799        assert!(sender.is_empty());
800        assert_eq!(
801            received,
802            (0..PACKETS).map(|index| index as u8).collect::<Vec<_>>(),
803            "datagrams arrived out of order or were lost"
804        );
805    }
806
807    #[tokio::test]
808    async fn receive_with_stride_smaller_than_length_splits_segments() {
809        let socket = bound_socket().await;
810        let pool = packet_pool();
811        let mut receiver =
812            UdpBatchReceiver::<MAX_BATCH_SIZE, TEST_PACKET_SIZE>::new(&socket, &pool).unwrap();
813        let source = "127.0.0.1:30000".parse::<SocketAddr>().unwrap();
814
815        receiver.recv_meta[0].addr = source;
816        receiver.recv_meta[0].len = 10;
817        receiver.recv_meta[0].stride = 4;
818        receiver.recv_slots[0][..10].copy_from_slice(b"abcdefghij");
819
820        let mut seen = Vec::new();
821        receiver
822            .handle_received(0, &pool, &mut |packet, addr| {
823                seen.push((packet[..].to_vec(), addr));
824                Ok::<(), ()>(())
825            })
826            .unwrap();
827
828        assert_eq!(
829            seen,
830            vec![
831                (b"abcd".to_vec(), source),
832                (b"efgh".to_vec(), source),
833                (b"ij".to_vec(), source),
834            ]
835        );
836    }
837
838    #[tokio::test]
839    async fn receive_with_stride_at_least_length_uses_single_packet() {
840        let socket = bound_socket().await;
841        let pool = packet_pool();
842        let mut receiver =
843            UdpBatchReceiver::<MAX_BATCH_SIZE, TEST_PACKET_SIZE>::new(&socket, &pool).unwrap();
844        let source = "127.0.0.1:30001".parse::<SocketAddr>().unwrap();
845
846        receiver.recv_meta[0].addr = source;
847        receiver.recv_meta[0].len = 5;
848        receiver.recv_meta[0].stride = 5;
849        receiver.recv_slots[0][..5].copy_from_slice(b"hello");
850
851        let mut seen = Vec::new();
852        receiver
853            .handle_received(0, &pool, &mut |packet, addr| {
854                seen.push((packet[..].to_vec(), addr));
855                Ok::<(), ()>(())
856            })
857            .unwrap();
858
859        assert_eq!(seen, vec![(b"hello".to_vec(), source)]);
860    }
861
862    #[test]
863    fn refuses_to_grow_beyond_batch_capacity() {
864        let runtime = tokio::runtime::Runtime::new().unwrap();
865        runtime.block_on(async {
866            let socket = bound_socket().await;
867            let pool = packet_pool();
868            let mut sender =
869                UdpBatchSender::<MAX_BATCH_SIZE, TEST_PACKET_SIZE>::new(&socket).unwrap();
870
871            for _ in 0..MAX_BATCH_SIZE {
872                sender
873                    .try_queue_packet(packet_from_bytes(&pool, b"x"), socket.local_addr().unwrap())
874                    .unwrap();
875            }
876
877            assert!(
878                sender
879                    .try_queue_packet(
880                        packet_from_bytes(&pool, b"overflow"),
881                        socket.local_addr().unwrap()
882                    )
883                    .is_err()
884            );
885        });
886    }
887}