dig_nat/lib.rs
1//! # dig-nat — abstract NAT traversal for DIG Node peer connections
2//!
3//! One API, [`connect`], establishes a **mutually-authenticated (mTLS)** connection to a peer using
4//! the best available NAT-traversal method, transparently. **The caller never chooses the method** —
5//! they describe the peer once and get back a verified [`PeerConnection`]; which technique got there
6//! is reported for observability but is not something the caller handles.
7//!
8//! ## Traversal order (first success wins, relay last)
9//!
10//! Internally the [`strategy`] attempts, in this order:
11//! 1. **Direct** — peer publicly reachable / already port-forwarded ([`method::direct`])
12//! 2. **UPnP/IGD** port mapping ([`method::upnp`])
13//! 3. **NAT-PMP** (RFC 6886, [`method::natpmp`])
14//! 4. **PCP** (RFC 6887, [`method::pcp`])
15//! 5. **Relay-coordinated hole-punch** (RLY-007, [`method::hole_punch`])
16//! 6. **Relayed transport** via `relay.dig.net` — the LAST resort ([`relay`])
17//!
18//! [`stun`] (RFC 5389) discovers this node's reflexive address for candidate advertisement +
19//! hole-punch coordination.
20//!
21//! ## Streaming-first + multiplexed transport
22//!
23//! Whatever tier establishes the connection, the result is uniform: a [`PeerConnection`] wrapping a
24//! single mTLS byte stream in [`yamux`](mux) multiplexing. The caller opens **many cheap concurrent
25//! logical streams** ([`PeerConnection::open_stream`]) with no head-of-line blocking, and
26//! **byte-range streams** ([`PeerConnection::open_range_stream`]) scoped to `[offset, len)` of a
27//! resource — so a downloader fetches DIFFERENT ranges from DIFFERENT peers in parallel and
28//! reassembles. The API is streaming (read bytes as they arrive), never buffer-the-whole-response.
29//!
30//! ## Identity + mTLS — delegated to `dig-tls`
31//!
32//! Every peer connection is mutual TLS, and the entire certificate model is owned by the canonical
33//! [`dig-tls`](dig_tls) crate (L00): `peer_id = SHA-256(TLS SPKI DER)`, the #1204 BLS-G1 cert
34//! binding, and the ready rustls mutual-auth configs. dig-nat presents this node's [`NodeCert`]
35//! and uses [`dig_tls::client_config_spki_pinned`] for the outbound handshake, authenticating the
36//! remote peer by its SPKI `peer_id` pin (matched to the [`peer::PeerTarget::peer_id`] the caller
37//! specified), rustls proof-of-possession, and the #1204 BLS cert binding — with NO DigNetwork-CA
38//! chain requirement; live self-signed peer leaves are accepted (the DIG-CA-everywhere migration
39//! #1378 is deferred). dig-nat holds NO cert/binding/peer_id code of its own (it was extracted to
40//! dig-tls in 0.6.0); the names below are re-exports for convenience.
41//!
42//! ## Graceful fallback + relay resilience
43//!
44//! Each method is bounded by a per-method timeout; if ALL fail, [`connect`] returns a clear
45//! [`NatError::AllMethodsFailed`] (never panics, never hangs). The [`relay`] client — used both as
46//! the last-resort transport and as a node's persistent reachability channel — establishes and
47//! maintains its session with keepalive + capped-exponential-backoff reconnect, tolerates the relay
48//! being down (retries in the background, never crashes the node), logs once per state change, and
49//! honours the `DIG_RELAY_URL=off` opt-out. See [`relay::RelayStatus`].
50//!
51//! ## Example
52//!
53//! ```no_run
54//! # use std::sync::Arc;
55//! # use dig_nat::{connect, NatConfig, NodeCert, PeerTarget, PeerId};
56//! # async fn run(node: Arc<NodeCert>, peer_id: PeerId, addr: std::net::SocketAddr) -> Result<(), dig_nat::NatError> {
57//! let peer = PeerTarget::with_addr(peer_id, addr, "DIG_MAINNET");
58//! let conn = connect(&peer, &node, &NatConfig::default()).await?;
59//! println!("connected to {} via {:?}", conn.peer_id, conn.method);
60//! # Ok(()) }
61//! ```
62
63#![forbid(unsafe_code)]
64#![warn(missing_docs)]
65
66pub mod accept;
67pub mod config;
68pub mod dialer;
69pub mod error;
70pub mod fast_connect;
71pub mod method;
72pub mod mux;
73pub mod peer;
74pub mod relay;
75pub mod relay_descriptor;
76pub mod runtime;
77pub mod safe_text;
78pub mod strategy;
79pub mod stun;
80pub mod tunnel;
81pub mod wire;
82
83#[cfg(test)]
84mod relayed_dial_tests;
85
86use std::sync::Arc;
87
88// --- Certificate / mTLS / identity model — re-exported from the canonical `dig-tls` crate (L00).
89// dig-nat CONSUMES dig-tls for ALL of these (it holds no copy of its own), so a single source of
90// truth means the DIG cert shape can never byte-drift between crates. ---
91pub use dig_tls::binding::{verify_binding_from_leaf_cert, BindingOutcome};
92pub use dig_tls::verify::{CapturedBlsPub, CapturedPeerId};
93pub use dig_tls::{
94 peer_id_from_leaf_cert_der, peer_id_from_tls_spki_der, BindingPolicy, NodeCert, PeerId,
95};
96
97pub use accept::RelayAcceptor;
98pub use config::{NatConfig, NatConfigBuilder};
99pub use error::{MethodError, NatError};
100pub use fast_connect::{connect_fast, FastPeerConnection, FastPeerStream};
101pub use method::hole_punch::{HolePunchCoordinator, HolePunchMethod};
102pub use method::relayed::{
103 RelayedDialMethod, RelayedDialer, RelayedTransport, ReservationRelayedTransport,
104};
105pub use method::upnp::{IgdGateway, RealIgd, UpnpMethod};
106pub use method::{TraversalKind, TraversalMethod};
107pub use mux::{
108 AvailabilityAnswer, AvailabilityItem, AvailabilityRequest, AvailabilityResponse,
109 ChunkLensAssembler, ChunkLensError, ClosedHandle, PeerSession, PeerStream, RangeFrame,
110 RangeRequest, MAX_CHUNK_LENS_PER_FRAME, MAX_FIRST_FRAME_CHUNK_LENS, MAX_FRAMED_BODY,
111 MAX_INCLUSION_PROOF_B64, MAX_RANGE_FRAME_PAYLOAD, MAX_RESOURCE_CHUNK_COUNT,
112};
113pub use peer::{PeerConnection, PeerTarget};
114pub use relay::{RelayState, RelayStatus, RelayTunnel};
115pub use relay_descriptor::{verify_relay_descriptor, RelayDescriptor, RelayDescriptorError};
116pub use runtime::{NatRuntime, NatRuntimeBuilder};
117pub use safe_text::{SafeText, MAX_SAFE_TEXT_CHARS};
118
119use dialer::MtlsDialer;
120use method::direct::DirectMethod;
121use method::natpmp::NatPmpMethod;
122use method::pcp::PcpMethod;
123
124/// Establish a mutually-authenticated connection to `peer` with an empty runtime — the convenience
125/// entry point for a caller that holds NO live transport handles (a publicly-reachable node). Only
126/// the **Direct** tier is composable without runtime handles; a NAT'd node that needs the full ladder
127/// (UPnP/NAT-PMP/PCP/hole-punch/relayed) calls [`connect_with_runtime`] with a [`NatRuntime`] carrying
128/// the gateway/port/relay handles.
129///
130/// `node` is this node's [`NodeCert`] — its CA-signed mTLS identity from [`dig-tls`](dig_tls),
131/// presented as the client certificate; `config` selects which methods are enabled, the per-method
132/// timeout, and the [`BindingPolicy`] applied to the peer's #1204 cert binding.
133///
134/// # Errors
135/// - [`NatError::NoMethodsEnabled`] — no method could be composed (nothing enabled, or the enabled
136/// tiers all lacked their runtime inputs — here, only Direct is available).
137/// - [`NatError::AllMethodsFailed`] — every composed method failed (with per-method reasons).
138///
139/// This never panics and never hangs: every method + dial is bounded by
140/// [`NatConfig::per_method_timeout`].
141pub async fn connect(
142 peer: &PeerTarget,
143 node: &Arc<NodeCert>,
144 config: &NatConfig,
145) -> Result<PeerConnection, NatError> {
146 connect_with_runtime(peer, node, config, &NatRuntime::default()).await
147}
148
149/// Establish a mutually-authenticated connection to `peer`, auto-composing the **FULL** NAT-traversal
150/// ladder — direct → UPnP → NAT-PMP → PCP → hole-punch → relayed — trying each in rank order, first
151/// success wins, relay last. The caller never chooses the method: it supplies the data [`NatConfig`]
152/// and the live [`NatRuntime`] handles, and the strategy picks the first tier that establishes an
153/// mTLS [`PeerConnection`] whose remote `peer_id` matches [`PeerTarget::peer_id`].
154///
155/// Each tier is composed ONLY when it is enabled in `config` AND its runtime inputs are present in
156/// `runtime` (an absent tier is skipped — the composition is honest, never a silently-broken dial):
157/// - **Direct** — always (no runtime input).
158/// - **UPnP** — `runtime.local_port` (+ an optional injected IGD gateway; else the real one).
159/// - **NAT-PMP** — `runtime.local_port` + `runtime.gateway_v4`.
160/// - **PCP** — `runtime.local_port` + `runtime.gateway_v4` + `runtime.client_ip`.
161/// - **Hole-punch** — `runtime.hole_punch` + `runtime.my_external_addr`.
162/// - **Relayed** — `runtime.relayed` (carries mTLS over the relay tunnel — NOT a weaker connection).
163///
164/// Every tier — including the relayed one — runs the SAME dig-tls mTLS: the CA-chained [`NodeCert`],
165/// the `peer_id` pin, and the #1204 BLS binding. IPv6 is preferred at every IP-dialing tier via
166/// `dig-ip` (§5.2). The relayed tier tunnels the identical handshake through the relay, which forwards
167/// only ciphertext it cannot read.
168///
169/// # Errors
170/// Same as [`connect`]: [`NatError::NoMethodsEnabled`] if no tier could be composed, else
171/// [`NatError::AllMethodsFailed`] with each composed tier's reason in attempt order.
172pub async fn connect_with_runtime(
173 peer: &PeerTarget,
174 node: &Arc<NodeCert>,
175 config: &NatConfig,
176 runtime: &NatRuntime,
177) -> Result<PeerConnection, NatError> {
178 let methods = compose_ladder(config, runtime);
179 if methods.is_empty() {
180 return Err(NatError::NoMethodsEnabled);
181 }
182 let mut dialer = MtlsDialer::new(Arc::clone(node)).with_binding_policy(config.binding_policy);
183 if let Some(relayed) = &runtime.relayed {
184 dialer = dialer.with_relayed_dialer(Arc::clone(relayed));
185 }
186 strategy::connect_with_strategy(peer, methods, &dialer, config.per_method_timeout).await
187}
188
189/// Assemble the [`TraversalMethod`] trait objects for the full ladder from the enabled tiers in
190/// `config` whose runtime inputs are present in `runtime`. The strategy orders them by
191/// [`TraversalKind::rank`], so the order they are pushed here is irrelevant. A tier missing its
192/// runtime inputs is silently omitted — `connect` only ever attempts a tier it can actually run.
193fn compose_ladder(config: &NatConfig, runtime: &NatRuntime) -> Vec<Arc<dyn TraversalMethod>> {
194 let mut methods: Vec<Arc<dyn TraversalMethod>> = Vec::new();
195
196 // Direct — always composable (the peer's own candidate addresses).
197 if config.is_enabled(TraversalKind::Direct) {
198 methods.push(Arc::new(DirectMethod));
199 }
200
201 // UPnP — needs a local port to map; uses an injected IGD gateway or the real SSDP-discovered one.
202 if config.is_enabled(TraversalKind::Upnp) {
203 if let Some(port) = runtime.local_port {
204 let gateway: Arc<dyn IgdGateway> = runtime
205 .igd
206 .clone()
207 .unwrap_or_else(|| Arc::new(RealIgd::default()));
208 methods.push(Arc::new(UpnpMethod::new(gateway, port)));
209 }
210 }
211
212 // NAT-PMP — needs the local port + the IPv4 gateway.
213 if config.is_enabled(TraversalKind::NatPmp) {
214 if let (Some(port), Some(gw)) = (runtime.local_port, runtime.gateway_v4) {
215 methods.push(Arc::new(NatPmpMethod::new(gw, port)));
216 }
217 }
218
219 // PCP — needs the local port + the IPv4 gateway + this node's client IP.
220 if config.is_enabled(TraversalKind::Pcp) {
221 if let (Some(port), Some(gw), Some(client_ip)) =
222 (runtime.local_port, runtime.gateway_v4, runtime.client_ip)
223 {
224 methods.push(Arc::new(PcpMethod::new(gw, port, client_ip)));
225 }
226 }
227
228 // Hole-punch — needs a relay coordinator + this node's STUN-discovered reflexive address.
229 if config.is_enabled(TraversalKind::HolePunch) {
230 if let (Some(coordinator), Some(my_addr)) =
231 (runtime.hole_punch.clone(), runtime.my_external_addr)
232 {
233 methods.push(Arc::new(HolePunchMethod::new(coordinator, my_addr)));
234 }
235 }
236
237 // Relayed (TURN-last) — needs the relay data-plane; the dial carries mTLS over the relay tunnel.
238 if config.is_enabled(TraversalKind::Relayed) {
239 if let Some(relayed) = runtime.relayed.clone() {
240 methods.push(Arc::new(RelayedDialMethod::new(relayed)));
241 }
242 }
243
244 methods
245}