Skip to main content

seyd_nat/
lib.rs

1// Copyright 2026 Anton Gravestam
2// SPDX-License-Identifier: Apache-2.0
3//! Reachability for the Seyd agent.
4//!
5//! The pilot is always the initiator, so Seyd's traversal problem is the
6//! narrow one: **make the agent reachable as a server**. In descending order
7//! of reliability that is an installed router port mapping, global IPv6, a
8//! STUN-reflexive address plus hole punching, and a manually forwarded port.
9//! Port-restricted and symmetric NAT are unreachable by construction while the
10//! pilot is a browser (PROTOTYPE.md, "NAT traversal — why this is not ICE").
11//! There is no relay: what this crate cannot make reachable is reported in
12//! [`NatReport`] so the pilot can tell the operator how to fix the network.
13//!
14//! Everything runs on the UDP sockets the QUIC server will later own
15//! ([`bind_sockets`]) — STUN in particular has to see the same NAT mapping
16//! QUIC will use, so the sockets are bound once and never rebound.
17
18pub mod addr;
19pub mod gather;
20pub mod ifaces;
21pub mod portmap;
22pub mod socket;
23pub mod stun;
24
25pub use addr::{format_host, is_global};
26pub use gather::{gather, Candidate, GatherOpts, Gathered, Hint, NatReport};
27pub use portmap::{map_port, renew, MapProtocol, PortMapping};
28pub use socket::bind_sockets;
29pub use stun::{discover, NatDiscovery, NatType};
30
31/// Network-change notifications.
32pub struct NetworkWatch {
33    /// One message per detected change; the payload is a human-readable reason.
34    pub events: tokio::sync::mpsc::Receiver<String>,
35    /// Inject a synthetic change (e.g. after repeated port-mapping renewal
36    /// failures) without waiting for the poller.
37    pub force: tokio::sync::mpsc::Sender<String>,
38}
39
40/// Watch for interface-address changes by polling [`ifaces::interfaces`]
41/// every 10 s. Polling rather than netlink/`SCDynamicStore` keeps it portable;
42/// a 10 s detection delay is negligible against QUIC's idle timeout. Callers
43/// treat a message as "re-run [`gather()`] on the same sockets".
44pub fn watch_network_changes() -> NetworkWatch {
45    watch_network_changes_with(ifaces::interfaces, std::time::Duration::from_secs(10))
46}
47
48/// Test seam: same watcher with an injected address provider and period.
49pub fn watch_network_changes_with<F>(mut provider: F, period: std::time::Duration) -> NetworkWatch
50where
51    F: FnMut() -> Vec<std::net::IpAddr> + Send + 'static,
52{
53    let (ev_tx, events) = tokio::sync::mpsc::channel(4);
54    let (force, mut force_rx) = tokio::sync::mpsc::channel::<String>(4);
55    // A clone lives inside the task so `force_rx.recv()` can never yield
56    // `None` and busy-loop the select.
57    let keep = force.clone();
58    tokio::spawn(async move {
59        let _keep = keep;
60        let mut prev: std::collections::BTreeSet<std::net::IpAddr> =
61            provider().into_iter().collect();
62        let mut tick = tokio::time::interval(period);
63        tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
64        tick.tick().await; // the immediate first tick is the baseline, not a change
65        loop {
66            tokio::select! {
67                _ = tick.tick() => {
68                    let now: std::collections::BTreeSet<std::net::IpAddr> =
69                        provider().into_iter().collect();
70                    if now != prev {
71                        let added = now.difference(&prev).count();
72                        let removed = prev.difference(&now).count();
73                        prev = now;
74                        // try_send: a slow consumer coalesces changes instead
75                        // of queueing a burst of stale ones.
76                        let _ = ev_tx.try_send(format!(
77                            "interface addresses changed (+{added} -{removed})"
78                        ));
79                    }
80                }
81                reason = force_rx.recv() => {
82                    if let Some(reason) = reason {
83                        let _ = ev_tx.try_send(reason);
84                    }
85                }
86            }
87        }
88    });
89    NetworkWatch { events, force }
90}
91
92#[cfg(test)]
93mod watch_tests {
94    use std::net::IpAddr;
95    use std::sync::{Arc, Mutex};
96    use std::time::Duration;
97
98    #[tokio::test]
99    async fn fires_on_address_change_and_force_only() {
100        let addrs: Arc<Mutex<Vec<IpAddr>>> =
101            Arc::new(Mutex::new(vec!["192.168.1.2".parse().unwrap()]));
102        let p = addrs.clone();
103        let mut w = super::watch_network_changes_with(
104            move || p.lock().unwrap().clone(),
105            Duration::from_millis(20),
106        );
107        // Stable set: no event.
108        assert!(
109            tokio::time::timeout(Duration::from_millis(120), w.events.recv())
110                .await
111                .is_err(),
112            "no event expected while addresses are stable"
113        );
114        // Change the set: exactly one event.
115        addrs.lock().unwrap().push("10.0.0.7".parse().unwrap());
116        let reason = tokio::time::timeout(Duration::from_millis(500), w.events.recv())
117            .await
118            .expect("event expected after change")
119            .unwrap();
120        assert!(reason.contains("+1"), "{reason}");
121        // Forced event passes through.
122        w.force.send("renewal failed".into()).await.unwrap();
123        let reason = tokio::time::timeout(Duration::from_millis(500), w.events.recv())
124            .await
125            .expect("forced event expected")
126            .unwrap();
127        assert_eq!(reason, "renewal failed");
128    }
129}