1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub struct UdpBudget {
27 pub datagrams_per_sec: u32,
29 pub bytes_per_sec: u32,
31 pub burst: u32,
33}
34
35impl UdpBudget {
36 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#[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 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 pub fn amplification_factor(mut self, factor: u8) -> UdpOptions {
88 self.factor = factor;
89 self
90 }
91
92 pub fn amplification_window(mut self, window: Duration) -> UdpOptions {
96 self.window = window;
97 self
98 }
99
100 pub fn source_budget(mut self, budget: UdpBudget) -> UdpOptions {
102 self.source = budget;
103 self
104 }
105
106 pub fn global_budget(mut self, budget: UdpBudget) -> UdpOptions {
108 self.global = Some(budget);
109 self
110 }
111
112 pub fn source_table(mut self, entries: u32) -> UdpOptions {
114 self.table = entries;
115 self
116 }
117
118 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
160pub struct Datagram {
161 pub len: usize,
164 pub from: SocketAddr,
166 pub truncated: bool,
168 pub destination: Option<IpAddr>,
170}
171
172#[non_exhaustive]
180#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
181pub struct UdpStats {
182 pub received: u64,
184 pub sent: u64,
186 pub dropped: u64,
188 pub refused: u64,
190 pub truncated: u64,
192 pub icmp_errors: u64,
194 pub overflow: u64,
196 pub sources: u32,
198}
199
200#[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 fn take(&mut self, budget: &UdpBudget, len: usize, now: Instant) -> bool {
221 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#[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 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
283const SWEEP: usize = 64;
286
287#[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 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 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 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#[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 pub fn bind<A: ToSocketAddrs>(addr: A) -> Result<UdpSocket, NetError> {
441 UdpSocket::bind_with(addr, UdpOptions::new())
442 }
443
444 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 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 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 pub fn recv(&mut self, buf: &mut [u8]) -> Result<Datagram, NetError> {
548 self.recv_from(buf)
549 }
550
551 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 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 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 pub fn mark_verified(&mut self, peer: SocketAddr) {
616 self.guard.verify(normalize(peer.ip()), Instant::now());
617 }
618
619 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 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 #[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 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 assert!(bucket.take(&b, 100, t + Duration::from_millis(100)));
709 assert!(!bucket.take(&b, 1_001, t + Duration::from_secs(5)));
711 }
712
713 #[test]
714 fn test_amplification_factor_one_until_verified() {
715 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 assert!(g.may_send(ip(1), 300, t + w + Duration::from_millis(1)));
741 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 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 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 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 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 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 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; };
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}