use std::collections::HashMap;
use std::io;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV6, ToSocketAddrs};
use std::time::{Duration, Instant};
use crate::addr::normalize;
use crate::error::{ConfigReason, LimitKind, NetError, TimeoutKind};
use crate::sys::{self, AsSock};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct UdpBudget {
pub datagrams_per_sec: u32,
pub bytes_per_sec: u32,
pub burst: u32,
}
impl UdpBudget {
pub const SOURCE_DEFAULT: UdpBudget = UdpBudget {
datagrams_per_sec: 1_000,
bytes_per_sec: 1_000_000,
burst: 100,
};
}
#[must_use]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UdpOptions {
factor: u8,
window: Duration,
source: UdpBudget,
global: Option<UdpBudget>,
table: u32,
ipv6_only: bool,
packet_info: bool,
}
impl Default for UdpOptions {
fn default() -> UdpOptions {
UdpOptions::new()
}
}
impl UdpOptions {
pub fn new() -> UdpOptions {
UdpOptions {
factor: 1,
window: Duration::from_secs(10),
source: UdpBudget::SOURCE_DEFAULT,
global: None,
table: 65_536,
ipv6_only: true,
packet_info: false,
}
}
pub fn amplification_factor(mut self, factor: u8) -> UdpOptions {
self.factor = factor;
self
}
pub fn amplification_window(mut self, window: Duration) -> UdpOptions {
self.window = window;
self
}
pub fn source_budget(mut self, budget: UdpBudget) -> UdpOptions {
self.source = budget;
self
}
pub fn global_budget(mut self, budget: UdpBudget) -> UdpOptions {
self.global = Some(budget);
self
}
pub fn source_table(mut self, entries: u32) -> UdpOptions {
self.table = entries;
self
}
pub fn packet_info(mut self, on: bool) -> UdpOptions {
self.packet_info = on;
self
}
pub fn ipv6_only(mut self, on: bool) -> UdpOptions {
self.ipv6_only = on;
self
}
fn validate(&self) -> Result<(), NetError> {
let invalid = |field, reason| Err(NetError::InvalidConfig { field, reason });
if self.factor == 0 {
return invalid("amplification_factor", ConfigReason::Zero);
}
if self.factor > 3 {
return invalid("amplification_factor", ConfigReason::TooLarge { max: 3 });
}
if self.window < Duration::from_secs(1) {
return invalid("amplification_window", ConfigReason::TooSmall { min: 1 });
}
if self.table == 0 {
return invalid("source_table", ConfigReason::Zero);
}
for (field, b) in [
("source_budget", Some(self.source)),
("global_budget", self.global),
] {
if let Some(b) = b {
if b.datagrams_per_sec == 0 || b.bytes_per_sec == 0 || b.burst == 0 {
return invalid(field, ConfigReason::Zero);
}
}
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Datagram {
pub len: usize,
pub from: SocketAddr,
pub truncated: bool,
pub destination: Option<IpAddr>,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct UdpStats {
pub received: u64,
pub sent: u64,
pub dropped: u64,
pub refused: u64,
pub truncated: u64,
pub icmp_errors: u64,
pub overflow: u64,
pub sources: u32,
}
#[derive(Clone, Copy, Debug)]
struct Bucket {
datagrams: u64,
bytes: u64,
at: Instant,
}
const MICRO: u64 = 1_000_000;
impl Bucket {
fn full(budget: &UdpBudget, now: Instant) -> Bucket {
Bucket {
datagrams: u64::from(budget.burst) * MICRO,
bytes: u64::from(budget.bytes_per_sec) * MICRO,
at: now,
}
}
fn take(&mut self, budget: &UdpBudget, len: usize, now: Instant) -> bool {
let us = now
.saturating_duration_since(self.at)
.as_micros()
.min(1_000_000_000) as u64;
self.at = now;
let cap_d = u64::from(budget.burst) * MICRO;
let cap_b = u64::from(budget.bytes_per_sec) * MICRO;
self.datagrams = (self.datagrams + us * u64::from(budget.datagrams_per_sec)).min(cap_d);
self.bytes = (self.bytes + us * u64::from(budget.bytes_per_sec)).min(cap_b);
let need_b = (len as u64).saturating_mul(MICRO);
if self.datagrams >= MICRO && self.bytes >= need_b {
self.datagrams -= MICRO;
self.bytes -= need_b;
true
} else {
false
}
}
}
#[derive(Clone, Copy, Debug)]
struct Entry {
ip: IpAddr,
bucket: Bucket,
received: [u64; 2],
sent: [u64; 2],
window_at: Instant,
verified: bool,
referenced: bool,
}
impl Entry {
fn new(ip: IpAddr, budget: &UdpBudget, now: Instant) -> Entry {
Entry {
ip,
bucket: Bucket::full(budget, now),
received: [0; 2],
sent: [0; 2],
window_at: now,
verified: false,
referenced: true,
}
}
fn roll(&mut self, window: Duration, now: Instant) {
let gone = now.saturating_duration_since(self.window_at);
if gone >= window * 2 {
self.received = [0; 2];
self.sent = [0; 2];
self.window_at = now;
} else if gone >= window {
self.received = [0, self.received[0]];
self.sent = [0, self.sent[0]];
self.window_at += window;
}
}
}
const SWEEP: usize = 64;
#[derive(Debug)]
struct Guard {
index: HashMap<IpAddr, u32>,
slots: Vec<Entry>,
hand: usize,
overflow: Bucket,
global: Option<Bucket>,
options: UdpOptions,
}
impl Guard {
fn new(options: UdpOptions, now: Instant) -> Guard {
Guard {
index: HashMap::new(),
slots: Vec::new(),
hand: 0,
overflow: Bucket::full(&options.source, now),
global: options.global.map(|g| Bucket::full(&g, now)),
options,
}
}
fn find(&mut self, ip: IpAddr) -> Option<usize> {
let i = *self.index.get(&ip)? as usize;
self.slots[i].referenced = true;
Some(i)
}
fn slot(&mut self, ip: IpAddr, now: Instant, force: bool) -> Option<usize> {
if let Some(i) = self.find(ip) {
return Some(i);
}
let entry = Entry::new(ip, &self.options.source, now);
if self.slots.len() < self.options.table as usize {
self.slots.push(entry);
let i = self.slots.len() - 1;
self.index.insert(ip, i as u32);
return Some(i);
}
let n = self.slots.len();
let looks = if force { 2 * n } else { SWEEP.min(2 * n) };
for _ in 0..looks {
let i = self.hand;
self.hand = (self.hand + 1) % n;
let e = &mut self.slots[i];
if e.verified {
continue;
}
if e.referenced {
e.referenced = false;
continue;
}
self.index.remove(&e.ip);
*e = entry;
self.index.insert(ip, i as u32);
return Some(i);
}
None
}
fn admit(&mut self, ip: IpAddr, len: usize, now: Instant, overflow: &mut bool) -> bool {
let source = self.options.source;
let ok = match self.slot(ip, now, false) {
Some(i) => {
let window = self.options.window;
let e = &mut self.slots[i];
if e.bucket.take(&source, len, now) {
e.roll(window, now);
e.received[0] += len as u64;
true
} else {
false
}
}
None => {
*overflow = true;
self.overflow.take(&source, len, now)
}
};
if !ok {
return false;
}
match (&mut self.global, &self.options.global) {
(Some(bucket), Some(budget)) => bucket.take(budget, len, now),
_ => true,
}
}
fn may_send(&mut self, ip: IpAddr, len: usize, now: Instant) -> bool {
let Some(i) = self.find(ip) else {
return len == 0;
};
let window = self.options.window;
let factor = u64::from(self.options.factor);
let e = &mut self.slots[i];
if e.verified {
return true;
}
e.roll(window, now);
let allowed = factor * (e.received[0] + e.received[1]);
e.sent[0] + e.sent[1] + len as u64 <= allowed
}
fn sent(&mut self, ip: IpAddr, len: usize) {
if let Some(&i) = self.index.get(&ip) {
self.slots[i as usize].sent[0] += len as u64;
}
}
fn verify(&mut self, ip: IpAddr, now: Instant) {
if let Some(i) = self.slot(ip, now, true) {
self.slots[i].verified = true;
}
}
}
#[derive(Debug)]
pub struct UdpSocket {
inner: std::net::UdpSocket,
guard: Guard,
read_timeout: Option<Duration>,
peer: Option<SocketAddr>,
v6: bool,
#[cfg_attr(windows, allow(dead_code))]
packet_info: bool,
stats: UdpStats,
}
#[derive(Debug)]
pub struct DatagramBuffer<'a> {
pub buf: &'a mut [u8],
pub datagram: Option<Datagram>,
}
impl<'a> DatagramBuffer<'a> {
#[must_use]
pub fn new(buf: &'a mut [u8]) -> DatagramBuffer<'a> {
DatagramBuffer {
buf,
datagram: None,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct OutgoingDatagram<'a> {
pub bytes: &'a [u8],
pub to: SocketAddr,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MulticastInterface {
Any,
V4(Ipv4Addr),
V6Index(u32),
}
impl UdpSocket {
pub fn bind<A: ToSocketAddrs>(addr: A) -> Result<UdpSocket, NetError> {
UdpSocket::bind_with(addr, UdpOptions::new())
}
pub fn bind_with<A: ToSocketAddrs>(
addr: A,
options: UdpOptions,
) -> Result<UdpSocket, NetError> {
options.validate()?;
let addrs: Vec<SocketAddr> = addr
.to_socket_addrs()
.map_err(|e| NetError::Os {
op: "resolve",
source: e,
})?
.collect();
let mut last = NetError::NoAddresses;
if options.packet_info && !cfg!(unix) {
return Err(NetError::Unsupported {
feature: "UDP packet info",
platform: std::env::consts::OS,
});
}
let packet_info = options.packet_info;
for a in addrs {
match sys::udp_bind(a, options.ipv6_only) {
Ok(inner) => {
#[cfg(unix)]
if packet_info {
sys::udp_packet_info(inner.sock(), a.is_ipv6())
.map_err(|e| NetError::from_os("setsockopt", e))?;
}
return Ok(UdpSocket {
inner,
guard: Guard::new(options, Instant::now()),
read_timeout: None,
peer: None,
v6: a.is_ipv6(),
packet_info,
stats: UdpStats::default(),
});
}
Err(e) => last = crate::tcp::bind_error(a, e),
}
}
Err(last)
}
pub fn recv_from(&mut self, buf: &mut [u8]) -> Result<Datagram, NetError> {
let deadline = self.read_timeout.map(|t| Instant::now() + t);
loop {
let (len, from, truncated, destination) = match self.recv_one(buf) {
Ok(got) => got,
Err(e) => match e.kind() {
io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut => {
return Err(NetError::TimedOut {
kind: TimeoutKind::Read,
});
}
io::ErrorKind::Interrupted => continue,
io::ErrorKind::ConnectionRefused | io::ErrorKind::ConnectionReset => {
self.stats.icmp_errors += 1;
if past(deadline) {
return Err(NetError::TimedOut {
kind: TimeoutKind::Read,
});
}
continue;
}
_ => return Err(NetError::from_os("recvfrom", e)),
},
};
let from = crate::addr::normalize_socket(from);
if !self.admit(from, len, truncated) {
if past(deadline) {
return Err(NetError::TimedOut {
kind: TimeoutKind::Read,
});
}
continue;
}
return Ok(Datagram {
len,
from,
truncated,
destination,
});
}
}
fn recv_one(
&mut self,
buf: &mut [u8],
) -> io::Result<(usize, SocketAddr, bool, Option<IpAddr>)> {
#[cfg(unix)]
if self.packet_info {
return sys::udp_recv_with_destination(self.inner.sock(), buf);
}
let (len, from, truncated) = sys::udp_recv_from(self.inner.sock(), buf)?;
Ok((len, from, truncated, None))
}
fn admit(&mut self, from: SocketAddr, len: usize, truncated: bool) -> bool {
let mut overflow = false;
let admitted = self
.guard
.admit(from.ip(), len, Instant::now(), &mut overflow);
if overflow {
self.stats.overflow += 1;
}
if !admitted {
self.stats.dropped += 1;
return false;
}
self.stats.received += 1;
if truncated {
self.stats.truncated += 1;
}
true
}
pub fn recv_batch(&mut self, slots: &mut [DatagramBuffer<'_>]) -> Result<usize, NetError> {
for slot in slots.iter_mut() {
slot.datagram = None;
}
if slots.is_empty() {
return Ok(0);
}
self.recv_many(slots)
}
#[cfg(target_os = "linux")]
fn recv_many(&mut self, slots: &mut [DatagramBuffer<'_>]) -> Result<usize, NetError> {
let count = slots.len().min(sys::BATCH_MAX);
let mut got = [None; sys::BATCH_MAX];
let n = loop {
let received = {
let mut bufs: [&mut [u8]; sys::BATCH_MAX] = std::array::from_fn(|_| &mut [][..]);
for (b, slot) in bufs.iter_mut().zip(slots.iter_mut()).take(count) {
*b = &mut *slot.buf;
}
sys::udp_recv_many(self.inner.sock(), &mut bufs[..count], &mut got)
};
match received {
Ok(n) => break n,
Err(e) => match e.kind() {
io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut => {
return Err(NetError::TimedOut {
kind: TimeoutKind::Read,
});
}
io::ErrorKind::Interrupted => {}
io::ErrorKind::ConnectionRefused | io::ErrorKind::ConnectionReset => {
self.stats.icmp_errors += 1;
}
_ => return Err(NetError::from_os("recvmmsg", e)),
},
}
};
let mut filled = 0;
for (slot, r) in slots.iter_mut().zip(got.iter()).take(n) {
let Some(r) = r else { continue };
let from = crate::addr::normalize_socket(r.from);
if self.admit(from, r.len, r.truncated) {
slot.datagram = Some(Datagram {
len: r.len,
from,
truncated: r.truncated,
destination: None,
});
filled += 1;
}
}
Ok(filled)
}
#[cfg(not(target_os = "linux"))]
fn recv_many(&mut self, slots: &mut [DatagramBuffer<'_>]) -> Result<usize, NetError> {
let (first, rest) = match slots.split_first_mut() {
Some(parts) => parts,
None => return Ok(0),
};
first.datagram = Some(self.recv_from(first.buf)?);
let mut filled = 1;
self.inner
.set_nonblocking(true)
.map_err(|e| NetError::from_os("ioctl", e))?;
for slot in rest.iter_mut().take(63) {
match self.recv_from(slot.buf) {
Ok(d) => {
slot.datagram = Some(d);
filled += 1;
}
Err(_) => break,
}
}
self.inner
.set_nonblocking(false)
.map_err(|e| NetError::from_os("ioctl", e))?;
Ok(filled)
}
pub fn send_batch(&mut self, datagrams: &[OutgoingDatagram<'_>]) -> Result<usize, NetError> {
let mut sent = 0;
while sent < datagrams.len() {
match self.send_some(&datagrams[sent..]) {
Ok(0) => break,
Ok(n) => sent += n,
Err(e) if sent == 0 => return Err(e),
Err(_) => break,
}
}
Ok(sent)
}
fn charge(&mut self, datagrams: &[OutgoingDatagram<'_>]) -> Result<usize, NetError> {
let now = Instant::now();
let mut allowed = 0;
for d in datagrams.iter().take(64) {
let ip = normalize(d.to.ip());
if !self.guard.may_send(ip, d.bytes.len(), now) {
self.stats.refused += 1;
break;
}
self.guard.sent(ip, d.bytes.len());
allowed += 1;
}
if allowed == 0 && !datagrams.is_empty() {
return Err(NetError::Limit {
which: LimitKind::Amplification,
});
}
Ok(allowed)
}
#[cfg(target_os = "linux")]
fn send_some(&mut self, datagrams: &[OutgoingDatagram<'_>]) -> Result<usize, NetError> {
let allowed = self.charge(datagrams)?;
let mut wire = [(&[][..], SocketAddr::from(([0, 0, 0, 0], 0))); sys::BATCH_MAX];
for (w, d) in wire.iter_mut().zip(datagrams.iter().take(allowed)) {
*w = (d.bytes, self.wire(d.to));
}
let n = sys::udp_send_many(self.inner.sock(), &wire[..allowed])
.map_err(|e| NetError::from_os("sendmmsg", e))?;
self.stats.sent += n as u64;
Ok(n)
}
#[cfg(not(target_os = "linux"))]
fn send_some(&mut self, datagrams: &[OutgoingDatagram<'_>]) -> Result<usize, NetError> {
let allowed = self.charge(datagrams)?;
let mut n = 0;
for d in &datagrams[..allowed] {
let wire = self.wire(d.to);
if let Err(e) = self.inner.send_to(d.bytes, wire) {
if n == 0 {
return Err(NetError::from_os("sendto", e));
}
break;
}
self.stats.sent += 1;
n += 1;
}
Ok(n)
}
pub fn join_multicast(
&self,
group: IpAddr,
interface: MulticastInterface,
) -> Result<(), NetError> {
let result = match (group, interface) {
(IpAddr::V4(g), MulticastInterface::Any) => {
self.inner.join_multicast_v4(&g, &Ipv4Addr::UNSPECIFIED)
}
(IpAddr::V4(g), MulticastInterface::V4(i)) => self.inner.join_multicast_v4(&g, &i),
(IpAddr::V6(g), MulticastInterface::Any) => self.inner.join_multicast_v6(&g, 0),
(IpAddr::V6(g), MulticastInterface::V6Index(i)) => self.inner.join_multicast_v6(&g, i),
_ => {
return Err(NetError::InvalidConfig {
field: "interface",
reason: ConfigReason::Inconsistent,
});
}
};
result.map_err(|e| NetError::from_os("setsockopt", e))
}
pub fn recv(&mut self, buf: &mut [u8]) -> Result<Datagram, NetError> {
self.recv_from(buf)
}
pub fn send_to(&mut self, buf: &[u8], to: SocketAddr) -> Result<usize, NetError> {
let ip = normalize(to.ip());
if !self.guard.may_send(ip, buf.len(), Instant::now()) {
self.stats.refused += 1;
return Err(NetError::Limit {
which: LimitKind::Amplification,
});
}
let wire = self.wire(to);
let n = self
.inner
.send_to(buf, wire)
.map_err(|e| NetError::from_os("sendto", e))?;
self.guard.sent(ip, n);
self.stats.sent += 1;
Ok(n)
}
pub fn send(&mut self, buf: &[u8]) -> Result<usize, NetError> {
let peer = self.peer.ok_or(NetError::Closed)?;
let ip = normalize(peer.ip());
if !self.guard.may_send(ip, buf.len(), Instant::now()) {
self.stats.refused += 1;
return Err(NetError::Limit {
which: LimitKind::Amplification,
});
}
let n = self
.inner
.send(buf)
.map_err(|e| NetError::from_os("send", e))?;
self.guard.sent(ip, n);
self.stats.sent += 1;
Ok(n)
}
pub fn connect(&mut self, to: SocketAddr) -> Result<(), NetError> {
self.inner
.connect(self.wire(to))
.map_err(|e| NetError::from_os("connect", e))?;
self.peer = Some(to);
Ok(())
}
pub fn mark_verified(&mut self, peer: SocketAddr) {
self.guard.verify(normalize(peer.ip()), Instant::now());
}
pub fn set_read_timeout(&mut self, timeout: Option<Duration>) -> Result<(), NetError> {
if timeout == Some(Duration::ZERO) {
return Err(NetError::InvalidConfig {
field: "read_timeout",
reason: ConfigReason::Zero,
});
}
self.inner
.set_read_timeout(timeout)
.map_err(|e| NetError::from_os("setsockopt", e))?;
self.read_timeout = timeout;
Ok(())
}
pub(crate) fn set_nonblocking(&self) -> Result<(), NetError> {
self.inner
.set_nonblocking(true)
.map_err(|e| NetError::from_os("ioctl", e))
}
pub(crate) fn raw_sock(&self) -> sys::Sock<'_> {
self.inner.sock()
}
pub fn local_addr(&self) -> Result<SocketAddr, NetError> {
self.inner
.local_addr()
.map_err(|e| NetError::from_os("getsockname", e))
}
#[must_use]
pub fn stats(&self) -> UdpStats {
UdpStats {
sources: self.guard.slots.len() as u32,
..self.stats
}
}
fn wire(&self, to: SocketAddr) -> SocketAddr {
match to {
SocketAddr::V4(a) if self.v6 => {
SocketAddr::V6(SocketAddrV6::new(a.ip().to_ipv6_mapped(), a.port(), 0, 0))
}
other => other,
}
}
}
pub trait UdpSession: Send + 'static {
fn on_datagram(&mut self, cx: &mut UdpContext<'_>, from: SocketAddr, bytes: &[u8]);
}
#[derive(Debug)]
pub struct UdpContext<'a> {
pub(crate) socket: &'a mut UdpSocket,
}
impl UdpContext<'_> {
pub fn send_to(&mut self, bytes: &[u8], to: SocketAddr) -> Result<usize, NetError> {
self.socket.send_to(bytes, to)
}
pub fn mark_verified(&mut self, peer: SocketAddr) {
self.socket.mark_verified(peer);
}
#[must_use]
pub fn now(&self) -> Instant {
Instant::now()
}
}
pub trait UdpSessionFactory: Send + Sync + 'static {
fn open(&self, listener: &crate::tcp::ListenerInfo) -> Box<dyn UdpSession>;
}
impl<F, S> UdpSessionFactory for F
where
F: Fn(&crate::tcp::ListenerInfo) -> S + Send + Sync + 'static,
S: UdpSession,
{
fn open(&self, listener: &crate::tcp::ListenerInfo) -> Box<dyn UdpSession> {
Box::new(self(listener))
}
}
fn past(deadline: Option<Instant>) -> bool {
deadline.is_some_and(|d| Instant::now() >= d)
}
#[cfg(unix)]
impl std::os::fd::AsFd for UdpSocket {
fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> {
self.inner.as_fd()
}
}
#[cfg(windows)]
impl std::os::windows::io::AsSocket for UdpSocket {
fn as_socket(&self) -> std::os::windows::io::BorrowedSocket<'_> {
self.inner.as_socket()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ip(n: u8) -> IpAddr {
IpAddr::from([10, 0, 0, n])
}
#[test]
fn test_bucket_refills_at_its_rate() {
let b = UdpBudget {
datagrams_per_sec: 10,
bytes_per_sec: 1_000,
burst: 2,
};
let t = Instant::now();
let mut bucket = Bucket::full(&b, t);
assert!(bucket.take(&b, 100, t));
assert!(bucket.take(&b, 100, t));
assert!(!bucket.take(&b, 100, t), "burst used up");
assert!(bucket.take(&b, 100, t + Duration::from_millis(100)));
assert!(!bucket.take(&b, 1_001, t + Duration::from_secs(5)));
}
#[test]
fn test_amplification_factor_one_until_verified() {
let t = Instant::now();
let mut g = Guard::new(UdpOptions::new(), t);
let mut o = false;
assert!(!g.may_send(ip(1), 1, t), "never heard from");
assert!(g.may_send(ip(1), 0, t));
assert!(g.admit(ip(1), 1, t, &mut o));
assert!(!g.may_send(ip(1), 2, t));
assert!(g.may_send(ip(1), 1, t));
g.sent(ip(1), 1);
assert!(!g.may_send(ip(1), 1, t), "the allowance is spent");
g.verify(ip(1), t);
assert!(g.may_send(ip(1), 10_000, t));
}
#[test]
fn test_amplification_factor_three_and_the_window() {
let t = Instant::now();
let w = Duration::from_secs(10);
let mut g = Guard::new(UdpOptions::new().amplification_factor(3), t);
let mut o = false;
assert!(g.admit(ip(1), 100, t, &mut o));
assert!(g.may_send(ip(1), 300, t));
assert!(!g.may_send(ip(1), 301, t));
assert!(g.may_send(ip(1), 300, t + w + Duration::from_millis(1)));
assert!(!g.may_send(ip(1), 1, t + 3 * w));
}
#[test]
fn test_source_budget_drops_and_others_pass() {
let t = Instant::now();
let budget = UdpBudget {
datagrams_per_sec: 1,
bytes_per_sec: 10_000,
burst: 3,
};
let mut g = Guard::new(UdpOptions::new().source_budget(budget), t);
let mut o = false;
let passed = (0..10).filter(|_| g.admit(ip(1), 10, t, &mut o)).count();
assert_eq!(passed, 3);
assert!(
g.admit(ip(2), 10, t, &mut o),
"another source has its own budget"
);
assert!(!o);
}
#[test]
fn test_global_budget_caps_every_source() {
let t = Instant::now();
let global = UdpBudget {
datagrams_per_sec: 1,
bytes_per_sec: 10_000,
burst: 4,
};
let mut g = Guard::new(UdpOptions::new().global_budget(global), t);
let mut o = false;
let passed = (1..=10).filter(|&n| g.admit(ip(n), 10, t, &mut o)).count();
assert_eq!(passed, 4);
}
#[test]
fn test_full_table_evicts_strangers_and_keeps_verified() {
let t = Instant::now();
let mut g = Guard::new(UdpOptions::new().source_table(4), t);
let mut o = false;
for n in 1..=4 {
assert!(g.admit(ip(n), 10, t, &mut o));
}
g.verify(ip(1), t);
g.verify(ip(2), t);
assert!(g.admit(ip(5), 10, t, &mut o));
assert!(!o, "an unverified entry was evicted");
assert!(g.index.contains_key(&ip(1)) && g.index.contains_key(&ip(2)));
assert_eq!(g.slots.len(), 4);
for n in [3, 4, 5] {
if g.index.contains_key(&ip(n)) {
g.verify(ip(n), t);
}
}
for n in 6..=7 {
g.verify(ip(n), t);
}
let mut o = false;
assert!(g.admit(ip(9), 10, t, &mut o));
assert!(o, "no room: the overflow budget");
assert!(
!g.may_send(ip(9), 1, t),
"an overflow stranger gets no allowance"
);
assert!(
g.index.contains_key(&ip(1)),
"verified peers keep their entries"
);
}
#[test]
fn test_options_validate() {
let bad = |o: UdpOptions| matches!(o.validate(), Err(NetError::InvalidConfig { .. }));
assert!(bad(UdpOptions::new().amplification_factor(0)));
assert!(bad(UdpOptions::new().amplification_factor(4)));
assert!(bad(
UdpOptions::new().amplification_window(Duration::from_millis(500))
));
assert!(bad(UdpOptions::new().source_table(0)));
assert!(bad(UdpOptions::new().source_budget(UdpBudget {
datagrams_per_sec: 0,
bytes_per_sec: 1,
burst: 1
})));
assert!(UdpOptions::new().validate().is_ok());
assert_eq!(UdpOptions::default(), UdpOptions::new());
}
#[test]
fn test_echo_truncation_and_zero_length() {
let mut s = UdpSocket::bind("127.0.0.1:0").unwrap();
s.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
let to = s.local_addr().unwrap();
let c = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
c.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
c.send_to(b"hello", to).unwrap();
let mut buf = [0u8; 64];
let d = s.recv_from(&mut buf).unwrap();
assert_eq!(
(d.len, d.truncated, d.from),
(5, false, c.local_addr().unwrap())
);
assert_eq!(s.send_to(&buf[..5], d.from).unwrap(), 5);
let mut back = [0u8; 8];
assert_eq!(c.recv(&mut back).unwrap(), 5);
assert!(matches!(
s.send_to(b"x", d.from),
Err(NetError::Limit {
which: LimitKind::Amplification
})
));
c.send_to(&[7u8; 100], to).unwrap();
let mut small = [0u8; 10];
let d = s.recv_from(&mut small).unwrap();
assert!(d.truncated);
c.send_to(&[], to).unwrap();
let d = s.recv_from(&mut buf).unwrap();
assert_eq!((d.len, d.truncated), (0, false));
let st = s.stats();
assert_eq!(
(st.received, st.sent, st.refused, st.truncated),
(3, 1, 1, 1)
);
assert_eq!(st.sources, 1);
}
#[test]
fn test_icmp_reports_never_end_the_socket() {
use crate::sys::shim::{Fault, Op, clear_here, program_here};
let mut s = UdpSocket::bind("127.0.0.1:0").unwrap();
s.set_read_timeout(Some(Duration::from_millis(300)))
.unwrap();
let closed = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
let gone = closed.local_addr().unwrap();
drop(closed);
s.connect(gone).unwrap();
s.mark_verified(gone);
s.send(b"anyone?").unwrap();
program_here(Op::Recv, 0, 1, Fault::Kind(io::ErrorKind::ConnectionReset));
let mut buf = [0u8; 16];
assert!(matches!(
s.recv(&mut buf),
Err(NetError::TimedOut {
kind: TimeoutKind::Read
})
));
clear_here();
assert!(s.stats().icmp_errors >= 1);
let mut t = UdpSocket::bind("127.0.0.1:0").unwrap();
t.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
let c = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
c.send_to(b"ok", t.local_addr().unwrap()).unwrap();
assert_eq!(t.recv_from(&mut buf).unwrap().len, 2);
}
#[test]
fn test_dual_stack_replies_to_ipv4_sources() {
let Ok(mut s) = UdpSocket::bind_with("[::]:0", UdpOptions::new().ipv6_only(false)) else {
return; };
s.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
let port = s.local_addr().unwrap().port();
let c = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
c.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
c.send_to(b"v4", ("127.0.0.1", port)).unwrap();
let mut buf = [0u8; 8];
let d = s.recv_from(&mut buf).unwrap();
assert!(d.from.is_ipv4(), "normalized: {}", d.from);
s.send_to(b"ok", d.from).unwrap();
assert_eq!(c.recv(&mut buf).unwrap(), 2);
}
fn verified_pair() -> (UdpSocket, UdpSocket) {
let mut a = UdpSocket::bind("127.0.0.1:0").unwrap();
let mut b = UdpSocket::bind("127.0.0.1:0").unwrap();
let (aa, ba) = (a.local_addr().unwrap(), b.local_addr().unwrap());
a.mark_verified(ba);
b.mark_verified(aa);
a.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
b.set_read_timeout(Some(Duration::from_secs(5))).unwrap();
(a, b)
}
#[test]
fn test_batches_cross_in_one_call_each() {
let (mut a, mut b) = verified_pair();
let to = b.local_addr().unwrap();
let bodies: Vec<Vec<u8>> = (0..10u8).map(|i| vec![i; 1 + usize::from(i)]).collect();
let out: Vec<_> = bodies
.iter()
.map(|b| OutgoingDatagram { bytes: b, to })
.collect();
assert_eq!(a.send_batch(&out).unwrap(), 10);
std::thread::sleep(Duration::from_millis(50));
let mut store = vec![[0u8; 64]; 16];
let mut seen = 0;
while seen < 10 {
let mut slots: Vec<_> = store.iter_mut().map(|b| DatagramBuffer::new(b)).collect();
let n = b.recv_batch(&mut slots).unwrap();
assert!(n > 0);
for slot in &slots {
if let Some(d) = slot.datagram {
assert_eq!(d.from, a.local_addr().unwrap());
assert_eq!(&slot.buf[..d.len], &bodies[seen][..]);
seen += 1;
}
}
}
assert_eq!(a.stats().sent, 10);
assert_eq!(b.stats().received, 10);
}
#[test]
fn test_batch_send_stays_under_the_guard() {
let mut server = UdpSocket::bind("127.0.0.1:0").unwrap();
let client = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
let to = client.local_addr().unwrap();
client
.send_to(&[0u8; 100], server.local_addr().unwrap())
.unwrap();
let mut buf = [0u8; 200];
server.recv_from(&mut buf).unwrap();
let big = [1u8; 40];
let out = [OutgoingDatagram { bytes: &big, to }; 4];
assert_eq!(
server.send_batch(&out).unwrap(),
2,
"100 bytes of allowance"
);
assert!(matches!(
server.send_batch(&out),
Err(NetError::Limit {
which: LimitKind::Amplification
})
));
}
#[cfg(unix)]
#[test]
fn test_packet_info_reports_the_destination() {
let mut server =
UdpSocket::bind_with("0.0.0.0:0", UdpOptions::new().packet_info(true)).unwrap();
server
.set_read_timeout(Some(Duration::from_secs(5)))
.unwrap();
let port = server.local_addr().unwrap().port();
let client = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
client.send_to(b"hi", ("127.0.0.1", port)).unwrap();
let mut buf = [0u8; 16];
let d = server.recv_from(&mut buf).unwrap();
assert_eq!(d.len, 2);
assert_eq!(d.destination, Some("127.0.0.1".parse().unwrap()));
}
#[cfg(windows)]
#[test]
fn test_packet_info_is_refused_on_windows() {
assert!(matches!(
UdpSocket::bind_with("127.0.0.1:0", UdpOptions::new().packet_info(true)),
Err(NetError::Unsupported { .. })
));
}
#[test]
fn test_multicast_join_checks_the_family() {
let socket = UdpSocket::bind("0.0.0.0:0").unwrap();
let group: IpAddr = "239.1.2.3".parse().unwrap();
assert!(matches!(
socket.join_multicast(group, MulticastInterface::V6Index(1)),
Err(NetError::InvalidConfig { .. })
));
let _ = socket.join_multicast(group, MulticastInterface::Any);
}
}