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, 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}
64
65impl Default for UdpOptions {
66    fn default() -> UdpOptions {
67        UdpOptions::new()
68    }
69}
70
71impl UdpOptions {
72    /// Factor 1, a 10 s window, [`UdpBudget::SOURCE_DEFAULT`] per source, no
73    /// global budget, 65,536 sources, IPv6-only for IPv6 addresses.
74    pub fn new() -> UdpOptions {
75        UdpOptions {
76            factor: 1,
77            window: Duration::from_secs(10),
78            source: UdpBudget::SOURCE_DEFAULT,
79            global: None,
80            table: 65_536,
81            ipv6_only: true,
82        }
83    }
84
85    /// How many times the bytes received from an unverified source may be sent
86    /// back to it (1 by default, at most 3; decision D-034).
87    pub fn amplification_factor(mut self, factor: u8) -> UdpOptions {
88        self.factor = factor;
89        self
90    }
91
92    /// How long received bytes count toward a source's send allowance (10 s by
93    /// default, at least 1 s). The allowance spans the current and the previous
94    /// window, so a reply just after a window boundary is not refused.
95    pub fn amplification_window(mut self, window: Duration) -> UdpOptions {
96        self.window = window;
97        self
98    }
99
100    /// The budget of each source address.
101    pub fn source_budget(mut self, budget: UdpBudget) -> UdpOptions {
102        self.source = budget;
103        self
104    }
105
106    /// A budget for the whole socket (none by default).
107    pub fn global_budget(mut self, budget: UdpBudget) -> UdpOptions {
108        self.global = Some(budget);
109        self
110    }
111
112    /// How many sources are tracked (65,536 by default, about 130 bytes each).
113    pub fn source_table(mut self, entries: u32) -> UdpOptions {
114        self.table = entries;
115        self
116    }
117
118    /// `IPV6_V6ONLY` for an IPv6 address (on by default). Off, an IPv6 socket also
119    /// receives IPv4; sources are reported as IPv4 and replies to them work.
120    pub fn ipv6_only(mut self, on: bool) -> UdpOptions {
121        self.ipv6_only = on;
122        self
123    }
124
125    fn validate(&self) -> Result<(), NetError> {
126        let invalid = |field, reason| Err(NetError::InvalidConfig { field, reason });
127        if self.factor == 0 {
128            return invalid("amplification_factor", ConfigReason::Zero);
129        }
130        if self.factor > 3 {
131            return invalid("amplification_factor", ConfigReason::TooLarge { max: 3 });
132        }
133        if self.window < Duration::from_secs(1) {
134            return invalid("amplification_window", ConfigReason::TooSmall { min: 1 });
135        }
136        if self.table == 0 {
137            return invalid("source_table", ConfigReason::Zero);
138        }
139        for (field, b) in [
140            ("source_budget", Some(self.source)),
141            ("global_budget", self.global),
142        ] {
143            if let Some(b) = b {
144                if b.datagrams_per_sec == 0 || b.bytes_per_sec == 0 || b.burst == 0 {
145                    return invalid(field, ConfigReason::Zero);
146                }
147            }
148        }
149        Ok(())
150    }
151}
152
153/// One received datagram.
154///
155/// ```
156/// let d = net_io::Datagram { len: 3, from: "127.0.0.1:9".parse().unwrap(), truncated: false, destination: None };
157/// assert!(!d.truncated);
158/// ```
159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
160pub struct Datagram {
161    /// Its length. When `truncated`: the real length on Linux, the bytes copied
162    /// (the buffer's length) elsewhere; never index the buffer with it then.
163    pub len: usize,
164    /// Its source, normalized.
165    pub from: SocketAddr,
166    /// It did not fit the buffer and was cut.
167    pub truncated: bool,
168    /// The local address it was sent to (packet info, `0.6.0`; always `None` before).
169    pub destination: Option<IpAddr>,
170}
171
172/// Counters of one socket since it was bound.
173///
174/// ```
175/// let s = net_io::UdpSocket::bind("127.0.0.1:0")?.stats();
176/// assert_eq!(s.received, 0);
177/// # Ok::<(), net_io::NetError>(())
178/// ```
179#[non_exhaustive]
180#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
181pub struct UdpStats {
182    /// Datagrams returned to the caller.
183    pub received: u64,
184    /// Datagrams sent.
185    pub sent: u64,
186    /// Datagrams read and dropped because their source or the socket was over budget.
187    pub dropped: u64,
188    /// Sends refused by the amplification guard.
189    pub refused: u64,
190    /// Datagrams returned truncated.
191    pub truncated: u64,
192    /// ICMP error reports read from the socket and ignored.
193    pub icmp_errors: u64,
194    /// Datagrams from sources that found the table full (the shared overflow budget).
195    pub overflow: u64,
196    /// Sources tracked now.
197    pub sources: u32,
198}
199
200/// A token bucket in micro-tokens: one token per datagram, one per byte.
201#[derive(Clone, Copy, Debug)]
202struct Bucket {
203    datagrams: u64,
204    bytes: u64,
205    at: Instant,
206}
207
208const MICRO: u64 = 1_000_000;
209
210impl Bucket {
211    fn full(budget: &UdpBudget, now: Instant) -> Bucket {
212        Bucket {
213            datagrams: u64::from(budget.burst) * MICRO,
214            bytes: u64::from(budget.bytes_per_sec) * MICRO,
215            at: now,
216        }
217    }
218
219    /// Refills for the time passed and takes one datagram of `len` bytes.
220    fn take(&mut self, budget: &UdpBudget, len: usize, now: Instant) -> bool {
221        // Micro-tokens per microsecond equal tokens per second; a gap of more
222        // than 1,000 s refills the buckets completely anyway.
223        let us = now
224            .saturating_duration_since(self.at)
225            .as_micros()
226            .min(1_000_000_000) as u64;
227        self.at = now;
228        let cap_d = u64::from(budget.burst) * MICRO;
229        let cap_b = u64::from(budget.bytes_per_sec) * MICRO;
230        self.datagrams = (self.datagrams + us * u64::from(budget.datagrams_per_sec)).min(cap_d);
231        self.bytes = (self.bytes + us * u64::from(budget.bytes_per_sec)).min(cap_b);
232        let need_b = (len as u64).saturating_mul(MICRO);
233        if self.datagrams >= MICRO && self.bytes >= need_b {
234            self.datagrams -= MICRO;
235            self.bytes -= need_b;
236            true
237        } else {
238            false
239        }
240    }
241}
242
243/// What the guard knows about one source address.
244#[derive(Clone, Copy, Debug)]
245struct Entry {
246    ip: IpAddr,
247    bucket: Bucket,
248    received: [u64; 2],
249    sent: [u64; 2],
250    window_at: Instant,
251    verified: bool,
252    referenced: bool,
253}
254
255impl Entry {
256    fn new(ip: IpAddr, budget: &UdpBudget, now: Instant) -> Entry {
257        Entry {
258            ip,
259            bucket: Bucket::full(budget, now),
260            received: [0; 2],
261            sent: [0; 2],
262            window_at: now,
263            verified: false,
264            referenced: true,
265        }
266    }
267
268    /// Index 0 is the current window, 1 the previous one.
269    fn roll(&mut self, window: Duration, now: Instant) {
270        let gone = now.saturating_duration_since(self.window_at);
271        if gone >= window * 2 {
272            self.received = [0; 2];
273            self.sent = [0; 2];
274            self.window_at = now;
275        } else if gone >= window {
276            self.received = [0, self.received[0]];
277            self.sent = [0, self.sent[0]];
278            self.window_at += window;
279        }
280    }
281}
282
283/// How many entries one insertion may look at before it gives up and uses the
284/// overflow budget.
285const SWEEP: usize = 64;
286
287/// The source table: a bounded map with CLOCK eviction of unverified entries.
288#[derive(Debug)]
289struct Guard {
290    index: HashMap<IpAddr, u32>,
291    slots: Vec<Entry>,
292    hand: usize,
293    overflow: Bucket,
294    global: Option<Bucket>,
295    options: UdpOptions,
296}
297
298impl Guard {
299    fn new(options: UdpOptions, now: Instant) -> Guard {
300        Guard {
301            index: HashMap::new(),
302            slots: Vec::new(),
303            hand: 0,
304            overflow: Bucket::full(&options.source, now),
305            global: options.global.map(|g| Bucket::full(&g, now)),
306            options,
307        }
308    }
309
310    fn find(&mut self, ip: IpAddr) -> Option<usize> {
311        let i = *self.index.get(&ip)? as usize;
312        self.slots[i].referenced = true;
313        Some(i)
314    }
315
316    /// Finds or makes room for `ip`. `force` (verification) may evict a
317    /// recently used unverified entry; plain traffic may not.
318    fn slot(&mut self, ip: IpAddr, now: Instant, force: bool) -> Option<usize> {
319        if let Some(i) = self.find(ip) {
320            return Some(i);
321        }
322        let entry = Entry::new(ip, &self.options.source, now);
323        if self.slots.len() < self.options.table as usize {
324            self.slots.push(entry);
325            let i = self.slots.len() - 1;
326            self.index.insert(ip, i as u32);
327            return Some(i);
328        }
329        let n = self.slots.len();
330        let looks = if force { 2 * n } else { SWEEP.min(2 * n) };
331        for _ in 0..looks {
332            let i = self.hand;
333            self.hand = (self.hand + 1) % n;
334            let e = &mut self.slots[i];
335            if e.verified {
336                continue;
337            }
338            if e.referenced {
339                e.referenced = false;
340                continue;
341            }
342            self.index.remove(&e.ip);
343            *e = entry;
344            self.index.insert(ip, i as u32);
345            return Some(i);
346        }
347        None
348    }
349
350    /// Charges a received datagram; `false` drops it. `overflow` is set when the
351    /// source found no room in the table.
352    fn admit(&mut self, ip: IpAddr, len: usize, now: Instant, overflow: &mut bool) -> bool {
353        let source = self.options.source;
354        let ok = match self.slot(ip, now, false) {
355            Some(i) => {
356                let window = self.options.window;
357                let e = &mut self.slots[i];
358                if e.bucket.take(&source, len, now) {
359                    e.roll(window, now);
360                    e.received[0] += len as u64;
361                    true
362                } else {
363                    false
364                }
365            }
366            None => {
367                *overflow = true;
368                self.overflow.take(&source, len, now)
369            }
370        };
371        if !ok {
372            return false;
373        }
374        match (&mut self.global, &self.options.global) {
375            (Some(bucket), Some(budget)) => bucket.take(budget, len, now),
376            _ => true,
377        }
378    }
379
380    /// Whether `len` bytes may go to `ip` now.
381    fn may_send(&mut self, ip: IpAddr, len: usize, now: Instant) -> bool {
382        let Some(i) = self.find(ip) else {
383            return len == 0;
384        };
385        let window = self.options.window;
386        let factor = u64::from(self.options.factor);
387        let e = &mut self.slots[i];
388        if e.verified {
389            return true;
390        }
391        e.roll(window, now);
392        let allowed = factor * (e.received[0] + e.received[1]);
393        e.sent[0] + e.sent[1] + len as u64 <= allowed
394    }
395
396    fn sent(&mut self, ip: IpAddr, len: usize) {
397        if let Some(&i) = self.index.get(&ip) {
398            self.slots[i as usize].sent[0] += len as u64;
399        }
400    }
401
402    fn verify(&mut self, ip: IpAddr, now: Instant) {
403        if let Some(i) = self.slot(ip, now, true) {
404            self.slots[i].verified = true;
405        }
406    }
407}
408
409/// A UDP socket with the guard in front of it.
410///
411/// ```no_run
412/// let mut socket = net_io::UdpSocket::bind("0.0.0.0:5353")?;
413/// let mut buf = [0u8; 1500];
414/// loop {
415///     let datagram = socket.recv_from(&mut buf)?;
416///     if datagram.truncated {
417///         continue; // larger than we accept
418///     }
419///     // Replies are limited to the size of what the peer sent until it is verified.
420///     socket.send_to(&buf[..datagram.len], datagram.from)?;
421/// }
422/// # Ok::<(), net_io::NetError>(())
423/// ```
424#[derive(Debug)]
425pub struct UdpSocket {
426    inner: std::net::UdpSocket,
427    guard: Guard,
428    read_timeout: Option<Duration>,
429    peer: Option<SocketAddr>,
430    v6: bool,
431    stats: UdpStats,
432}
433
434impl UdpSocket {
435    /// Binds with [`UdpOptions::new`]. The first resolved address that binds wins.
436    ///
437    /// # Errors
438    ///
439    /// `AddrInUse`, `AddrNotAvailable`, `PermissionDenied`, `NoAddresses`, `Bind`.
440    pub fn bind<A: ToSocketAddrs>(addr: A) -> Result<UdpSocket, NetError> {
441        UdpSocket::bind_with(addr, UdpOptions::new())
442    }
443
444    /// Binds with `options`.
445    ///
446    /// # Errors
447    ///
448    /// As [`UdpSocket::bind`]; `InvalidConfig` for an option out of range.
449    pub fn bind_with<A: ToSocketAddrs>(
450        addr: A,
451        options: UdpOptions,
452    ) -> Result<UdpSocket, NetError> {
453        options.validate()?;
454        let addrs: Vec<SocketAddr> = addr
455            .to_socket_addrs()
456            .map_err(|e| NetError::Os {
457                op: "resolve",
458                source: e,
459            })?
460            .collect();
461        let mut last = NetError::NoAddresses;
462        for a in addrs {
463            match sys::udp_bind(a, options.ipv6_only) {
464                Ok(inner) => {
465                    return Ok(UdpSocket {
466                        inner,
467                        guard: Guard::new(options, Instant::now()),
468                        read_timeout: None,
469                        peer: None,
470                        v6: a.is_ipv6(),
471                        stats: UdpStats::default(),
472                    });
473                }
474                Err(e) => last = crate::tcp::bind_error(a, e),
475            }
476        }
477        Err(last)
478    }
479
480    /// Receives one datagram within the budgets. Datagrams over budget are read,
481    /// dropped, and counted; ICMP error reports are counted and never end the
482    /// socket. Under a flood the call returns within twice the read timeout.
483    ///
484    /// # Errors
485    ///
486    /// `TimedOut { kind: Read }` (only with a read timeout), `Os`.
487    pub fn recv_from(&mut self, buf: &mut [u8]) -> Result<Datagram, NetError> {
488        let deadline = self.read_timeout.map(|t| Instant::now() + t);
489        loop {
490            let (len, from, truncated) = match sys::udp_recv_from(self.inner.sock(), buf) {
491                Ok(got) => got,
492                Err(e) => match e.kind() {
493                    io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut => {
494                        return Err(NetError::TimedOut {
495                            kind: TimeoutKind::Read,
496                        });
497                    }
498                    io::ErrorKind::Interrupted => continue,
499                    io::ErrorKind::ConnectionRefused | io::ErrorKind::ConnectionReset => {
500                        // An ICMP report about an earlier send: never fatal (edge case 92).
501                        self.stats.icmp_errors += 1;
502                        if past(deadline) {
503                            return Err(NetError::TimedOut {
504                                kind: TimeoutKind::Read,
505                            });
506                        }
507                        continue;
508                    }
509                    _ => return Err(NetError::from_os("recvfrom", e)),
510                },
511            };
512            let from = crate::addr::normalize_socket(from);
513            let mut overflow = false;
514            let admitted = self
515                .guard
516                .admit(from.ip(), len, Instant::now(), &mut overflow);
517            if overflow {
518                self.stats.overflow += 1;
519            }
520            if !admitted {
521                self.stats.dropped += 1;
522                if past(deadline) {
523                    return Err(NetError::TimedOut {
524                        kind: TimeoutKind::Read,
525                    });
526                }
527                continue;
528            }
529            self.stats.received += 1;
530            if truncated {
531                self.stats.truncated += 1;
532            }
533            return Ok(Datagram {
534                len,
535                from,
536                truncated,
537                destination: None,
538            });
539        }
540    }
541
542    /// Receives from the connected peer (see [`UdpSocket::connect`]).
543    ///
544    /// # Errors
545    ///
546    /// As [`UdpSocket::recv_from`].
547    pub fn recv(&mut self, buf: &mut [u8]) -> Result<Datagram, NetError> {
548        self.recv_from(buf)
549    }
550
551    /// Sends one datagram. To a source not marked verified, the bytes sent within
552    /// the window never exceed `factor` times the bytes received from it; a
553    /// source never heard from gets nothing (an empty datagram aside).
554    ///
555    /// # Errors
556    ///
557    /// `Limit { which: Amplification }` (nothing was sent), `Os`.
558    pub fn send_to(&mut self, buf: &[u8], to: SocketAddr) -> Result<usize, NetError> {
559        let ip = normalize(to.ip());
560        if !self.guard.may_send(ip, buf.len(), Instant::now()) {
561            self.stats.refused += 1;
562            return Err(NetError::Limit {
563                which: LimitKind::Amplification,
564            });
565        }
566        let wire = self.wire(to);
567        let n = self
568            .inner
569            .send_to(buf, wire)
570            .map_err(|e| NetError::from_os("sendto", e))?;
571        self.guard.sent(ip, n);
572        self.stats.sent += 1;
573        Ok(n)
574    }
575
576    /// Sends to the connected peer, under the same guard as [`UdpSocket::send_to`].
577    ///
578    /// # Errors
579    ///
580    /// `Closed` when not connected; as [`UdpSocket::send_to`].
581    pub fn send(&mut self, buf: &[u8]) -> Result<usize, NetError> {
582        let peer = self.peer.ok_or(NetError::Closed)?;
583        let ip = normalize(peer.ip());
584        if !self.guard.may_send(ip, buf.len(), Instant::now()) {
585            self.stats.refused += 1;
586            return Err(NetError::Limit {
587                which: LimitKind::Amplification,
588            });
589        }
590        let n = self
591            .inner
592            .send(buf)
593            .map_err(|e| NetError::from_os("send", e))?;
594        self.guard.sent(ip, n);
595        self.stats.sent += 1;
596        Ok(n)
597    }
598
599    /// Fixes the peer: the kernel drops datagrams from other sources, and
600    /// [`UdpSocket::send`] goes to it.
601    ///
602    /// # Errors
603    ///
604    /// `Os`.
605    pub fn connect(&mut self, to: SocketAddr) -> Result<(), NetError> {
606        self.inner
607            .connect(self.wire(to))
608            .map_err(|e| NetError::from_os("connect", e))?;
609        self.peer = Some(to);
610        Ok(())
611    }
612
613    /// The protocol proved `peer` can receive (a cookie, a handshake): the guard
614    /// no longer limits sends to it, and its table entry is kept.
615    pub fn mark_verified(&mut self, peer: SocketAddr) {
616        self.guard.verify(normalize(peer.ip()), Instant::now());
617    }
618
619    /// How long a receive waits (`None`: forever, the default).
620    ///
621    /// # Errors
622    ///
623    /// `InvalidConfig` for a zero duration, `Os`.
624    pub fn set_read_timeout(&mut self, timeout: Option<Duration>) -> Result<(), NetError> {
625        if timeout == Some(Duration::ZERO) {
626            return Err(NetError::InvalidConfig {
627                field: "read_timeout",
628                reason: ConfigReason::Zero,
629            });
630        }
631        self.inner
632            .set_read_timeout(timeout)
633            .map_err(|e| NetError::from_os("setsockopt", e))?;
634        self.read_timeout = timeout;
635        Ok(())
636    }
637
638    /// The bound address.
639    ///
640    /// # Errors
641    ///
642    /// `Os` when `getsockname` fails.
643    pub fn local_addr(&self) -> Result<SocketAddr, NetError> {
644        self.inner
645            .local_addr()
646            .map_err(|e| NetError::from_os("getsockname", e))
647    }
648
649    /// Counters since the socket was bound.
650    #[must_use]
651    pub fn stats(&self) -> UdpStats {
652        UdpStats {
653            sources: self.guard.slots.len() as u32,
654            ..self.stats
655        }
656    }
657
658    /// An IPv4 destination on an IPv6 socket goes out as its IPv4-mapped form.
659    fn wire(&self, to: SocketAddr) -> SocketAddr {
660        match to {
661            SocketAddr::V4(a) if self.v6 => {
662                SocketAddr::V6(SocketAddrV6::new(a.ip().to_ipv6_mapped(), a.port(), 0, 0))
663            }
664            other => other,
665        }
666    }
667}
668
669fn past(deadline: Option<Instant>) -> bool {
670    deadline.is_some_and(|d| Instant::now() >= d)
671}
672
673#[cfg(unix)]
674impl std::os::fd::AsFd for UdpSocket {
675    fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> {
676        self.inner.as_fd()
677    }
678}
679
680#[cfg(windows)]
681impl std::os::windows::io::AsSocket for UdpSocket {
682    fn as_socket(&self) -> std::os::windows::io::BorrowedSocket<'_> {
683        self.inner.as_socket()
684    }
685}
686
687#[cfg(test)]
688mod tests {
689    use super::*;
690
691    fn ip(n: u8) -> IpAddr {
692        IpAddr::from([10, 0, 0, n])
693    }
694
695    #[test]
696    fn test_bucket_refills_at_its_rate() {
697        let b = UdpBudget {
698            datagrams_per_sec: 10,
699            bytes_per_sec: 1_000,
700            burst: 2,
701        };
702        let t = Instant::now();
703        let mut bucket = Bucket::full(&b, t);
704        assert!(bucket.take(&b, 100, t));
705        assert!(bucket.take(&b, 100, t));
706        assert!(!bucket.take(&b, 100, t), "burst used up");
707        // 100 ms refills one datagram.
708        assert!(bucket.take(&b, 100, t + Duration::from_millis(100)));
709        // Bytes: the bucket holds one second; 1,001 bytes never pass.
710        assert!(!bucket.take(&b, 1_001, t + Duration::from_secs(5)));
711    }
712
713    #[test]
714    fn test_amplification_factor_one_until_verified() {
715        // Edge case 94: a 1-byte request, a 2-byte reply refused until verified.
716        let t = Instant::now();
717        let mut g = Guard::new(UdpOptions::new(), t);
718        let mut o = false;
719        assert!(!g.may_send(ip(1), 1, t), "never heard from");
720        assert!(g.may_send(ip(1), 0, t));
721        assert!(g.admit(ip(1), 1, t, &mut o));
722        assert!(!g.may_send(ip(1), 2, t));
723        assert!(g.may_send(ip(1), 1, t));
724        g.sent(ip(1), 1);
725        assert!(!g.may_send(ip(1), 1, t), "the allowance is spent");
726        g.verify(ip(1), t);
727        assert!(g.may_send(ip(1), 10_000, t));
728    }
729
730    #[test]
731    fn test_amplification_factor_three_and_the_window() {
732        let t = Instant::now();
733        let w = Duration::from_secs(10);
734        let mut g = Guard::new(UdpOptions::new().amplification_factor(3), t);
735        let mut o = false;
736        assert!(g.admit(ip(1), 100, t, &mut o));
737        assert!(g.may_send(ip(1), 300, t));
738        assert!(!g.may_send(ip(1), 301, t));
739        // Just after the window edge the previous window still counts.
740        assert!(g.may_send(ip(1), 300, t + w + Duration::from_millis(1)));
741        // Two windows later it is gone.
742        assert!(!g.may_send(ip(1), 1, t + 3 * w));
743    }
744
745    #[test]
746    fn test_source_budget_drops_and_others_pass() {
747        let t = Instant::now();
748        let budget = UdpBudget {
749            datagrams_per_sec: 1,
750            bytes_per_sec: 10_000,
751            burst: 3,
752        };
753        let mut g = Guard::new(UdpOptions::new().source_budget(budget), t);
754        let mut o = false;
755        let passed = (0..10).filter(|_| g.admit(ip(1), 10, t, &mut o)).count();
756        assert_eq!(passed, 3);
757        assert!(
758            g.admit(ip(2), 10, t, &mut o),
759            "another source has its own budget"
760        );
761        assert!(!o);
762    }
763
764    #[test]
765    fn test_global_budget_caps_every_source() {
766        let t = Instant::now();
767        let global = UdpBudget {
768            datagrams_per_sec: 1,
769            bytes_per_sec: 10_000,
770            burst: 4,
771        };
772        let mut g = Guard::new(UdpOptions::new().global_budget(global), t);
773        let mut o = false;
774        let passed = (1..=10).filter(|&n| g.admit(ip(n), 10, t, &mut o)).count();
775        assert_eq!(passed, 4);
776    }
777
778    #[test]
779    fn test_full_table_evicts_strangers_and_keeps_verified() {
780        // Edge case 93.
781        let t = Instant::now();
782        let mut g = Guard::new(UdpOptions::new().source_table(4), t);
783        let mut o = false;
784        for n in 1..=4 {
785            assert!(g.admit(ip(n), 10, t, &mut o));
786        }
787        g.verify(ip(1), t);
788        g.verify(ip(2), t);
789        // Recently used entries survive one pass of the hand; the next newcomer
790        // takes an unverified one.
791        assert!(g.admit(ip(5), 10, t, &mut o));
792        assert!(!o, "an unverified entry was evicted");
793        assert!(g.index.contains_key(&ip(1)) && g.index.contains_key(&ip(2)));
794        assert_eq!(g.slots.len(), 4);
795        // Fill with verified peers: strangers share the overflow budget.
796        for n in [3, 4, 5] {
797            if g.index.contains_key(&ip(n)) {
798                g.verify(ip(n), t);
799            }
800        }
801        for n in 6..=7 {
802            g.verify(ip(n), t);
803        }
804        let mut o = false;
805        assert!(g.admit(ip(9), 10, t, &mut o));
806        assert!(o, "no room: the overflow budget");
807        assert!(
808            !g.may_send(ip(9), 1, t),
809            "an overflow stranger gets no allowance"
810        );
811        assert!(
812            g.index.contains_key(&ip(1)),
813            "verified peers keep their entries"
814        );
815    }
816
817    #[test]
818    fn test_options_validate() {
819        let bad = |o: UdpOptions| matches!(o.validate(), Err(NetError::InvalidConfig { .. }));
820        assert!(bad(UdpOptions::new().amplification_factor(0)));
821        assert!(bad(UdpOptions::new().amplification_factor(4)));
822        assert!(bad(
823            UdpOptions::new().amplification_window(Duration::from_millis(500))
824        ));
825        assert!(bad(UdpOptions::new().source_table(0)));
826        assert!(bad(UdpOptions::new().source_budget(UdpBudget {
827            datagrams_per_sec: 0,
828            bytes_per_sec: 1,
829            burst: 1
830        })));
831        assert!(UdpOptions::new().validate().is_ok());
832        assert_eq!(UdpOptions::default(), UdpOptions::new());
833    }
834
835    #[test]
836    fn test_echo_truncation_and_zero_length() {
837        let mut s = UdpSocket::bind("127.0.0.1:0").unwrap();
838        s.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
839        let to = s.local_addr().unwrap();
840        let c = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
841        c.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
842        c.send_to(b"hello", to).unwrap();
843        let mut buf = [0u8; 64];
844        let d = s.recv_from(&mut buf).unwrap();
845        assert_eq!(
846            (d.len, d.truncated, d.from),
847            (5, false, c.local_addr().unwrap())
848        );
849        assert_eq!(s.send_to(&buf[..5], d.from).unwrap(), 5);
850        let mut back = [0u8; 8];
851        assert_eq!(c.recv(&mut back).unwrap(), 5);
852        assert!(matches!(
853            s.send_to(b"x", d.from),
854            Err(NetError::Limit {
855                which: LimitKind::Amplification
856            })
857        ));
858        // Edge cases 90 and 91.
859        c.send_to(&[7u8; 100], to).unwrap();
860        let mut small = [0u8; 10];
861        let d = s.recv_from(&mut small).unwrap();
862        assert!(d.truncated);
863        c.send_to(&[], to).unwrap();
864        let d = s.recv_from(&mut buf).unwrap();
865        assert_eq!((d.len, d.truncated), (0, false));
866        let st = s.stats();
867        assert_eq!(
868            (st.received, st.sent, st.refused, st.truncated),
869            (3, 1, 1, 1)
870        );
871        assert_eq!(st.sources, 1);
872    }
873
874    #[test]
875    fn test_icmp_reports_never_end_the_socket() {
876        // Edge case 92: an injected "connection reset" and a real port-unreachable.
877        use crate::sys::shim::{Fault, Op, clear_here, program_here};
878        let mut s = UdpSocket::bind("127.0.0.1:0").unwrap();
879        s.set_read_timeout(Some(Duration::from_millis(300)))
880            .unwrap();
881        let closed = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
882        let gone = closed.local_addr().unwrap();
883        drop(closed);
884        s.connect(gone).unwrap();
885        s.mark_verified(gone);
886        s.send(b"anyone?").unwrap();
887        program_here(Op::Recv, 0, 1, Fault::Kind(io::ErrorKind::ConnectionReset));
888        let mut buf = [0u8; 16];
889        assert!(matches!(
890            s.recv(&mut buf),
891            Err(NetError::TimedOut {
892                kind: TimeoutKind::Read
893            })
894        ));
895        clear_here();
896        assert!(s.stats().icmp_errors >= 1);
897        // Still usable.
898        let mut t = UdpSocket::bind("127.0.0.1:0").unwrap();
899        t.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
900        let c = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
901        c.send_to(b"ok", t.local_addr().unwrap()).unwrap();
902        assert_eq!(t.recv_from(&mut buf).unwrap().len, 2);
903    }
904
905    #[test]
906    fn test_dual_stack_replies_to_ipv4_sources() {
907        let Ok(mut s) = UdpSocket::bind_with("[::]:0", UdpOptions::new().ipv6_only(false)) else {
908            return; // no IPv6 on this host
909        };
910        s.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
911        let port = s.local_addr().unwrap().port();
912        let c = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
913        c.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
914        c.send_to(b"v4", ("127.0.0.1", port)).unwrap();
915        let mut buf = [0u8; 8];
916        let d = s.recv_from(&mut buf).unwrap();
917        assert!(d.from.is_ipv4(), "normalized: {}", d.from);
918        s.send_to(b"ok", d.from).unwrap();
919        assert_eq!(c.recv(&mut buf).unwrap(), 2);
920    }
921}