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 (in
76/// discovery order), plus which technique produced them. The [`crate::strategy`] then performs the
77/// mTLS dial, `dig-ip` selecting the family-aware order — IPv6-first over the local∩peer family
78/// intersection with IPv4 fallback (see [`crate::dialer`]) — except the relayed method, which
79/// returns the already-open relay tunnel.
80///
81/// The direct/mapping methods carry the peer's whole candidate list so the dial can fall back across
82/// families; the hole-punch/relayed methods yield a single coordinated/relay address
83/// ([`MethodOutcome::single`]).
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct MethodOutcome {
86    /// Which technique produced these reachable addresses.
87    pub kind: TraversalKind,
88    /// The candidate addresses the strategy should dial, in discovery order (peer public endpoints,
89    /// a hole-punched peer address, the relay endpoint, or — mapping methods — the peer candidates
90    /// to try after opening the local pinhole). `dig-ip` orders them IPv6-first over the local∩peer
91    /// intersection at dial time. Never empty on success.
92    pub dial_addrs: Vec<SocketAddr>,
93}
94
95impl MethodOutcome {
96    /// An outcome carrying a SINGLE dial address (hole-punch / relayed tiers, which yield one
97    /// coordinated peer address or the relay endpoint).
98    pub fn single(kind: TraversalKind, dial_addr: SocketAddr) -> Self {
99        MethodOutcome {
100            kind,
101            dial_addrs: vec![dial_addr],
102        }
103    }
104
105    /// An outcome carrying the peer's candidate list (direct / mapping tiers), in discovery order.
106    /// The IPv6-first preference + local∩peer family intersection are applied at dial time by
107    /// `dig-ip` ([`crate::dialer`]), so the addresses are stored as the method produced them.
108    pub fn candidates(kind: TraversalKind, dial_addrs: Vec<SocketAddr>) -> Self {
109        MethodOutcome { kind, dial_addrs }
110    }
111
112    /// The first dial address in discovery order, or `None` if empty.
113    pub fn dial_addr(&self) -> Option<SocketAddr> {
114        self.dial_addrs.first().copied()
115    }
116}
117
118/// A single NAT-traversal technique. Implementors are small + single-purpose and MUST honour the
119/// deadline the strategy hands them (they are additionally wrapped in a hard timeout by the
120/// strategy, so a hung method can never block `connect`).
121#[async_trait]
122pub trait TraversalMethod: Send + Sync {
123    /// Which technique this is (for ordering + observability).
124    fn kind(&self) -> TraversalKind;
125
126    /// Attempt to produce a reachable address for `peer`. `Ok(outcome)` means "try dialing this";
127    /// `Err` means this technique did not work (the strategy falls through to the next one).
128    async fn attempt(&self, peer: &PeerTarget) -> Result<MethodOutcome, MethodError>;
129}