Skip to main content

dig_nat/method/
mod.rs

1//! Traversal methods — one module per NAT-traversal technique behind a common
2//! [`TraversalMethod`] trait, plus the [`TraversalKind`] tag the strategy orders them by.
3//!
4//! Each method answers ONE question: "given this peer, can you produce a reachable socket address I
5//! can dial (and, for the relayed method, an already-open transport)?" The [`crate::strategy`]
6//! module owns the ORDER + the racing/sequencing; a method never decides it is "the one". This keeps
7//! every technique small, single-purpose, and independently testable with a mock socket / fake IGD /
8//! loopback relay.
9//!
10//! Attempt order (first success wins), from the crate `DESIGN.md`:
11//! 1. [`direct`] — peer publicly reachable / already port-forwarded
12//! 2. [`upnp`] — UPnP/IGD port mapping
13//! 3. [`natpmp`] — NAT-PMP (RFC 6886)
14//! 4. [`pcp`] — PCP (RFC 6887)
15//! 5. [`hole_punch`] — relay-coordinated simultaneous-open hole punch: the relay is used ONLY as a
16//!    signaling/rendezvous channel to exchange candidates + coordinate timing; the DATA path is
17//!    peer-to-peer DIRECT (relay carries no data → minimal relay bandwidth).
18//! 6. [`relayed`] — TURN-like relayed transport: the relay carries ALL data (highest relay
19//!    bandwidth). The genuine LAST resort, tried only after the hole punch (tier 5) fails.
20//!
21//! Tiers 5 and 6 are deliberately SEPARATE methods with separate abstractions
22//! ([`hole_punch::HolePunchCoordinator`] = signaling-only vs [`relayed::RelayedTransport`] =
23//! data-proxy) and separate [`TraversalKind`]s so observability reports exactly which succeeded and
24//! the strategy prefers the bandwidth-cheap punch before the bandwidth-heavy TURN.
25
26pub mod direct;
27pub mod hole_punch;
28pub mod natpmp;
29pub mod pcp;
30pub mod relayed;
31pub mod upnp;
32
33use std::net::SocketAddr;
34
35use async_trait::async_trait;
36
37use crate::error::MethodError;
38use crate::peer::PeerTarget;
39
40/// Which traversal technique produced a result — used to order methods, tag failures, and report
41/// (observability) which method actually succeeded WITHOUT the caller caring.
42///
43/// Ordinal order == attempt order == relay-last: a smaller [`TraversalKind::rank`] is tried first,
44/// and `Relayed` has the highest rank so it is always the last resort.
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46pub enum TraversalKind {
47    /// Peer is publicly reachable (or already port-forwarded) — dial it directly.
48    Direct,
49    /// A UPnP/IGD port mapping was created on the local gateway.
50    Upnp,
51    /// A NAT-PMP (RFC 6886) mapping was created.
52    NatPmp,
53    /// A PCP (RFC 6887) mapping was created.
54    Pcp,
55    /// A relay-coordinated hole punch established a direct path across both NATs.
56    HolePunch,
57    /// Traffic is tunnelled THROUGH the relay (last resort).
58    Relayed,
59}
60
61impl TraversalKind {
62    /// Attempt-order rank (lower = tried earlier). Guarantees direct-first and relayed-last.
63    pub fn rank(self) -> u8 {
64        match self {
65            TraversalKind::Direct => 0,
66            TraversalKind::Upnp => 1,
67            TraversalKind::NatPmp => 2,
68            TraversalKind::Pcp => 3,
69            TraversalKind::HolePunch => 4,
70            TraversalKind::Relayed => 5,
71        }
72    }
73}
74
75/// What a traversal method yields on success: the dialable candidate addresses for the peer
76/// (ordered **IPv6-first**), plus which technique produced them. The [`crate::strategy`] then
77/// performs the mTLS dial, trying the candidates IPv6-first with IPv4 fallback (happy-eyeballs, see
78/// [`crate::dialer`]) — except the relayed method, which returns the already-open relay tunnel.
79///
80/// The direct/mapping methods carry the peer's whole IPv6-first candidate list so the dial can fall
81/// back across families; the hole-punch/relayed methods yield a single coordinated/relay address
82/// ([`MethodOutcome::single`]).
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct MethodOutcome {
85    /// Which technique produced these reachable addresses.
86    pub kind: TraversalKind,
87    /// The candidate addresses the strategy should dial, ordered **IPv6-first** (peer public
88    /// endpoints, a hole-punched peer address, the relay endpoint, or — mapping methods — the peer
89    /// candidates to try after opening the local pinhole). The dialer tries them IPv6-first and
90    /// falls back to IPv4. Never empty on success.
91    pub dial_addrs: Vec<SocketAddr>,
92}
93
94impl MethodOutcome {
95    /// An outcome carrying a SINGLE dial address (hole-punch / relayed tiers, which yield one
96    /// coordinated peer address or the relay endpoint).
97    pub fn single(kind: TraversalKind, dial_addr: SocketAddr) -> Self {
98        MethodOutcome {
99            kind,
100            dial_addrs: vec![dial_addr],
101        }
102    }
103
104    /// An outcome carrying the peer's ordered candidate list (direct / mapping tiers). The addresses
105    /// are (re-)sorted IPv6-first so the dial honours the fallback order regardless of input order.
106    pub fn candidates(kind: TraversalKind, mut dial_addrs: Vec<SocketAddr>) -> Self {
107        crate::peer::sort_ipv6_first(&mut dial_addrs);
108        MethodOutcome { kind, dial_addrs }
109    }
110
111    /// The single best (IPv6-preferred) dial address — the first candidate — or `None` if empty.
112    pub fn dial_addr(&self) -> Option<SocketAddr> {
113        self.dial_addrs.first().copied()
114    }
115}
116
117/// A single NAT-traversal technique. Implementors are small + single-purpose and MUST honour the
118/// deadline the strategy hands them (they are additionally wrapped in a hard timeout by the
119/// strategy, so a hung method can never block `connect`).
120#[async_trait]
121pub trait TraversalMethod: Send + Sync {
122    /// Which technique this is (for ordering + observability).
123    fn kind(&self) -> TraversalKind;
124
125    /// Attempt to produce a reachable address for `peer`. `Ok(outcome)` means "try dialing this";
126    /// `Err` means this technique did not work (the strategy falls through to the next one).
127    async fn attempt(&self, peer: &PeerTarget) -> Result<MethodOutcome, MethodError>;
128}