Skip to main content

seyd_transport/
prober.rs

1// Copyright 2026 Anton Gravestam
2// SPDX-License-Identifier: Apache-2.0
3//! NAT hole punching: send small UDP packets from the QUIC socket toward the
4//! pilot's address so an address-restricted NAT opens a mapping before the
5//! pilot's QUIC Initial arrives. Repeated because a lone packet can be lost and
6//! Chrome retries its handshake with backoff. One destination port: for the
7//! NAT types this helps, filtering is by source address and the port is
8//! irrelevant; where the port would matter it is unknowable anyway.
9
10use std::net::{SocketAddr, UdpSocket};
11use std::sync::Mutex;
12use std::time::Duration;
13
14pub struct Prober {
15    sock: UdpSocket,
16    task: Mutex<Option<tokio::task::JoinHandle<()>>>,
17}
18
19impl Prober {
20    pub(crate) fn new(sock: UdpSocket) -> Self {
21        Self {
22            sock,
23            task: Mutex::new(None),
24        }
25    }
26
27    /// Start probing `target`; a running probe is replaced.
28    pub fn start(&self, target: SocketAddr, interval: Duration, duration: Duration) {
29        self.stop();
30        let sock = match self.sock.try_clone() {
31            Ok(s) => s,
32            Err(e) => {
33                tracing::warn!("prober: cannot clone socket: {e}");
34                return;
35            }
36        };
37        let handle = tokio::spawn(async move {
38            let deadline = tokio::time::Instant::now() + duration;
39            let mut ticker = tokio::time::interval(interval);
40            let mut sent = 0u32;
41            while tokio::time::Instant::now() < deadline {
42                ticker.tick().await;
43                // Non-blocking socket: a would-block here just skips a tick.
44                if sock.send_to(&[0u8], target).is_ok() {
45                    sent += 1;
46                }
47            }
48            tracing::debug!(%target, sent, "prober finished");
49        });
50        *self.task.lock().unwrap() = Some(handle);
51    }
52
53    pub fn start_default(&self, target: SocketAddr) {
54        self.start(target, Duration::from_millis(250), Duration::from_secs(12));
55    }
56
57    pub fn stop(&self) {
58        if let Some(t) = self.task.lock().unwrap().take() {
59            t.abort();
60        }
61    }
62}
63
64impl Drop for Prober {
65    fn drop(&mut self) {
66        self.stop();
67    }
68}