Skip to main content

rns_net/holepunch/
udp_direct.rs

1//! Point-to-point UDP direct interface.
2//!
3//! After hole punching succeeds, this module wraps the punched UDP socket
4//! as a Reticulum interface (implementing Writer + reader thread).
5//! No HDLC framing needed — one datagram = one packet (same as UdpInterface).
6
7use std::io;
8use std::net::{SocketAddr, UdpSocket};
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::sync::Arc;
11use std::thread;
12use std::time::Duration;
13
14use rns_core::transport::types::{InterfaceId, InterfaceInfo};
15
16use crate::event::{Event, EventSender, LoopContext, LoopSource, SourceStatus};
17use crate::interface::Writer;
18
19use super::puncher;
20
21/// Keepalive interval for maintaining NAT mappings.
22const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(30);
23
24/// Timeout for considering the direct connection dead.
25const INACTIVITY_TIMEOUT: Duration = Duration::from_secs(120);
26
27/// Writer for a direct UDP peer-to-peer connection.
28///
29/// When dropped, signals the reader thread to stop.
30pub struct UdpDirectWriter {
31    socket: Arc<UdpSocket>,
32    peer_addr: SocketAddr,
33    running: Arc<AtomicBool>,
34    /// Called by the driver directly on a non-blocking socket, with
35    /// interface I/O folded (see [`crate::engine::FOLD_IO`]).
36    inline: bool,
37}
38
39impl Writer for UdpDirectWriter {
40    fn send_frame(&mut self, data: &[u8]) -> io::Result<()> {
41        match self.socket.send_to(data, self.peer_addr) {
42            // A datagram the socket buffer cannot take is dropped, as the
43            // network may drop it.
44            Err(e) if self.inline && e.kind() == io::ErrorKind::WouldBlock => Ok(()),
45            result => result.map(|_| ()),
46        }
47    }
48
49    fn send_frame_confirmed(&mut self, data: &[u8]) -> io::Result<()> {
50        self.socket.send_to(data, self.peer_addr).map(|_| ())
51    }
52
53    fn enqueue_confirmed(
54        &mut self,
55        data: &[u8],
56        completion: &mut Option<crate::link_send::Completion>,
57    ) -> io::Result<()> {
58        // A full buffer returns WouldBlock with the completion untouched,
59        // and the driver retries the frame.
60        self.socket.send_to(data, self.peer_addr)?;
61        if let Some(completion) = completion.take() {
62            completion.finish(Ok(()));
63        }
64        Ok(())
65    }
66
67    fn sends_inline(&self) -> bool {
68        self.inline
69    }
70}
71
72impl Drop for UdpDirectWriter {
73    fn drop(&mut self) {
74        self.running.store(false, Ordering::Relaxed);
75    }
76}
77
78/// Start a direct UDP interface from a punched socket.
79///
80/// Returns `(interface_id, writer, InterfaceInfo)` and spawns a reader thread.
81/// The reader thread sends `Event::Frame` for incoming packets and
82/// `Event::InterfaceDown` if the connection times out.
83pub fn start_direct_interface(
84    socket: UdpSocket,
85    peer_addr: SocketAddr,
86    interface_id: InterfaceId,
87    session_id: [u8; 16],
88    punch_token: [u8; 32],
89    tx: EventSender,
90) -> io::Result<(Box<dyn Writer>, InterfaceInfo)> {
91    let socket = Arc::new(socket);
92    let inline = crate::engine::FOLD_IO;
93    if inline {
94        socket.set_nonblocking(true)?;
95    } else {
96        socket.set_read_timeout(Some(Duration::from_secs(1)))?;
97    }
98
99    let running = Arc::new(AtomicBool::new(true));
100
101    let writer = UdpDirectWriter {
102        socket: socket.clone(),
103        peer_addr,
104        running: running.clone(),
105        inline,
106    };
107
108    let name = format!(
109        "DirectPeer/{:02x}{:02x}{:02x}{:02x}",
110        session_id[0], session_id[1], session_id[2], session_id[3]
111    );
112
113    let info = InterfaceInfo {
114        stamp_costs: Default::default(),
115        id: interface_id,
116        name: name.clone(),
117        mode: rns_core::constants::MODE_FULL,
118        gravity: 0,
119        recursive_prs: false,
120        announces_from_internal: true,
121        announces_to_internal: None,
122        out_capable: true,
123        in_capable: true,
124        bitrate: None,
125        airtime_profile: None,
126        announce_rate_target: None,
127        announce_rate_grace: 0,
128        announce_rate_penalty: 0.0,
129        announce_cap: 1.0,
130        is_local_client: false,
131        wants_tunnel: false,
132        tunnel_id: None,
133        mtu: 1400,
134        ia_freq: 0.0,
135        ip_freq: 0.0,
136        op_freq: 0.0,
137        op_samples: 0,
138        started: 0.0,
139        ingress_control: rns_core::transport::types::IngressControlConfig::disabled(),
140    };
141
142    if inline {
143        let now = std::time::Instant::now();
144        tx.register_source(
145            crate::interface::loop_writer::read_key(interface_id),
146            Box::new(DirectSource {
147                socket,
148                peer_addr,
149                interface_id,
150                keepalive: puncher::build_keepalive_packet(&session_id, &punch_token),
151                last_inbound: now,
152                last_keepalive: now,
153                tx: tx.clone(),
154                running,
155            }),
156        )?;
157        return Ok((Box::new(writer), info));
158    }
159
160    let running_clone = running.clone();
161
162    // Spawn reader + keepalive thread
163    thread::Builder::new()
164        .name(format!("direct-udp-{}", &name))
165        .spawn(move || {
166            run_reader(
167                socket,
168                peer_addr,
169                interface_id,
170                session_id,
171                punch_token,
172                tx,
173                running_clone,
174            );
175        })?;
176
177    Ok((Box::new(writer), info))
178}
179
180fn run_reader(
181    socket: Arc<UdpSocket>,
182    peer_addr: SocketAddr,
183    interface_id: InterfaceId,
184    session_id: [u8; 16],
185    punch_token: [u8; 32],
186    tx: EventSender,
187    running: Arc<AtomicBool>,
188) {
189    let mut buf = [0u8; 2048];
190    let mut last_inbound = std::time::Instant::now();
191    let mut last_keepalive = std::time::Instant::now();
192    let keepalive_pkt = puncher::build_keepalive_packet(&session_id, &punch_token);
193
194    while running.load(Ordering::Relaxed) {
195        // Send keepalive if due
196        if last_keepalive.elapsed() >= KEEPALIVE_INTERVAL {
197            let _ = socket.send_to(&keepalive_pkt, peer_addr);
198            last_keepalive = std::time::Instant::now();
199        }
200
201        // Check inactivity timeout
202        if last_inbound.elapsed() >= INACTIVITY_TIMEOUT {
203            log::info!("[{}] Direct UDP interface timed out", interface_id.0);
204            let _ = tx.send(Event::InterfaceDown(interface_id));
205            break;
206        }
207
208        match socket.recv_from(&mut buf) {
209            Ok((len, src)) => {
210                // Only accept packets from our verified peer
211                if src != peer_addr {
212                    continue;
213                }
214                last_inbound = std::time::Instant::now();
215
216                // Skip keepalive/punch packets (they have RNSH/RNSA magic)
217                if len >= 4 && (buf[..4] == *b"RNSH" || buf[..4] == *b"RNSA") {
218                    continue;
219                }
220
221                // Deliver as Reticulum frame
222                if len > 0 {
223                    let _ = tx.send(Event::Frame {
224                        interface_id,
225                        data: buf[..len].into(),
226                        rssi: None,
227                        snr: None,
228                    });
229                }
230            }
231            Err(ref e)
232                if e.kind() == io::ErrorKind::WouldBlock || e.kind() == io::ErrorKind::TimedOut =>
233            {
234                continue;
235            }
236            Err(e) => {
237                log::warn!("[{}] Direct UDP recv error: {}", interface_id.0, e);
238                let _ = tx.send(Event::InterfaceDown(interface_id));
239                break;
240            }
241        }
242    }
243}
244
245/// Datagrams read per readiness before other sources get a turn.
246const READ_BUDGET: usize = 64;
247
248/// A direct UDP link, read on the driver thread, with its keepalive and
249/// inactivity timeout on timers.
250struct DirectSource {
251    socket: Arc<UdpSocket>,
252    peer_addr: SocketAddr,
253    interface_id: InterfaceId,
254    keepalive: Vec<u8>,
255    last_inbound: std::time::Instant,
256    last_keepalive: std::time::Instant,
257    tx: EventSender,
258    /// Cleared when the writer is dropped with the interface.
259    running: Arc<AtomicBool>,
260}
261
262impl LoopSource for DirectSource {
263    fn fd(&self) -> std::os::fd::RawFd {
264        use std::os::fd::AsRawFd;
265        self.socket.as_raw_fd()
266    }
267
268    fn stopped(&self) -> bool {
269        !self.running.load(Ordering::Relaxed)
270    }
271
272    fn on_ready(&mut self, cx: &mut LoopContext<'_>) -> SourceStatus {
273        if self.stopped() {
274            return SourceStatus::Remove;
275        }
276        if !self.tx.inbound_data_has_room() {
277            return SourceStatus::Suspend;
278        }
279        let mut frames = Vec::new();
280        for _ in 0..READ_BUDGET {
281            let (len, src) = match self.socket.recv_from(cx.buf) {
282                Ok(received) => received,
283                Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
284                Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
285                Err(e) => {
286                    log::warn!("[{}] Direct UDP recv error: {}", self.interface_id.0, e);
287                    let _ = self.tx.push_from_loop(frames);
288                    let _ = self
289                        .tx
290                        .push_from_loop([Event::InterfaceDown(self.interface_id)]);
291                    return SourceStatus::Remove;
292                }
293            };
294            // Only accept packets from our verified peer
295            if src != self.peer_addr {
296                continue;
297            }
298            self.last_inbound = std::time::Instant::now();
299            // Skip keepalive/punch packets (they have RNSH/RNSA magic)
300            if len >= 4 && (cx.buf[..4] == *b"RNSH" || cx.buf[..4] == *b"RNSA") {
301                continue;
302            }
303            if len > 0 {
304                frames.push(Event::Frame {
305                    interface_id: self.interface_id,
306                    data: cx.buf[..len].into(),
307                    rssi: None,
308                    snr: None,
309                });
310            }
311        }
312        match self.tx.push_from_loop(frames) {
313            Ok(true) => SourceStatus::Rearm,
314            Ok(false) => SourceStatus::Suspend,
315            Err(_) => SourceStatus::Remove,
316        }
317    }
318
319    fn deadline(&self) -> Option<std::time::Instant> {
320        Some((self.last_keepalive + KEEPALIVE_INTERVAL).min(self.last_inbound + INACTIVITY_TIMEOUT))
321    }
322
323    fn on_timer(&mut self, _cx: &mut LoopContext<'_>) -> SourceStatus {
324        if self.stopped() {
325            return SourceStatus::Remove;
326        }
327        let now = std::time::Instant::now();
328        if now >= self.last_inbound + INACTIVITY_TIMEOUT {
329            log::info!("[{}] Direct UDP interface timed out", self.interface_id.0);
330            let _ = self
331                .tx
332                .push_from_loop([Event::InterfaceDown(self.interface_id)]);
333            return SourceStatus::Remove;
334        }
335        if now >= self.last_keepalive + KEEPALIVE_INTERVAL {
336            let _ = self.socket.send_to(&self.keepalive, self.peer_addr);
337            self.last_keepalive = now;
338        }
339        SourceStatus::Rearm
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346
347    #[test]
348    fn test_udp_direct_writer() {
349        // Create a pair of sockets
350        let sock_a = UdpSocket::bind("127.0.0.1:0").unwrap();
351        let sock_b = UdpSocket::bind("127.0.0.1:0").unwrap();
352        let addr_b = sock_b.local_addr().unwrap();
353        sock_b
354            .set_read_timeout(Some(Duration::from_secs(1)))
355            .unwrap();
356
357        let mut writer = UdpDirectWriter {
358            socket: Arc::new(sock_a),
359            peer_addr: addr_b,
360            running: Arc::new(AtomicBool::new(true)),
361            inline: false,
362        };
363
364        // Send a frame
365        let data = b"hello direct peer";
366        writer.send_frame(data).unwrap();
367
368        // Receive on other end
369        let mut buf = [0u8; 64];
370        let (len, _src) = sock_b.recv_from(&mut buf).unwrap();
371        assert_eq!(&buf[..len], data);
372    }
373
374    #[test]
375    fn folded_direct_link_runs_on_the_driver_loop() {
376        if !crate::engine::FOLD_IO {
377            return;
378        }
379        let local = UdpSocket::bind("127.0.0.1:0").unwrap();
380        let peer = UdpSocket::bind("127.0.0.1:0").unwrap();
381        let peer_addr = peer.local_addr().unwrap();
382        let local_addr = local.local_addr().unwrap();
383        let (tx, rx) = crate::event::channel();
384        let (writer, _info) = start_direct_interface(
385            local,
386            peer_addr,
387            InterfaceId(910),
388            [1; 16],
389            [2; 32],
390            tx.clone(),
391        )
392        .unwrap();
393        assert!(writer.sends_inline());
394        // Served on the driver loop: no reader thread.
395        assert!(!std::fs::read_dir("/proc/self/task").unwrap().any(|task| {
396            std::fs::read_to_string(task.unwrap().path().join("comm"))
397                .is_ok_and(|comm| comm.starts_with("direct-udp-"))
398        }));
399        let (mut writer, _) =
400            crate::interface::wrap_async_writer(writer, InterfaceId(910), "direct", tx, 16);
401
402        // Inbound from the peer; keepalives and strangers are skipped.
403        let stranger = UdpSocket::bind("127.0.0.1:0").unwrap();
404        stranger.send_to(b"not from the peer", local_addr).unwrap();
405        peer.send_to(b"RNSH keepalive", local_addr).unwrap();
406        peer.send_to(b"from the peer", local_addr).unwrap();
407        match rx.recv_timeout(Duration::from_secs(2)).unwrap() {
408            Event::Frame {
409                interface_id, data, ..
410            } => {
411                assert_eq!(interface_id, InterfaceId(910));
412                assert_eq!(&*data, b"from the peer");
413            }
414            other => panic!("expected Frame, got {other:?}"),
415        }
416        assert!(rx.recv_timeout(Duration::from_millis(100)).is_err());
417
418        // Outbound, sent by the driver directly.
419        writer.send_frame(b"to the peer").unwrap();
420        let mut buf = [0u8; 64];
421        peer.set_read_timeout(Some(Duration::from_secs(1))).unwrap();
422        let (n, _) = peer.recv_from(&mut buf).unwrap();
423        assert_eq!(&buf[..n], b"to the peer");
424    }
425}