Skip to main content

rns_net/interface/
udp.rs

1//! UDP broadcast interface.
2//!
3//! Connectionless, no HDLC framing — each UDP datagram is one packet.
4//! Matches Python `UDPInterface` from `UDPInterface.py`.
5
6use std::io::{self};
7use std::net::{SocketAddr, UdpSocket};
8use std::os::unix::io::AsRawFd;
9use std::sync::{Arc, Mutex};
10use std::thread;
11use std::time::Duration;
12
13use rns_core::transport::types::InterfaceId;
14
15use crate::event::{Event, EventSender, LoopContext, LoopSource, SourceStatus};
16use crate::interface::{lock_or_recover, ListenerControl, Writer};
17
18/// Configuration for a UDP interface.
19#[derive(Debug, Clone)]
20pub struct UdpConfig {
21    pub name: String,
22    pub listen_ip: Option<String>,
23    pub listen_port: Option<u16>,
24    pub forward_ip: Option<String>,
25    pub forward_port: Option<u16>,
26    pub interface_id: InterfaceId,
27    pub runtime: Arc<Mutex<UdpRuntime>>,
28}
29
30#[derive(Debug, Clone)]
31pub struct UdpRuntime {
32    pub forward_ip: Option<String>,
33    pub forward_port: Option<u16>,
34}
35
36impl UdpRuntime {
37    pub fn from_config(config: &UdpConfig) -> Self {
38        Self {
39            forward_ip: config.forward_ip.clone(),
40            forward_port: config.forward_port,
41        }
42    }
43}
44
45#[derive(Debug, Clone)]
46pub struct UdpRuntimeConfigHandle {
47    pub interface_name: String,
48    pub runtime: Arc<Mutex<UdpRuntime>>,
49    pub startup: UdpRuntime,
50}
51
52impl Default for UdpConfig {
53    fn default() -> Self {
54        let mut config = UdpConfig {
55            name: String::new(),
56            listen_ip: None,
57            listen_port: None,
58            forward_ip: None,
59            forward_port: None,
60            interface_id: InterfaceId(0),
61            runtime: Arc::new(Mutex::new(UdpRuntime {
62                forward_ip: None,
63                forward_port: None,
64            })),
65        };
66        let startup = UdpRuntime::from_config(&config);
67        config.runtime = Arc::new(Mutex::new(startup));
68        config
69    }
70}
71
72/// Datagrams read per wake-up by a reader on the driver loop, so that one
73/// busy interface cannot hold the loop; the rest are read on the next pass.
74const LOOP_READ_BUDGET: usize = 64;
75
76/// The largest datagram read, as the reader thread reads.
77const MAX_DATAGRAM: usize = 2048;
78
79/// Writer that sends raw data via UDP to a target address.
80struct UdpWriter {
81    socket: UdpSocket,
82    runtime: Arc<Mutex<UdpRuntime>>,
83    /// Called by the driver directly, on a non-blocking socket.
84    inline: bool,
85}
86
87impl UdpWriter {
88    fn target(&self) -> io::Result<SocketAddr> {
89        let runtime = lock_or_recover(&self.runtime, "udp runtime");
90        match (runtime.forward_ip.as_deref(), runtime.forward_port) {
91            (Some(ip), Some(port)) => ip
92                .parse::<std::net::IpAddr>()
93                .map(|ip| SocketAddr::new(ip, port))
94                .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e)),
95            _ => Err(io::Error::other(
96                "UDP interface has no forward target configured",
97            )),
98        }
99    }
100
101    fn send(&mut self, data: &[u8]) -> io::Result<()> {
102        let target = self.target()?;
103        self.socket.send_to(data, target)?;
104        Ok(())
105    }
106}
107
108impl Writer for UdpWriter {
109    fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
110        match self.send(data) {
111            // The socket's buffer is full: the datagram is dropped, as the
112            // network may drop it, rather than blocking the driver.
113            Err(e) if self.inline && e.kind() == io::ErrorKind::WouldBlock => {
114                log::debug!("UDP send buffer full, datagram dropped");
115                Ok(())
116            }
117            result => result,
118        }
119    }
120
121    fn send_frame_confirmed(&mut self, data: &[u8]) -> io::Result<()> {
122        self.send(data)
123    }
124
125    fn enqueue_confirmed(
126        &mut self,
127        data: &[u8],
128        completion: &mut Option<crate::link_send::Completion>,
129    ) -> io::Result<()> {
130        // A full buffer returns WouldBlock with the completion untouched,
131        // and the driver retries the frame.
132        self.send(data)?;
133        if let Some(completion) = completion.take() {
134            completion.finish(Ok(()));
135        }
136        Ok(())
137    }
138
139    fn sends_inline(&self) -> bool {
140        self.inline
141    }
142}
143
144/// Start a UDP interface. Spawns a reader thread if listen_ip/port are set.
145/// Returns a writer if forward_ip/port are set.
146pub fn start(config: UdpConfig, tx: EventSender) -> io::Result<Option<Box<dyn Writer>>> {
147    start_with_mark(config, tx, None).map(|(writer, _)| writer)
148}
149
150fn start_with_mark(
151    config: UdpConfig,
152    tx: EventSender,
153    underlay_mark: Option<u32>,
154) -> io::Result<(Option<Box<dyn Writer>>, ListenerControl)> {
155    let id = config.interface_id;
156    let control = ListenerControl::new();
157    {
158        let startup = UdpRuntime::from_config(&config);
159        *lock_or_recover(&config.runtime, "udp runtime") = startup;
160    }
161    let inline = crate::engine::FOLD_IO;
162    let send_socket = UdpSocket::bind("0.0.0.0:0")?;
163    super::apply_underlay_mark(send_socket.as_raw_fd(), underlay_mark)?;
164    send_socket.set_broadcast(true)?;
165    send_socket.set_nonblocking(inline)?;
166    let writer: Option<Box<dyn Writer>> = Some(Box::new(UdpWriter {
167        socket: send_socket,
168        runtime: Arc::clone(&config.runtime),
169        inline,
170    }));
171
172    // Create reader socket if listen params are set
173    if let (Some(ref bind_ip), Some(bind_port)) = (&config.listen_ip, config.listen_port) {
174        let bind_addr = format!("{}:{}", bind_ip, bind_port);
175        let recv_socket = UdpSocket::bind(&bind_addr)?;
176        super::apply_underlay_mark(recv_socket.as_raw_fd(), underlay_mark)?;
177        log::info!("[{}] UDP listening on {}", config.name, bind_addr);
178
179        // Signal interface is up
180        let _ = tx.send(Event::InterfaceUp(id, None, None));
181
182        if inline {
183            recv_socket.set_nonblocking(true)?;
184            let source = UdpSource {
185                socket: recv_socket,
186                id,
187                name: config.name.clone(),
188                tx: tx.clone(),
189                control: control.clone(),
190            };
191            tx.register_source(
192                crate::interface::loop_writer::read_key(id),
193                Box::new(source),
194            )?;
195            return Ok((writer, control));
196        }
197        recv_socket.set_read_timeout(Some(Duration::from_millis(200)))?;
198
199        let name = config.name.clone();
200        let reader_control = control.clone();
201        thread::Builder::new()
202            .name(format!("udp-reader-{}", id.0))
203            .spawn(move || {
204                udp_reader_loop(recv_socket, id, name, tx, reader_control);
205            })?;
206    }
207
208    Ok((writer, control))
209}
210
211/// Reader thread: receives UDP datagrams and sends them as frames.
212fn udp_reader_loop(
213    socket: UdpSocket,
214    id: InterfaceId,
215    name: String,
216    tx: EventSender,
217    control: ListenerControl,
218) {
219    let mut buf = [0u8; 2048];
220
221    loop {
222        if control.should_stop() {
223            return;
224        }
225        match socket.recv_from(&mut buf) {
226            Ok((n, _src)) => {
227                if n == 0 {
228                    continue;
229                }
230                if tx
231                    .send(Event::Frame {
232                        interface_id: id,
233                        data: buf[..n].into(),
234                        rssi: None,
235                        snr: None,
236                    })
237                    .is_err()
238                {
239                    // Driver shut down
240                    return;
241                }
242            }
243            Err(e)
244                if matches!(
245                    e.kind(),
246                    io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut
247                ) =>
248            {
249                continue;
250            }
251            Err(e) => {
252                if control.should_stop() {
253                    return;
254                }
255                log::warn!("[{}] recv error: {}", name, e);
256                let _ = tx.send(Event::InterfaceDown(id));
257                return;
258            }
259        }
260    }
261}
262
263/// The listening socket, read on the driver loop (see
264/// [`crate::engine::FOLD_IO`]).
265struct UdpSource {
266    socket: UdpSocket,
267    id: InterfaceId,
268    name: String,
269    tx: EventSender,
270    control: ListenerControl,
271}
272
273impl LoopSource for UdpSource {
274    fn fd(&self) -> std::os::fd::RawFd {
275        self.socket.as_raw_fd()
276    }
277
278    fn stopped(&self) -> bool {
279        self.control.should_stop()
280    }
281
282    fn on_ready(&mut self, cx: &mut LoopContext<'_>) -> SourceStatus {
283        if self.control.should_stop() {
284            return SourceStatus::Remove;
285        }
286        if !self.tx.inbound_data_has_room() {
287            return SourceStatus::Suspend;
288        }
289        let buf = &mut cx.buf[..MAX_DATAGRAM];
290        let mut frames = Vec::new();
291        let mut status = SourceStatus::Rearm;
292        for _ in 0..LOOP_READ_BUDGET {
293            match self.socket.recv_from(buf) {
294                Ok((0, _)) => {}
295                Ok((n, _)) => frames.push(Event::Frame {
296                    interface_id: self.id,
297                    data: buf[..n].into(),
298                    rssi: None,
299                    snr: None,
300                }),
301                Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
302                Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
303                Err(e) => {
304                    log::warn!("[{}] recv error: {}", self.name, e);
305                    frames.push(Event::InterfaceDown(self.id));
306                    status = SourceStatus::Remove;
307                    break;
308                }
309            }
310        }
311        match self.tx.push_from_loop(frames) {
312            Ok(true) => status,
313            Ok(false) if status == SourceStatus::Rearm => SourceStatus::Suspend,
314            Ok(false) => status,
315            Err(_) => SourceStatus::Remove,
316        }
317    }
318}
319
320// --- Factory implementation ---
321
322use super::{InterfaceConfigData, InterfaceFactory, StartContext, StartResult};
323use rns_core::transport::types::InterfaceInfo;
324use std::collections::HashMap;
325
326/// Factory for `UDPInterface`.
327pub struct UdpFactory;
328
329impl InterfaceFactory for UdpFactory {
330    fn type_name(&self) -> &str {
331        "UDPInterface"
332    }
333
334    fn parse_config(
335        &self,
336        name: &str,
337        id: InterfaceId,
338        params: &HashMap<String, String>,
339    ) -> Result<Box<dyn InterfaceConfigData>, String> {
340        let listen_ip = params.get("listen_ip").cloned();
341
342        // 'port' is a shorthand that sets both listen_port and forward_port
343        let port_shorthand: Option<u16> = params.get("port").and_then(|v| v.parse().ok());
344
345        let listen_port: Option<u16> = params
346            .get("listen_port")
347            .and_then(|v| v.parse().ok())
348            .or(port_shorthand);
349
350        let forward_ip = params.get("forward_ip").cloned();
351
352        let forward_port: Option<u16> = params
353            .get("forward_port")
354            .and_then(|v| v.parse().ok())
355            .or(port_shorthand);
356
357        let mut config = UdpConfig {
358            name: name.to_string(),
359            listen_ip,
360            listen_port,
361            forward_ip,
362            forward_port,
363            interface_id: id,
364            runtime: Arc::new(Mutex::new(UdpRuntime {
365                forward_ip: None,
366                forward_port: None,
367            })),
368        };
369        let startup = UdpRuntime::from_config(&config);
370        config.runtime = Arc::new(Mutex::new(startup));
371        Ok(Box::new(config))
372    }
373
374    fn start(
375        &self,
376        config: Box<dyn InterfaceConfigData>,
377        ctx: StartContext,
378    ) -> io::Result<StartResult> {
379        let udp_config = *config
380            .into_any()
381            .downcast::<UdpConfig>()
382            .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "wrong config type"))?;
383
384        let id = udp_config.interface_id;
385        let name = udp_config.name.clone();
386        let out_capable = udp_config.forward_ip.is_some();
387        let in_capable = udp_config.listen_ip.is_some();
388
389        let info = InterfaceInfo {
390            stamp_costs: Default::default(),
391            id,
392            name,
393            mode: ctx.mode,
394            gravity: ctx.gravity,
395            recursive_prs: ctx.recursive_prs,
396            announces_from_internal: ctx.announces_from_internal,
397            announces_to_internal: ctx.announces_to_internal,
398            out_capable,
399            in_capable,
400            bitrate: Some(10_000_000),
401            airtime_profile: None,
402            announce_rate_target: None,
403            announce_rate_grace: 0,
404            announce_rate_penalty: 0.0,
405            announce_cap: rns_core::constants::ANNOUNCE_CAP,
406            is_local_client: false,
407            wants_tunnel: false,
408            tunnel_id: None,
409            mtu: 1400,
410            ingress_control: rns_core::transport::types::IngressControlConfig::enabled(),
411            ia_freq: 0.0,
412            ip_freq: 0.0,
413            op_freq: 0.0,
414            op_samples: 0,
415            started: crate::time::now(),
416        };
417
418        let (maybe_writer, control) = start_with_mark(udp_config, ctx.tx, ctx.underlay_mark)?;
419
420        let writer: Box<dyn Writer> = maybe_writer
421            .ok_or_else(|| io::Error::other("UDPInterface did not provide a writer"))?;
422
423        Ok(StartResult::Simple {
424            control: Some(control),
425            id,
426            info,
427            writer,
428            interface_type_name: "UDPInterface".to_string(),
429        })
430    }
431}
432
433pub(crate) fn udp_runtime_handle_from_config(config: &UdpConfig) -> UdpRuntimeConfigHandle {
434    UdpRuntimeConfigHandle {
435        interface_name: config.name.clone(),
436        runtime: Arc::clone(&config.runtime),
437        startup: UdpRuntime::from_config(config),
438    }
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444
445    #[test]
446    fn detach_stops_idle_udp_listener() {
447        let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
448        socket
449            .set_read_timeout(Some(Duration::from_millis(200)))
450            .unwrap();
451        let (tx, rx) = crate::event::channel();
452        let control = ListenerControl::new();
453        let reader_control = control.clone();
454
455        let reader = thread::spawn(move || {
456            udp_reader_loop(
457                socket,
458                InterfaceId(76),
459                "detach-test".into(),
460                tx,
461                reader_control,
462            )
463        });
464        thread::sleep(Duration::from_millis(50));
465        control.request_stop();
466        reader.join().unwrap();
467        assert!(rx.try_recv().is_err());
468    }
469    use std::net::UdpSocket;
470    use std::time::Duration;
471
472    fn find_free_port() -> u16 {
473        crate::test_support::port()
474    }
475
476    #[test]
477    fn bind_and_receive() {
478        let port = find_free_port();
479        let (tx, rx) = crate::event::channel();
480
481        let config = UdpConfig {
482            name: "test-udp".into(),
483            listen_ip: Some("127.0.0.1".into()),
484            listen_port: Some(port),
485            forward_ip: None,
486            forward_port: None,
487            interface_id: InterfaceId(10),
488            ..UdpConfig::default()
489        };
490
491        let _writer = start(config, tx).unwrap();
492
493        // Drain InterfaceUp
494        let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
495
496        // Send a UDP packet to the listener
497        let sender = UdpSocket::bind("127.0.0.1:0").unwrap();
498        let payload = b"hello udp";
499        sender
500            .send_to(payload, format!("127.0.0.1:{}", port))
501            .unwrap();
502
503        // Should receive Frame event
504        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
505        match event {
506            Event::Frame {
507                interface_id,
508                data,
509                rssi: _,
510                snr: _,
511            } => {
512                assert_eq!(interface_id, InterfaceId(10));
513                assert_eq!(&*data, &payload[..]);
514            }
515            other => panic!("expected Frame, got {:?}", other),
516        }
517    }
518
519    #[test]
520    fn send_broadcast() {
521        let recv_port = find_free_port();
522        let (tx, _rx) = crate::event::channel();
523
524        let config = UdpConfig {
525            name: "test-udp-send".into(),
526            listen_ip: None,
527            listen_port: None,
528            forward_ip: Some("127.0.0.1".into()),
529            forward_port: Some(recv_port),
530            interface_id: InterfaceId(11),
531            ..UdpConfig::default()
532        };
533
534        let writer = start(config, tx).unwrap();
535        let mut writer = writer.unwrap();
536
537        // Bind a receiver
538        let receiver = UdpSocket::bind(format!("127.0.0.1:{}", recv_port)).unwrap();
539        receiver
540            .set_read_timeout(Some(Duration::from_secs(2)))
541            .unwrap();
542
543        // Send via writer
544        let payload = b"broadcast data";
545        writer.send_frame(payload).unwrap();
546
547        // Receive on the other socket
548        let mut buf = [0u8; 256];
549        let (n, _) = receiver.recv_from(&mut buf).unwrap();
550        assert_eq!(&buf[..n], payload);
551    }
552
553    #[test]
554    fn round_trip() {
555        let listen_port = find_free_port();
556        let forward_port = find_free_port();
557        let (tx, rx) = crate::event::channel();
558
559        let config = UdpConfig {
560            name: "test-udp-rt".into(),
561            listen_ip: Some("127.0.0.1".into()),
562            listen_port: Some(listen_port),
563            forward_ip: Some("127.0.0.1".into()),
564            forward_port: Some(forward_port),
565            interface_id: InterfaceId(12),
566            ..UdpConfig::default()
567        };
568
569        let writer = start(config, tx).unwrap();
570        assert!(writer.is_some());
571
572        // Drain InterfaceUp
573        let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
574
575        // Send to the listener
576        let sender = UdpSocket::bind("127.0.0.1:0").unwrap();
577        sender
578            .send_to(b"ping", format!("127.0.0.1:{}", listen_port))
579            .unwrap();
580
581        let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
582        match event {
583            Event::Frame { data, .. } => assert_eq!(&*data, b"ping"),
584            other => panic!("expected Frame, got {:?}", other),
585        }
586    }
587
588    #[test]
589    fn multiple_datagrams() {
590        let port = find_free_port();
591        let (tx, rx) = crate::event::channel();
592
593        let config = UdpConfig {
594            name: "test-udp-multi".into(),
595            listen_ip: Some("127.0.0.1".into()),
596            listen_port: Some(port),
597            forward_ip: None,
598            forward_port: None,
599            interface_id: InterfaceId(13),
600            ..UdpConfig::default()
601        };
602
603        let _writer = start(config, tx).unwrap();
604
605        // Drain InterfaceUp
606        let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
607
608        let sender = UdpSocket::bind("127.0.0.1:0").unwrap();
609        for i in 0..5u8 {
610            sender.send_to(&[i], format!("127.0.0.1:{}", port)).unwrap();
611        }
612
613        for i in 0..5u8 {
614            let event = rx.recv_timeout(Duration::from_secs(2)).unwrap();
615            match event {
616                Event::Frame { data, .. } => assert_eq!(data, vec![i].into()),
617                other => panic!("expected Frame, got {:?}", other),
618            }
619        }
620    }
621
622    #[test]
623    fn empty_datagram_is_ignored_before_transport_ingress() {
624        let port = find_free_port();
625        let (tx, rx) = crate::event::channel();
626        let config = UdpConfig {
627            name: "test-udp-empty".into(),
628            listen_ip: Some("127.0.0.1".into()),
629            listen_port: Some(port),
630            interface_id: InterfaceId(14),
631            ..UdpConfig::default()
632        };
633        let _writer = start(config, tx).unwrap();
634        let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
635
636        let sender = UdpSocket::bind("127.0.0.1:0").unwrap();
637        let target = format!("127.0.0.1:{port}");
638        sender.send_to(&[], &target).unwrap();
639        sender.send_to(b"after-empty", &target).unwrap();
640
641        match rx.recv_timeout(Duration::from_secs(2)).unwrap() {
642            Event::Frame { data, .. } => assert_eq!(&*data, b"after-empty"),
643            other => panic!("expected non-empty Frame, got {other:?}"),
644        }
645    }
646
647    #[test]
648    fn writer_send_to() {
649        let recv_port = find_free_port();
650
651        // Bind receiver first
652        let receiver = UdpSocket::bind(format!("127.0.0.1:{}", recv_port)).unwrap();
653        receiver
654            .set_read_timeout(Some(Duration::from_secs(2)))
655            .unwrap();
656
657        // Create writer directly
658        let send_socket = UdpSocket::bind("127.0.0.1:0").unwrap();
659        send_socket.set_broadcast(true).unwrap();
660        let mut writer = UdpWriter {
661            socket: send_socket,
662            runtime: Arc::new(Mutex::new(UdpRuntime {
663                forward_ip: Some("127.0.0.1".into()),
664                forward_port: Some(recv_port),
665            })),
666            inline: false,
667        };
668
669        let payload = vec![0xAA, 0xBB, 0xCC];
670        writer.send_frame(&payload).unwrap();
671
672        let mut buf = [0u8; 256];
673        let (n, _) = receiver.recv_from(&mut buf).unwrap();
674        assert_eq!(&buf[..n], &payload);
675    }
676
677    fn thread_named(name: &str) -> bool {
678        std::fs::read_dir("/proc/self/task").unwrap().any(|task| {
679            let comm = task.unwrap().path().join("comm");
680            std::fs::read_to_string(comm).is_ok_and(|comm| comm.trim() == name)
681        })
682    }
683
684    #[test]
685    fn folded_udp_interface_runs_on_the_driver_loop() {
686        if !crate::engine::FOLD_IO {
687            return;
688        }
689        let listen_port = find_free_port();
690        let receiver = UdpSocket::bind("127.0.0.1:0").unwrap();
691        receiver
692            .set_read_timeout(Some(Duration::from_secs(2)))
693            .unwrap();
694        let (tx, rx) = crate::event::channel();
695        let config = UdpConfig {
696            name: "test-udp-folded".into(),
697            listen_ip: Some("127.0.0.1".into()),
698            listen_port: Some(listen_port),
699            forward_ip: Some("127.0.0.1".into()),
700            forward_port: Some(receiver.local_addr().unwrap().port()),
701            interface_id: InterfaceId(77),
702            ..UdpConfig::default()
703        };
704        let writer = start(config, tx.clone()).unwrap().unwrap();
705        assert!(writer.sends_inline());
706        let (mut writer, _) =
707            crate::interface::wrap_async_writer(writer, InterfaceId(77), "folded", tx, 16);
708        assert!(!thread_named("udp-reader-77"));
709        assert!(!thread_named("iface-writer-77"));
710
711        // Inbound, read by the receiver's wait.
712        let _ = rx.recv_timeout(Duration::from_secs(1)).unwrap();
713        let sender = UdpSocket::bind("127.0.0.1:0").unwrap();
714        sender
715            .send_to(b"in", format!("127.0.0.1:{listen_port}"))
716            .unwrap();
717        match rx.recv_timeout(Duration::from_secs(2)).unwrap() {
718            Event::Frame { data, .. } => assert_eq!(&*data, b"in"),
719            other => panic!("expected Frame, got {other:?}"),
720        }
721
722        // Outbound, plain and confirmed.
723        let mut buf = [0u8; 64];
724        writer.send_frame(b"plain").unwrap();
725        let (n, _) = receiver.recv_from(&mut buf).unwrap();
726        assert_eq!(&buf[..n], b"plain");
727
728        let pool = crate::link_send::SendPool::new(1);
729        let (completion, receipt) = crate::link_send::Completion::new(pool.try_acquire().unwrap());
730        let mut completion = Some(completion);
731        writer
732            .enqueue_confirmed(b"confirmed", &mut completion)
733            .unwrap();
734        assert!(completion.is_none());
735        receipt.wait().unwrap();
736        let (n, _) = receiver.recv_from(&mut buf).unwrap();
737        assert_eq!(&buf[..n], b"confirmed");
738    }
739
740    #[test]
741    fn writer_sends_to_an_ipv6_target() {
742        let receiver = UdpSocket::bind("[::1]:0").unwrap();
743        receiver
744            .set_read_timeout(Some(Duration::from_secs(2)))
745            .unwrap();
746        let mut writer = UdpWriter {
747            socket: UdpSocket::bind("[::1]:0").unwrap(),
748            runtime: Arc::new(Mutex::new(UdpRuntime {
749                forward_ip: Some("::1".into()),
750                forward_port: Some(receiver.local_addr().unwrap().port()),
751            })),
752            inline: false,
753        };
754        writer.send_frame(b"v6").unwrap();
755        let mut buf = [0u8; 16];
756        let (n, _) = receiver.recv_from(&mut buf).unwrap();
757        assert_eq!(&buf[..n], b"v6");
758    }
759}