Skip to main content

net_io/
udp.rs

1//! UDP: a bound socket that reports truncated datagrams, budgets sources, and
2//! never amplifies (FR-49 to FR-51).
3//!
4//! UDP source addresses are trivially spoofed, so the guard sits between the
5//! socket and the caller: every datagram is charged to its source's budget
6//! (and the global one) before it is returned, and every send to a source that
7//! has not proved it can receive is limited to `factor` times what it sent us.
8
9use std::collections::HashMap;
10use std::io;
11use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV6, ToSocketAddrs};
12use std::time::{Duration, Instant};
13
14use crate::addr::normalize;
15use crate::error::{ConfigReason, LimitKind, NetError, TimeoutKind};
16use crate::sys::{self, AsSock};
17
18/// Rates for one budget: datagrams and bytes per second, and how many datagrams
19/// may arrive at once. The byte bucket holds one second of bytes.
20///
21/// ```
22/// let budget = net_io::UdpBudget { datagrams_per_sec: 100, bytes_per_sec: 150_000, burst: 20 };
23/// # let _ = budget;
24/// ```
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub struct UdpBudget {
27    /// Datagrams per second, sustained.
28    pub datagrams_per_sec: u32,
29    /// Bytes per second, sustained.
30    pub bytes_per_sec: u32,
31    /// Datagrams that may arrive at once (the datagram bucket's depth).
32    pub burst: u32,
33}
34
35impl UdpBudget {
36    /// The default per-source budget: 1,000 datagrams and 1 MB per second,
37    /// bursts of 100.
38    pub const SOURCE_DEFAULT: UdpBudget = UdpBudget {
39        datagrams_per_sec: 1_000,
40        bytes_per_sec: 1_000_000,
41        burst: 100,
42    };
43}
44
45/// Options for [`UdpSocket::bind_with`].
46///
47/// ```
48/// use net_io::{UdpBudget, UdpOptions};
49/// let options = UdpOptions::new()
50///     .amplification_factor(3) // a QUIC handshake (RFC 9000)
51///     .global_budget(UdpBudget { datagrams_per_sec: 50_000, bytes_per_sec: 60_000_000, burst: 5_000 });
52/// # let _ = options;
53/// ```
54#[must_use]
55#[derive(Clone, Debug, PartialEq, Eq)]
56pub struct UdpOptions {
57    factor: u8,
58    window: Duration,
59    source: UdpBudget,
60    global: Option<UdpBudget>,
61    table: u32,
62    ipv6_only: bool,
63    packet_info: bool,
64}
65
66impl Default for UdpOptions {
67    fn default() -> UdpOptions {
68        UdpOptions::new()
69    }
70}
71
72impl UdpOptions {
73    /// Factor 1, a 10 s window, [`UdpBudget::SOURCE_DEFAULT`] per source, no
74    /// global budget, 65,536 sources, IPv6-only for IPv6 addresses.
75    pub fn new() -> UdpOptions {
76        UdpOptions {
77            factor: 1,
78            window: Duration::from_secs(10),
79            source: UdpBudget::SOURCE_DEFAULT,
80            global: None,
81            table: 65_536,
82            ipv6_only: true,
83            packet_info: false,
84        }
85    }
86
87    /// How many times the bytes received from an unverified source may be sent
88    /// back to it (1 by default, at most 3; decision D-034).
89    pub fn amplification_factor(mut self, factor: u8) -> UdpOptions {
90        self.factor = factor;
91        self
92    }
93
94    /// How long received bytes count toward a source's send allowance (10 s by
95    /// default, at least 1 s). The allowance spans the current and the previous
96    /// window, so a reply just after a window boundary is not refused.
97    pub fn amplification_window(mut self, window: Duration) -> UdpOptions {
98        self.window = window;
99        self
100    }
101
102    /// The budget of each source address.
103    pub fn source_budget(mut self, budget: UdpBudget) -> UdpOptions {
104        self.source = budget;
105        self
106    }
107
108    /// A budget for the whole socket (none by default).
109    pub fn global_budget(mut self, budget: UdpBudget) -> UdpOptions {
110        self.global = Some(budget);
111        self
112    }
113
114    /// How many sources are tracked (65,536 by default, about 130 bytes each).
115    pub fn source_table(mut self, entries: u32) -> UdpOptions {
116        self.table = entries;
117        self
118    }
119
120    /// `IPV6_V6ONLY` for an IPv6 address (on by default). Off, an IPv6 socket also
121    /// receives IPv4; sources are reported as IPv4 and replies to them work.
122    /// Reports each datagram's destination address in [`Datagram::destination`], for a
123    /// socket bound to a wildcard that must answer from the address it was asked on.
124    /// One `recvmsg` per datagram instead of `recvfrom`. Unix only: `bind_with` fails
125    /// with `Unsupported` on Windows, and `recv_batch` does not fill it.
126    pub fn packet_info(mut self, on: bool) -> UdpOptions {
127        self.packet_info = on;
128        self
129    }
130
131    /// `IPV6_V6ONLY` for IPv6 addresses (on by default).
132    pub fn ipv6_only(mut self, on: bool) -> UdpOptions {
133        self.ipv6_only = on;
134        self
135    }
136
137    fn validate(&self) -> Result<(), NetError> {
138        let invalid = |field, reason| Err(NetError::InvalidConfig { field, reason });
139        if self.factor == 0 {
140            return invalid("amplification_factor", ConfigReason::Zero);
141        }
142        if self.factor > 3 {
143            return invalid("amplification_factor", ConfigReason::TooLarge { max: 3 });
144        }
145        if self.window < Duration::from_secs(1) {
146            return invalid("amplification_window", ConfigReason::TooSmall { min: 1 });
147        }
148        if self.table == 0 {
149            return invalid("source_table", ConfigReason::Zero);
150        }
151        for (field, b) in [
152            ("source_budget", Some(self.source)),
153            ("global_budget", self.global),
154        ] {
155            if let Some(b) = b {
156                if b.datagrams_per_sec == 0 || b.bytes_per_sec == 0 || b.burst == 0 {
157                    return invalid(field, ConfigReason::Zero);
158                }
159            }
160        }
161        Ok(())
162    }
163}
164
165/// One received datagram.
166///
167/// ```
168/// let d = net_io::Datagram { len: 3, from: "127.0.0.1:9".parse().unwrap(), truncated: false, destination: None };
169/// assert!(!d.truncated);
170/// ```
171#[derive(Clone, Copy, Debug, PartialEq, Eq)]
172pub struct Datagram {
173    /// Its length. When `truncated`: the real length on Linux, the bytes copied
174    /// (the buffer's length) elsewhere; never index the buffer with it then.
175    pub len: usize,
176    /// Its source, normalized.
177    pub from: SocketAddr,
178    /// It did not fit the buffer and was cut.
179    pub truncated: bool,
180    /// The local address it was sent to (packet info, `0.6.0`; always `None` before).
181    pub destination: Option<IpAddr>,
182}
183
184/// Counters of one socket since it was bound.
185///
186/// ```
187/// let s = net_io::UdpSocket::bind("127.0.0.1:0")?.stats();
188/// assert_eq!(s.received, 0);
189/// # Ok::<(), net_io::NetError>(())
190/// ```
191#[non_exhaustive]
192#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
193pub struct UdpStats {
194    /// Datagrams returned to the caller.
195    pub received: u64,
196    /// Datagrams sent.
197    pub sent: u64,
198    /// Datagrams read and dropped because their source or the socket was over budget.
199    pub dropped: u64,
200    /// Sends refused by the amplification guard.
201    pub refused: u64,
202    /// Datagrams returned truncated.
203    pub truncated: u64,
204    /// ICMP error reports read from the socket and ignored.
205    pub icmp_errors: u64,
206    /// Datagrams from sources that found the table full (the shared overflow budget).
207    pub overflow: u64,
208    /// Sources tracked now.
209    pub sources: u32,
210}
211
212/// A token bucket in micro-tokens: one token per datagram, one per byte.
213#[derive(Clone, Copy, Debug)]
214struct Bucket {
215    datagrams: u64,
216    bytes: u64,
217    at: Instant,
218}
219
220const MICRO: u64 = 1_000_000;
221
222impl Bucket {
223    fn full(budget: &UdpBudget, now: Instant) -> Bucket {
224        Bucket {
225            datagrams: u64::from(budget.burst) * MICRO,
226            bytes: u64::from(budget.bytes_per_sec) * MICRO,
227            at: now,
228        }
229    }
230
231    /// Refills for the time passed and takes one datagram of `len` bytes.
232    fn take(&mut self, budget: &UdpBudget, len: usize, now: Instant) -> bool {
233        // Micro-tokens per microsecond equal tokens per second; a gap of more
234        // than 1,000 s refills the buckets completely anyway.
235        let us = now
236            .saturating_duration_since(self.at)
237            .as_micros()
238            .min(1_000_000_000) as u64;
239        self.at = now;
240        let cap_d = u64::from(budget.burst) * MICRO;
241        let cap_b = u64::from(budget.bytes_per_sec) * MICRO;
242        self.datagrams = (self.datagrams + us * u64::from(budget.datagrams_per_sec)).min(cap_d);
243        self.bytes = (self.bytes + us * u64::from(budget.bytes_per_sec)).min(cap_b);
244        let need_b = (len as u64).saturating_mul(MICRO);
245        if self.datagrams >= MICRO && self.bytes >= need_b {
246            self.datagrams -= MICRO;
247            self.bytes -= need_b;
248            true
249        } else {
250            false
251        }
252    }
253}
254
255/// What the guard knows about one source address.
256#[derive(Clone, Copy, Debug)]
257struct Entry {
258    ip: IpAddr,
259    bucket: Bucket,
260    received: [u64; 2],
261    sent: [u64; 2],
262    window_at: Instant,
263    verified: bool,
264    referenced: bool,
265}
266
267impl Entry {
268    fn new(ip: IpAddr, budget: &UdpBudget, now: Instant) -> Entry {
269        Entry {
270            ip,
271            bucket: Bucket::full(budget, now),
272            received: [0; 2],
273            sent: [0; 2],
274            window_at: now,
275            verified: false,
276            referenced: true,
277        }
278    }
279
280    /// Index 0 is the current window, 1 the previous one.
281    fn roll(&mut self, window: Duration, now: Instant) {
282        let gone = now.saturating_duration_since(self.window_at);
283        if gone >= window * 2 {
284            self.received = [0; 2];
285            self.sent = [0; 2];
286            self.window_at = now;
287        } else if gone >= window {
288            self.received = [0, self.received[0]];
289            self.sent = [0, self.sent[0]];
290            self.window_at += window;
291        }
292    }
293}
294
295/// How many entries one insertion may look at before it gives up and uses the
296/// overflow budget.
297const SWEEP: usize = 64;
298
299/// The source table: a bounded map with CLOCK eviction of unverified entries.
300#[derive(Debug)]
301struct Guard {
302    index: HashMap<IpAddr, u32>,
303    slots: Vec<Entry>,
304    hand: usize,
305    overflow: Bucket,
306    global: Option<Bucket>,
307    options: UdpOptions,
308}
309
310impl Guard {
311    fn new(options: UdpOptions, now: Instant) -> Guard {
312        Guard {
313            index: HashMap::new(),
314            slots: Vec::new(),
315            hand: 0,
316            overflow: Bucket::full(&options.source, now),
317            global: options.global.map(|g| Bucket::full(&g, now)),
318            options,
319        }
320    }
321
322    fn find(&mut self, ip: IpAddr) -> Option<usize> {
323        let i = *self.index.get(&ip)? as usize;
324        self.slots[i].referenced = true;
325        Some(i)
326    }
327
328    /// Finds or makes room for `ip`. `force` (verification) may evict a
329    /// recently used unverified entry; plain traffic may not.
330    fn slot(&mut self, ip: IpAddr, now: Instant, force: bool) -> Option<usize> {
331        if let Some(i) = self.find(ip) {
332            return Some(i);
333        }
334        let entry = Entry::new(ip, &self.options.source, now);
335        if self.slots.len() < self.options.table as usize {
336            self.slots.push(entry);
337            let i = self.slots.len() - 1;
338            self.index.insert(ip, i as u32);
339            return Some(i);
340        }
341        let n = self.slots.len();
342        let looks = if force { 2 * n } else { SWEEP.min(2 * n) };
343        for _ in 0..looks {
344            let i = self.hand;
345            self.hand = (self.hand + 1) % n;
346            let e = &mut self.slots[i];
347            if e.verified {
348                continue;
349            }
350            if e.referenced {
351                e.referenced = false;
352                continue;
353            }
354            self.index.remove(&e.ip);
355            *e = entry;
356            self.index.insert(ip, i as u32);
357            return Some(i);
358        }
359        None
360    }
361
362    /// Charges a received datagram; `false` drops it. `overflow` is set when the
363    /// source found no room in the table.
364    fn admit(&mut self, ip: IpAddr, len: usize, now: Instant, overflow: &mut bool) -> bool {
365        let source = self.options.source;
366        let ok = match self.slot(ip, now, false) {
367            Some(i) => {
368                let window = self.options.window;
369                let e = &mut self.slots[i];
370                if e.bucket.take(&source, len, now) {
371                    e.roll(window, now);
372                    e.received[0] += len as u64;
373                    true
374                } else {
375                    false
376                }
377            }
378            None => {
379                *overflow = true;
380                self.overflow.take(&source, len, now)
381            }
382        };
383        if !ok {
384            return false;
385        }
386        match (&mut self.global, &self.options.global) {
387            (Some(bucket), Some(budget)) => bucket.take(budget, len, now),
388            _ => true,
389        }
390    }
391
392    /// Whether `len` bytes may go to `ip` now.
393    fn may_send(&mut self, ip: IpAddr, len: usize, now: Instant) -> bool {
394        let Some(i) = self.find(ip) else {
395            return len == 0;
396        };
397        let window = self.options.window;
398        let factor = u64::from(self.options.factor);
399        let e = &mut self.slots[i];
400        if e.verified {
401            return true;
402        }
403        e.roll(window, now);
404        let allowed = factor * (e.received[0] + e.received[1]);
405        e.sent[0] + e.sent[1] + len as u64 <= allowed
406    }
407
408    fn sent(&mut self, ip: IpAddr, len: usize) {
409        if let Some(&i) = self.index.get(&ip) {
410            self.slots[i as usize].sent[0] += len as u64;
411        }
412    }
413
414    fn verify(&mut self, ip: IpAddr, now: Instant) {
415        if let Some(i) = self.slot(ip, now, true) {
416            self.slots[i].verified = true;
417        }
418    }
419}
420
421/// A UDP socket with the guard in front of it.
422///
423/// ```no_run
424/// let mut socket = net_io::UdpSocket::bind("0.0.0.0:5353")?;
425/// let mut buf = [0u8; 1500];
426/// loop {
427///     let datagram = socket.recv_from(&mut buf)?;
428///     if datagram.truncated {
429///         continue; // larger than we accept
430///     }
431///     // Replies are limited to the size of what the peer sent until it is verified.
432///     socket.send_to(&buf[..datagram.len], datagram.from)?;
433/// }
434/// # Ok::<(), net_io::NetError>(())
435/// ```
436#[derive(Debug)]
437pub struct UdpSocket {
438    inner: std::net::UdpSocket,
439    guard: Guard,
440    read_timeout: Option<Duration>,
441    peer: Option<SocketAddr>,
442    v6: bool,
443    #[cfg_attr(windows, allow(dead_code))]
444    packet_info: bool,
445    stats: UdpStats,
446}
447
448/// A receive buffer of a batch; [`UdpSocket::recv_batch`] fills `datagram`.
449///
450/// ```
451/// let mut bytes = [0u8; 1500];
452/// let slot = net_io::DatagramBuffer::new(&mut bytes);
453/// assert!(slot.datagram.is_none());
454/// ```
455#[derive(Debug)]
456pub struct DatagramBuffer<'a> {
457    /// Where the bytes land.
458    pub buf: &'a mut [u8],
459    /// What arrived; `None` for a slot not filled, or dropped over budget.
460    pub datagram: Option<Datagram>,
461}
462
463impl<'a> DatagramBuffer<'a> {
464    /// An empty slot over `buf`.
465    #[must_use]
466    pub fn new(buf: &'a mut [u8]) -> DatagramBuffer<'a> {
467        DatagramBuffer {
468            buf,
469            datagram: None,
470        }
471    }
472}
473
474/// One datagram of a batch to send.
475///
476/// ```
477/// let d = net_io::OutgoingDatagram { bytes: b"hi", to: "127.0.0.1:9".parse().unwrap() };
478/// assert_eq!(d.bytes.len(), 2);
479/// ```
480#[derive(Clone, Copy, Debug)]
481pub struct OutgoingDatagram<'a> {
482    /// The bytes.
483    pub bytes: &'a [u8],
484    /// The destination.
485    pub to: SocketAddr,
486}
487
488/// Which interface joins a multicast group.
489///
490/// ```
491/// let any = net_io::MulticastInterface::Any;
492/// # let _ = any;
493/// ```
494#[non_exhaustive]
495#[derive(Clone, Copy, Debug, PartialEq, Eq)]
496pub enum MulticastInterface {
497    /// The one the OS picks.
498    Any,
499    /// The IPv4 interface with this address.
500    V4(Ipv4Addr),
501    /// The IPv6 interface with this index.
502    V6Index(u32),
503}
504
505impl UdpSocket {
506    /// Binds with [`UdpOptions::new`]. The first resolved address that binds wins.
507    ///
508    /// # Errors
509    ///
510    /// `AddrInUse`, `AddrNotAvailable`, `PermissionDenied`, `NoAddresses`, `Bind`.
511    pub fn bind<A: ToSocketAddrs>(addr: A) -> Result<UdpSocket, NetError> {
512        UdpSocket::bind_with(addr, UdpOptions::new())
513    }
514
515    /// Binds with `options`.
516    ///
517    /// # Errors
518    ///
519    /// As [`UdpSocket::bind`]; `InvalidConfig` for an option out of range.
520    pub fn bind_with<A: ToSocketAddrs>(
521        addr: A,
522        options: UdpOptions,
523    ) -> Result<UdpSocket, NetError> {
524        options.validate()?;
525        let addrs: Vec<SocketAddr> = addr
526            .to_socket_addrs()
527            .map_err(|e| NetError::Os {
528                op: "resolve",
529                source: e,
530            })?
531            .collect();
532        let mut last = NetError::NoAddresses;
533        if options.packet_info && !cfg!(unix) {
534            return Err(NetError::Unsupported {
535                feature: "UDP packet info",
536                platform: std::env::consts::OS,
537            });
538        }
539        let packet_info = options.packet_info;
540        for a in addrs {
541            match sys::udp_bind(a, options.ipv6_only) {
542                Ok(inner) => {
543                    #[cfg(unix)]
544                    if packet_info {
545                        sys::udp_packet_info(inner.sock(), a.is_ipv6())
546                            .map_err(|e| NetError::from_os("setsockopt", e))?;
547                    }
548                    return Ok(UdpSocket {
549                        inner,
550                        guard: Guard::new(options, Instant::now()),
551                        read_timeout: None,
552                        peer: None,
553                        v6: a.is_ipv6(),
554                        packet_info,
555                        stats: UdpStats::default(),
556                    });
557                }
558                Err(e) => last = crate::tcp::bind_error(a, e),
559            }
560        }
561        Err(last)
562    }
563
564    /// Receives one datagram within the budgets. Datagrams over budget are read,
565    /// dropped, and counted; ICMP error reports are counted and never end the
566    /// socket. Under a flood the call returns within twice the read timeout.
567    ///
568    /// # Errors
569    ///
570    /// `TimedOut { kind: Read }` (only with a read timeout), `Os`.
571    pub fn recv_from(&mut self, buf: &mut [u8]) -> Result<Datagram, NetError> {
572        let deadline = self.read_timeout.map(|t| Instant::now() + t);
573        loop {
574            let (len, from, truncated, destination) = match self.recv_one(buf) {
575                Ok(got) => got,
576                Err(e) => match e.kind() {
577                    io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut => {
578                        return Err(NetError::TimedOut {
579                            kind: TimeoutKind::Read,
580                        });
581                    }
582                    io::ErrorKind::Interrupted => continue,
583                    io::ErrorKind::ConnectionRefused | io::ErrorKind::ConnectionReset => {
584                        // An ICMP report about an earlier send: never fatal (edge case 92).
585                        self.stats.icmp_errors += 1;
586                        if past(deadline) {
587                            return Err(NetError::TimedOut {
588                                kind: TimeoutKind::Read,
589                            });
590                        }
591                        continue;
592                    }
593                    _ => return Err(NetError::from_os("recvfrom", e)),
594                },
595            };
596            let from = crate::addr::normalize_socket(from);
597            if !self.admit(from, len, truncated) {
598                if past(deadline) {
599                    return Err(NetError::TimedOut {
600                        kind: TimeoutKind::Read,
601                    });
602                }
603                continue;
604            }
605            return Ok(Datagram {
606                len,
607                from,
608                truncated,
609                destination,
610            });
611        }
612    }
613
614    fn recv_one(
615        &mut self,
616        buf: &mut [u8],
617    ) -> io::Result<(usize, SocketAddr, bool, Option<IpAddr>)> {
618        #[cfg(unix)]
619        if self.packet_info {
620            return sys::udp_recv_with_destination(self.inner.sock(), buf);
621        }
622        let (len, from, truncated) = sys::udp_recv_from(self.inner.sock(), buf)?;
623        Ok((len, from, truncated, None))
624    }
625
626    /// Charges a datagram to its source and counts it.
627    fn admit(&mut self, from: SocketAddr, len: usize, truncated: bool) -> bool {
628        let mut overflow = false;
629        let admitted = self
630            .guard
631            .admit(from.ip(), len, Instant::now(), &mut overflow);
632        if overflow {
633            self.stats.overflow += 1;
634        }
635        if !admitted {
636            self.stats.dropped += 1;
637            return false;
638        }
639        self.stats.received += 1;
640        if truncated {
641            self.stats.truncated += 1;
642        }
643        true
644    }
645
646    /// Waits for a datagram, then takes what else is ready, up to `slots.len()`
647    /// (64 at most). Each datagram passes the budgets as in [`UdpSocket::recv_from`]:
648    /// one that does not leaves its slot's `datagram` at `None`, so walk every slot.
649    /// One system call on Linux (`recvmmsg`), a loop elsewhere. Returns how many
650    /// slots hold a datagram. Packet info is not reported here.
651    ///
652    /// # Errors
653    ///
654    /// As [`UdpSocket::recv_from`].
655    pub fn recv_batch(&mut self, slots: &mut [DatagramBuffer<'_>]) -> Result<usize, NetError> {
656        for slot in slots.iter_mut() {
657            slot.datagram = None;
658        }
659        if slots.is_empty() {
660            return Ok(0);
661        }
662        self.recv_many(slots)
663    }
664
665    #[cfg(target_os = "linux")]
666    fn recv_many(&mut self, slots: &mut [DatagramBuffer<'_>]) -> Result<usize, NetError> {
667        let count = slots.len().min(sys::BATCH_MAX);
668        let mut got = [None; sys::BATCH_MAX];
669        let n = loop {
670            let received = {
671                let mut bufs: [&mut [u8]; sys::BATCH_MAX] = std::array::from_fn(|_| &mut [][..]);
672                for (b, slot) in bufs.iter_mut().zip(slots.iter_mut()).take(count) {
673                    *b = &mut *slot.buf;
674                }
675                sys::udp_recv_many(self.inner.sock(), &mut bufs[..count], &mut got)
676            };
677            match received {
678                Ok(n) => break n,
679                Err(e) => match e.kind() {
680                    io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut => {
681                        return Err(NetError::TimedOut {
682                            kind: TimeoutKind::Read,
683                        });
684                    }
685                    io::ErrorKind::Interrupted => {}
686                    io::ErrorKind::ConnectionRefused | io::ErrorKind::ConnectionReset => {
687                        self.stats.icmp_errors += 1;
688                    }
689                    _ => return Err(NetError::from_os("recvmmsg", e)),
690                },
691            }
692        };
693        let mut filled = 0;
694        for (slot, r) in slots.iter_mut().zip(got.iter()).take(n) {
695            let Some(r) = r else { continue };
696            let from = crate::addr::normalize_socket(r.from);
697            if self.admit(from, r.len, r.truncated) {
698                slot.datagram = Some(Datagram {
699                    len: r.len,
700                    from,
701                    truncated: r.truncated,
702                    destination: None,
703                });
704                filled += 1;
705            }
706        }
707        Ok(filled)
708    }
709
710    #[cfg(not(target_os = "linux"))]
711    fn recv_many(&mut self, slots: &mut [DatagramBuffer<'_>]) -> Result<usize, NetError> {
712        let (first, rest) = match slots.split_first_mut() {
713            Some(parts) => parts,
714            None => return Ok(0),
715        };
716        first.datagram = Some(self.recv_from(first.buf)?);
717        let mut filled = 1;
718        self.inner
719            .set_nonblocking(true)
720            .map_err(|e| NetError::from_os("ioctl", e))?;
721        for slot in rest.iter_mut().take(63) {
722            match self.recv_from(slot.buf) {
723                Ok(d) => {
724                    slot.datagram = Some(d);
725                    filled += 1;
726                }
727                Err(_) => break,
728            }
729        }
730        self.inner
731            .set_nonblocking(false)
732            .map_err(|e| NetError::from_os("ioctl", e))?;
733        Ok(filled)
734    }
735
736    /// Sends from the front of `datagrams`, each under the guard of
737    /// [`UdpSocket::send_to`], until one is refused or the OS takes no more. One system
738    /// call per 64 on Linux (`sendmmsg`), a loop elsewhere. Returns how many went.
739    ///
740    /// # Errors
741    ///
742    /// `Limit { which: Amplification }` when the first is refused, `Os`.
743    pub fn send_batch(&mut self, datagrams: &[OutgoingDatagram<'_>]) -> Result<usize, NetError> {
744        let mut sent = 0;
745        while sent < datagrams.len() {
746            match self.send_some(&datagrams[sent..]) {
747                Ok(0) => break,
748                Ok(n) => sent += n,
749                Err(e) if sent == 0 => return Err(e),
750                Err(_) => break,
751            }
752        }
753        Ok(sent)
754    }
755
756    /// The guard's prefix of `datagrams`. Charged before the send, so a short send
757    /// leaves the peer's allowance smaller, never larger.
758    fn charge(&mut self, datagrams: &[OutgoingDatagram<'_>]) -> Result<usize, NetError> {
759        let now = Instant::now();
760        let mut allowed = 0;
761        for d in datagrams.iter().take(64) {
762            let ip = normalize(d.to.ip());
763            if !self.guard.may_send(ip, d.bytes.len(), now) {
764                self.stats.refused += 1;
765                break;
766            }
767            self.guard.sent(ip, d.bytes.len());
768            allowed += 1;
769        }
770        if allowed == 0 && !datagrams.is_empty() {
771            return Err(NetError::Limit {
772                which: LimitKind::Amplification,
773            });
774        }
775        Ok(allowed)
776    }
777
778    #[cfg(target_os = "linux")]
779    fn send_some(&mut self, datagrams: &[OutgoingDatagram<'_>]) -> Result<usize, NetError> {
780        let allowed = self.charge(datagrams)?;
781        let mut wire = [(&[][..], SocketAddr::from(([0, 0, 0, 0], 0))); sys::BATCH_MAX];
782        for (w, d) in wire.iter_mut().zip(datagrams.iter().take(allowed)) {
783            *w = (d.bytes, self.wire(d.to));
784        }
785        let n = sys::udp_send_many(self.inner.sock(), &wire[..allowed])
786            .map_err(|e| NetError::from_os("sendmmsg", e))?;
787        self.stats.sent += n as u64;
788        Ok(n)
789    }
790
791    #[cfg(not(target_os = "linux"))]
792    fn send_some(&mut self, datagrams: &[OutgoingDatagram<'_>]) -> Result<usize, NetError> {
793        let allowed = self.charge(datagrams)?;
794        let mut n = 0;
795        for d in &datagrams[..allowed] {
796            let wire = self.wire(d.to);
797            if let Err(e) = self.inner.send_to(d.bytes, wire) {
798                if n == 0 {
799                    return Err(NetError::from_os("sendto", e));
800                }
801                break;
802            }
803            self.stats.sent += 1;
804            n += 1;
805        }
806        Ok(n)
807    }
808
809    /// Joins a multicast group. Datagrams to the group then pass the same budgets as
810    /// any other; a reply to a group member is still under the amplification guard.
811    ///
812    /// # Errors
813    ///
814    /// `InvalidConfig` when the interface's family is not the group's, `Os`.
815    pub fn join_multicast(
816        &self,
817        group: IpAddr,
818        interface: MulticastInterface,
819    ) -> Result<(), NetError> {
820        let result = match (group, interface) {
821            (IpAddr::V4(g), MulticastInterface::Any) => {
822                self.inner.join_multicast_v4(&g, &Ipv4Addr::UNSPECIFIED)
823            }
824            (IpAddr::V4(g), MulticastInterface::V4(i)) => self.inner.join_multicast_v4(&g, &i),
825            (IpAddr::V6(g), MulticastInterface::Any) => self.inner.join_multicast_v6(&g, 0),
826            (IpAddr::V6(g), MulticastInterface::V6Index(i)) => self.inner.join_multicast_v6(&g, i),
827            _ => {
828                return Err(NetError::InvalidConfig {
829                    field: "interface",
830                    reason: ConfigReason::Inconsistent,
831                });
832            }
833        };
834        result.map_err(|e| NetError::from_os("setsockopt", e))
835    }
836
837    /// Receives from the connected peer (see [`UdpSocket::connect`]).
838    ///
839    /// # Errors
840    ///
841    /// As [`UdpSocket::recv_from`].
842    pub fn recv(&mut self, buf: &mut [u8]) -> Result<Datagram, NetError> {
843        self.recv_from(buf)
844    }
845
846    /// Sends one datagram. To a source not marked verified, the bytes sent within
847    /// the window never exceed `factor` times the bytes received from it; a
848    /// source never heard from gets nothing (an empty datagram aside).
849    ///
850    /// # Errors
851    ///
852    /// `Limit { which: Amplification }` (nothing was sent), `Os`.
853    pub fn send_to(&mut self, buf: &[u8], to: SocketAddr) -> Result<usize, NetError> {
854        let ip = normalize(to.ip());
855        if !self.guard.may_send(ip, buf.len(), Instant::now()) {
856            self.stats.refused += 1;
857            return Err(NetError::Limit {
858                which: LimitKind::Amplification,
859            });
860        }
861        let wire = self.wire(to);
862        let n = self
863            .inner
864            .send_to(buf, wire)
865            .map_err(|e| NetError::from_os("sendto", e))?;
866        self.guard.sent(ip, n);
867        self.stats.sent += 1;
868        Ok(n)
869    }
870
871    /// Sends to the connected peer, under the same guard as [`UdpSocket::send_to`].
872    ///
873    /// # Errors
874    ///
875    /// `Closed` when not connected; as [`UdpSocket::send_to`].
876    pub fn send(&mut self, buf: &[u8]) -> Result<usize, NetError> {
877        let peer = self.peer.ok_or(NetError::Closed)?;
878        let ip = normalize(peer.ip());
879        if !self.guard.may_send(ip, buf.len(), Instant::now()) {
880            self.stats.refused += 1;
881            return Err(NetError::Limit {
882                which: LimitKind::Amplification,
883            });
884        }
885        let n = self
886            .inner
887            .send(buf)
888            .map_err(|e| NetError::from_os("send", e))?;
889        self.guard.sent(ip, n);
890        self.stats.sent += 1;
891        Ok(n)
892    }
893
894    /// Fixes the peer: the kernel drops datagrams from other sources, and
895    /// [`UdpSocket::send`] goes to it.
896    ///
897    /// # Errors
898    ///
899    /// `Os`.
900    pub fn connect(&mut self, to: SocketAddr) -> Result<(), NetError> {
901        self.inner
902            .connect(self.wire(to))
903            .map_err(|e| NetError::from_os("connect", e))?;
904        self.peer = Some(to);
905        Ok(())
906    }
907
908    /// The protocol proved `peer` can receive (a cookie, a handshake): the guard
909    /// no longer limits sends to it, and its table entry is kept.
910    pub fn mark_verified(&mut self, peer: SocketAddr) {
911        self.guard.verify(normalize(peer.ip()), Instant::now());
912    }
913
914    /// How long a receive waits (`None`: forever, the default).
915    ///
916    /// # Errors
917    ///
918    /// `InvalidConfig` for a zero duration, `Os`.
919    pub fn set_read_timeout(&mut self, timeout: Option<Duration>) -> Result<(), NetError> {
920        if timeout == Some(Duration::ZERO) {
921            return Err(NetError::InvalidConfig {
922                field: "read_timeout",
923                reason: ConfigReason::Zero,
924            });
925        }
926        self.inner
927            .set_read_timeout(timeout)
928            .map_err(|e| NetError::from_os("setsockopt", e))?;
929        self.read_timeout = timeout;
930        Ok(())
931    }
932
933    /// Receives never wait: a server's thread waits in `poll` with its wake channel.
934    pub(crate) fn set_nonblocking(&self) -> Result<(), NetError> {
935        self.inner
936            .set_nonblocking(true)
937            .map_err(|e| NetError::from_os("ioctl", e))
938    }
939
940    pub(crate) fn raw_sock(&self) -> sys::Sock<'_> {
941        self.inner.sock()
942    }
943
944    /// The bound address.
945    ///
946    /// # Errors
947    ///
948    /// `Os` when `getsockname` fails.
949    pub fn local_addr(&self) -> Result<SocketAddr, NetError> {
950        self.inner
951            .local_addr()
952            .map_err(|e| NetError::from_os("getsockname", e))
953    }
954
955    /// Counters since the socket was bound.
956    #[must_use]
957    pub fn stats(&self) -> UdpStats {
958        UdpStats {
959            sources: self.guard.slots.len() as u32,
960            ..self.stats
961        }
962    }
963
964    /// An IPv4 destination on an IPv6 socket goes out as its IPv4-mapped form.
965    fn wire(&self, to: SocketAddr) -> SocketAddr {
966        match to {
967            SocketAddr::V4(a) if self.v6 => {
968                SocketAddr::V6(SocketAddrV6::new(a.ip().to_ipv6_mapped(), a.port(), 0, 0))
969            }
970            other => other,
971        }
972    }
973}
974
975/// A datagram protocol run by a server: one per UDP listener, called on that listener's
976/// thread, so it needs no locks of its own. Budgets and the amplification guard apply
977/// before it sees a datagram and on every reply.
978///
979/// ```
980/// use std::net::SocketAddr;
981/// use net_io::{UdpContext, UdpSession};
982/// struct Echo;
983/// impl UdpSession for Echo {
984///     fn on_datagram(&mut self, cx: &mut UdpContext<'_>, from: SocketAddr, bytes: &[u8]) {
985///         let _ = cx.send_to(bytes, from);
986///     }
987/// }
988/// ```
989pub trait UdpSession: Send + 'static {
990    /// One datagram that passed its budgets.
991    fn on_datagram(&mut self, cx: &mut UdpContext<'_>, from: SocketAddr, bytes: &[u8]);
992}
993
994/// What a [`UdpSession`] may do during a call.
995#[derive(Debug)]
996pub struct UdpContext<'a> {
997    pub(crate) socket: &'a mut UdpSocket,
998}
999
1000impl UdpContext<'_> {
1001    /// Sends a datagram under the guard of [`UdpSocket::send_to`].
1002    ///
1003    /// # Errors
1004    ///
1005    /// As [`UdpSocket::send_to`].
1006    pub fn send_to(&mut self, bytes: &[u8], to: SocketAddr) -> Result<usize, NetError> {
1007        self.socket.send_to(bytes, to)
1008    }
1009
1010    /// As [`UdpSocket::mark_verified`].
1011    pub fn mark_verified(&mut self, peer: SocketAddr) {
1012        self.socket.mark_verified(peer);
1013    }
1014
1015    /// The time.
1016    #[must_use]
1017    pub fn now(&self) -> Instant {
1018        Instant::now()
1019    }
1020}
1021
1022/// Makes the [`UdpSession`] of each UDP listener; any
1023/// `Fn(&ListenerInfo) -> impl UdpSession` is one.
1024pub trait UdpSessionFactory: Send + Sync + 'static {
1025    /// The session for this listener.
1026    fn open(&self, listener: &crate::tcp::ListenerInfo) -> Box<dyn UdpSession>;
1027}
1028
1029impl<F, S> UdpSessionFactory for F
1030where
1031    F: Fn(&crate::tcp::ListenerInfo) -> S + Send + Sync + 'static,
1032    S: UdpSession,
1033{
1034    fn open(&self, listener: &crate::tcp::ListenerInfo) -> Box<dyn UdpSession> {
1035        Box::new(self(listener))
1036    }
1037}
1038
1039fn past(deadline: Option<Instant>) -> bool {
1040    deadline.is_some_and(|d| Instant::now() >= d)
1041}
1042
1043#[cfg(unix)]
1044impl std::os::fd::AsFd for UdpSocket {
1045    fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> {
1046        self.inner.as_fd()
1047    }
1048}
1049
1050#[cfg(windows)]
1051impl std::os::windows::io::AsSocket for UdpSocket {
1052    fn as_socket(&self) -> std::os::windows::io::BorrowedSocket<'_> {
1053        self.inner.as_socket()
1054    }
1055}
1056
1057#[cfg(test)]
1058mod tests {
1059    use super::*;
1060
1061    fn ip(n: u8) -> IpAddr {
1062        IpAddr::from([10, 0, 0, n])
1063    }
1064
1065    #[test]
1066    fn test_bucket_refills_at_its_rate() {
1067        let b = UdpBudget {
1068            datagrams_per_sec: 10,
1069            bytes_per_sec: 1_000,
1070            burst: 2,
1071        };
1072        let t = Instant::now();
1073        let mut bucket = Bucket::full(&b, t);
1074        assert!(bucket.take(&b, 100, t));
1075        assert!(bucket.take(&b, 100, t));
1076        assert!(!bucket.take(&b, 100, t), "burst used up");
1077        // 100 ms refills one datagram.
1078        assert!(bucket.take(&b, 100, t + Duration::from_millis(100)));
1079        // Bytes: the bucket holds one second; 1,001 bytes never pass.
1080        assert!(!bucket.take(&b, 1_001, t + Duration::from_secs(5)));
1081    }
1082
1083    #[test]
1084    fn test_amplification_factor_one_until_verified() {
1085        // Edge case 94: a 1-byte request, a 2-byte reply refused until verified.
1086        let t = Instant::now();
1087        let mut g = Guard::new(UdpOptions::new(), t);
1088        let mut o = false;
1089        assert!(!g.may_send(ip(1), 1, t), "never heard from");
1090        assert!(g.may_send(ip(1), 0, t));
1091        assert!(g.admit(ip(1), 1, t, &mut o));
1092        assert!(!g.may_send(ip(1), 2, t));
1093        assert!(g.may_send(ip(1), 1, t));
1094        g.sent(ip(1), 1);
1095        assert!(!g.may_send(ip(1), 1, t), "the allowance is spent");
1096        g.verify(ip(1), t);
1097        assert!(g.may_send(ip(1), 10_000, t));
1098    }
1099
1100    #[test]
1101    fn test_amplification_factor_three_and_the_window() {
1102        let t = Instant::now();
1103        let w = Duration::from_secs(10);
1104        let mut g = Guard::new(UdpOptions::new().amplification_factor(3), t);
1105        let mut o = false;
1106        assert!(g.admit(ip(1), 100, t, &mut o));
1107        assert!(g.may_send(ip(1), 300, t));
1108        assert!(!g.may_send(ip(1), 301, t));
1109        // Just after the window edge the previous window still counts.
1110        assert!(g.may_send(ip(1), 300, t + w + Duration::from_millis(1)));
1111        // Two windows later it is gone.
1112        assert!(!g.may_send(ip(1), 1, t + 3 * w));
1113    }
1114
1115    #[test]
1116    fn test_source_budget_drops_and_others_pass() {
1117        let t = Instant::now();
1118        let budget = UdpBudget {
1119            datagrams_per_sec: 1,
1120            bytes_per_sec: 10_000,
1121            burst: 3,
1122        };
1123        let mut g = Guard::new(UdpOptions::new().source_budget(budget), t);
1124        let mut o = false;
1125        let passed = (0..10).filter(|_| g.admit(ip(1), 10, t, &mut o)).count();
1126        assert_eq!(passed, 3);
1127        assert!(
1128            g.admit(ip(2), 10, t, &mut o),
1129            "another source has its own budget"
1130        );
1131        assert!(!o);
1132    }
1133
1134    #[test]
1135    fn test_global_budget_caps_every_source() {
1136        let t = Instant::now();
1137        let global = UdpBudget {
1138            datagrams_per_sec: 1,
1139            bytes_per_sec: 10_000,
1140            burst: 4,
1141        };
1142        let mut g = Guard::new(UdpOptions::new().global_budget(global), t);
1143        let mut o = false;
1144        let passed = (1..=10).filter(|&n| g.admit(ip(n), 10, t, &mut o)).count();
1145        assert_eq!(passed, 4);
1146    }
1147
1148    #[test]
1149    fn test_full_table_evicts_strangers_and_keeps_verified() {
1150        // Edge case 93.
1151        let t = Instant::now();
1152        let mut g = Guard::new(UdpOptions::new().source_table(4), t);
1153        let mut o = false;
1154        for n in 1..=4 {
1155            assert!(g.admit(ip(n), 10, t, &mut o));
1156        }
1157        g.verify(ip(1), t);
1158        g.verify(ip(2), t);
1159        // Recently used entries survive one pass of the hand; the next newcomer
1160        // takes an unverified one.
1161        assert!(g.admit(ip(5), 10, t, &mut o));
1162        assert!(!o, "an unverified entry was evicted");
1163        assert!(g.index.contains_key(&ip(1)) && g.index.contains_key(&ip(2)));
1164        assert_eq!(g.slots.len(), 4);
1165        // Fill with verified peers: strangers share the overflow budget.
1166        for n in [3, 4, 5] {
1167            if g.index.contains_key(&ip(n)) {
1168                g.verify(ip(n), t);
1169            }
1170        }
1171        for n in 6..=7 {
1172            g.verify(ip(n), t);
1173        }
1174        let mut o = false;
1175        assert!(g.admit(ip(9), 10, t, &mut o));
1176        assert!(o, "no room: the overflow budget");
1177        assert!(
1178            !g.may_send(ip(9), 1, t),
1179            "an overflow stranger gets no allowance"
1180        );
1181        assert!(
1182            g.index.contains_key(&ip(1)),
1183            "verified peers keep their entries"
1184        );
1185    }
1186
1187    #[test]
1188    fn test_options_validate() {
1189        let bad = |o: UdpOptions| matches!(o.validate(), Err(NetError::InvalidConfig { .. }));
1190        assert!(bad(UdpOptions::new().amplification_factor(0)));
1191        assert!(bad(UdpOptions::new().amplification_factor(4)));
1192        assert!(bad(
1193            UdpOptions::new().amplification_window(Duration::from_millis(500))
1194        ));
1195        assert!(bad(UdpOptions::new().source_table(0)));
1196        assert!(bad(UdpOptions::new().source_budget(UdpBudget {
1197            datagrams_per_sec: 0,
1198            bytes_per_sec: 1,
1199            burst: 1
1200        })));
1201        assert!(UdpOptions::new().validate().is_ok());
1202        assert_eq!(UdpOptions::default(), UdpOptions::new());
1203    }
1204
1205    #[test]
1206    fn test_echo_truncation_and_zero_length() {
1207        let mut s = UdpSocket::bind("127.0.0.1:0").unwrap();
1208        s.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
1209        let to = s.local_addr().unwrap();
1210        let c = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
1211        c.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
1212        c.send_to(b"hello", to).unwrap();
1213        let mut buf = [0u8; 64];
1214        let d = s.recv_from(&mut buf).unwrap();
1215        assert_eq!(
1216            (d.len, d.truncated, d.from),
1217            (5, false, c.local_addr().unwrap())
1218        );
1219        assert_eq!(s.send_to(&buf[..5], d.from).unwrap(), 5);
1220        let mut back = [0u8; 8];
1221        assert_eq!(c.recv(&mut back).unwrap(), 5);
1222        assert!(matches!(
1223            s.send_to(b"x", d.from),
1224            Err(NetError::Limit {
1225                which: LimitKind::Amplification
1226            })
1227        ));
1228        // Edge cases 90 and 91.
1229        c.send_to(&[7u8; 100], to).unwrap();
1230        let mut small = [0u8; 10];
1231        let d = s.recv_from(&mut small).unwrap();
1232        assert!(d.truncated);
1233        c.send_to(&[], to).unwrap();
1234        let d = s.recv_from(&mut buf).unwrap();
1235        assert_eq!((d.len, d.truncated), (0, false));
1236        let st = s.stats();
1237        assert_eq!(
1238            (st.received, st.sent, st.refused, st.truncated),
1239            (3, 1, 1, 1)
1240        );
1241        assert_eq!(st.sources, 1);
1242    }
1243
1244    #[test]
1245    fn test_icmp_reports_never_end_the_socket() {
1246        // Edge case 92: an injected "connection reset" and a real port-unreachable.
1247        use crate::sys::shim::{Fault, Op, clear_here, program_here};
1248        let mut s = UdpSocket::bind("127.0.0.1:0").unwrap();
1249        s.set_read_timeout(Some(Duration::from_millis(300)))
1250            .unwrap();
1251        let closed = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
1252        let gone = closed.local_addr().unwrap();
1253        drop(closed);
1254        s.connect(gone).unwrap();
1255        s.mark_verified(gone);
1256        s.send(b"anyone?").unwrap();
1257        program_here(Op::Recv, 0, 1, Fault::Kind(io::ErrorKind::ConnectionReset));
1258        let mut buf = [0u8; 16];
1259        assert!(matches!(
1260            s.recv(&mut buf),
1261            Err(NetError::TimedOut {
1262                kind: TimeoutKind::Read
1263            })
1264        ));
1265        clear_here();
1266        assert!(s.stats().icmp_errors >= 1);
1267        // Still usable.
1268        let mut t = UdpSocket::bind("127.0.0.1:0").unwrap();
1269        t.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
1270        let c = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
1271        c.send_to(b"ok", t.local_addr().unwrap()).unwrap();
1272        assert_eq!(t.recv_from(&mut buf).unwrap().len, 2);
1273    }
1274
1275    #[test]
1276    fn test_dual_stack_replies_to_ipv4_sources() {
1277        let Ok(mut s) = UdpSocket::bind_with("[::]:0", UdpOptions::new().ipv6_only(false)) else {
1278            return; // no IPv6 on this host
1279        };
1280        s.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
1281        let port = s.local_addr().unwrap().port();
1282        let c = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
1283        c.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
1284        c.send_to(b"v4", ("127.0.0.1", port)).unwrap();
1285        let mut buf = [0u8; 8];
1286        let d = s.recv_from(&mut buf).unwrap();
1287        assert!(d.from.is_ipv4(), "normalized: {}", d.from);
1288        s.send_to(b"ok", d.from).unwrap();
1289        assert_eq!(c.recv(&mut buf).unwrap(), 2);
1290    }
1291
1292    fn verified_pair() -> (UdpSocket, UdpSocket) {
1293        let mut a = UdpSocket::bind("127.0.0.1:0").unwrap();
1294        let mut b = UdpSocket::bind("127.0.0.1:0").unwrap();
1295        let (aa, ba) = (a.local_addr().unwrap(), b.local_addr().unwrap());
1296        a.mark_verified(ba);
1297        b.mark_verified(aa);
1298        a.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
1299        b.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
1300        (a, b)
1301    }
1302
1303    #[test]
1304    fn test_batches_cross_in_one_call_each() {
1305        let (mut a, mut b) = verified_pair();
1306        let to = b.local_addr().unwrap();
1307        let bodies: Vec<Vec<u8>> = (0..10u8).map(|i| vec![i; 1 + usize::from(i)]).collect();
1308        let out: Vec<_> = bodies
1309            .iter()
1310            .map(|b| OutgoingDatagram { bytes: b, to })
1311            .collect();
1312        assert_eq!(a.send_batch(&out).unwrap(), 10);
1313        std::thread::sleep(Duration::from_millis(50));
1314        let mut store = vec![[0u8; 64]; 16];
1315        let mut seen = 0;
1316        while seen < 10 {
1317            let mut slots: Vec<_> = store.iter_mut().map(|b| DatagramBuffer::new(b)).collect();
1318            let n = b.recv_batch(&mut slots).unwrap();
1319            assert!(n > 0);
1320            for slot in &slots {
1321                if let Some(d) = slot.datagram {
1322                    assert_eq!(d.from, a.local_addr().unwrap());
1323                    assert_eq!(&slot.buf[..d.len], &bodies[seen][..]);
1324                    seen += 1;
1325                }
1326            }
1327        }
1328        assert_eq!(a.stats().sent, 10);
1329        assert_eq!(b.stats().received, 10);
1330    }
1331
1332    /// An unverified peer's allowance holds across a batch: what got through is what
1333    /// the guard allows, and a batch cannot spend it twice.
1334    #[test]
1335    fn test_batch_send_stays_under_the_guard() {
1336        let mut server = UdpSocket::bind("127.0.0.1:0").unwrap();
1337        let client = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
1338        let to = client.local_addr().unwrap();
1339        client
1340            .send_to(&[0u8; 100], server.local_addr().unwrap())
1341            .unwrap();
1342        let mut buf = [0u8; 200];
1343        server.recv_from(&mut buf).unwrap();
1344        let big = [1u8; 40];
1345        let out = [OutgoingDatagram { bytes: &big, to }; 4];
1346        assert_eq!(
1347            server.send_batch(&out).unwrap(),
1348            2,
1349            "100 bytes of allowance"
1350        );
1351        assert!(matches!(
1352            server.send_batch(&out),
1353            Err(NetError::Limit {
1354                which: LimitKind::Amplification
1355            })
1356        ));
1357    }
1358
1359    #[cfg(unix)]
1360    #[test]
1361    fn test_packet_info_reports_the_destination() {
1362        let mut server =
1363            UdpSocket::bind_with("0.0.0.0:0", UdpOptions::new().packet_info(true)).unwrap();
1364        server
1365            .set_read_timeout(Some(Duration::from_secs(5)))
1366            .unwrap();
1367        let port = server.local_addr().unwrap().port();
1368        let client = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
1369        client.send_to(b"hi", ("127.0.0.1", port)).unwrap();
1370        let mut buf = [0u8; 16];
1371        let d = server.recv_from(&mut buf).unwrap();
1372        assert_eq!(d.len, 2);
1373        assert_eq!(d.destination, Some("127.0.0.1".parse().unwrap()));
1374    }
1375
1376    #[cfg(windows)]
1377    #[test]
1378    fn test_packet_info_is_refused_on_windows() {
1379        assert!(matches!(
1380            UdpSocket::bind_with("127.0.0.1:0", UdpOptions::new().packet_info(true)),
1381            Err(NetError::Unsupported { .. })
1382        ));
1383    }
1384
1385    #[test]
1386    fn test_multicast_join_checks_the_family() {
1387        let socket = UdpSocket::bind("0.0.0.0:0").unwrap();
1388        let group: IpAddr = "239.1.2.3".parse().unwrap();
1389        assert!(matches!(
1390            socket.join_multicast(group, MulticastInterface::V6Index(1)),
1391            Err(NetError::InvalidConfig { .. })
1392        ));
1393        // A host without a multicast route refuses; a broken family check would not.
1394        let _ = socket.join_multicast(group, MulticastInterface::Any);
1395    }
1396}