Skip to main content

dig_nat/
strategy.rs

1//! The traversal strategy — orders the enabled methods (direct-first, relay-last), tries each with
2//! a bounded timeout, and returns the FIRST connection that establishes. This is where "the caller
3//! doesn't choose the method" is realised.
4//!
5//! ## Order
6//!
7//! Methods are always attempted in [`TraversalKind::rank`] order — Direct → Upnp → NatPmp → Pcp →
8//! HolePunch → Relayed — regardless of the order the caller listed them, so the cheapest/most-direct
9//! path is preferred and the fully-relayed transport is genuinely the LAST resort.
10//!
11//! ## Two-stage attempt
12//!
13//! A method yields a [`MethodOutcome`] (a dial address + which technique). The strategy then asks
14//! the [`Dialer`] to establish the mTLS session to that address. Both stages are bounded by the
15//! per-method timeout, so a hung method OR a hung dial can never block `connect`. If both stages
16//! fail for every method, the strategy returns [`NatError::AllMethodsFailed`] with the ordered
17//! per-method reasons.
18//!
19//! ## Testability
20//!
21//! [`Dialer`] abstracts the mTLS dial (real impl in [`crate::dialer`]; tests inject a fake that
22//! returns a canned outcome), and the methods are [`TraversalMethod`] trait objects (tests inject
23//! mocks). So the ordering, first-success-wins, relay-last, and all-fail→error behaviour are all
24//! tested with NO real network.
25
26use std::sync::Arc;
27use std::time::Duration;
28
29use async_trait::async_trait;
30
31use crate::error::{MethodError, NatError};
32use crate::method::{MethodOutcome, TraversalKind, TraversalMethod};
33use crate::peer::{PeerConnection, PeerTarget};
34
35/// Establishes the actual mTLS peer connection once a method has produced reachable address(es).
36///
37/// For the direct/mapping/hole-punch methods this is a rustls mTLS dial that races
38/// `outcome.dial_addrs` **IPv6-first with IPv4 fallback** (happy eyeballs); for the relayed method it
39/// opens the mTLS session tunnelled through the relay. Abstracted so the strategy is testable and the
40/// transport detail lives in one place ([`crate::dialer`]).
41#[async_trait]
42pub trait Dialer: Send + Sync {
43    /// Establish an mTLS connection to `peer` using the reachable address in `outcome`, verifying
44    /// the remote's `peer_id` matches [`PeerTarget::peer_id`]. `Err` = the dial or verification
45    /// failed (the strategy falls through to the next method).
46    async fn dial(
47        &self,
48        peer: &PeerTarget,
49        outcome: &MethodOutcome,
50    ) -> Result<PeerConnection, MethodError>;
51}
52
53/// Run the traversal strategy: try each enabled method in rank order; the first that produces a
54/// verified mTLS [`PeerConnection`] wins. `methods` may be listed in any order — they are sorted by
55/// [`TraversalKind::rank`] here so ordering is guaranteed independent of caller input.
56///
57/// Returns [`NatError::NoMethodsEnabled`] if `methods` is empty, else the first success, else
58/// [`NatError::AllMethodsFailed`] with every method's reason in attempt order.
59pub async fn connect_with_strategy(
60    peer: &PeerTarget,
61    methods: Vec<Arc<dyn TraversalMethod>>,
62    dialer: &dyn Dialer,
63    per_method_timeout: Duration,
64) -> Result<PeerConnection, NatError> {
65    if methods.is_empty() {
66        return Err(NatError::NoMethodsEnabled);
67    }
68
69    // Guarantee direct-first, relay-last regardless of how the caller ordered `methods`.
70    let mut ordered = methods;
71    ordered.sort_by_key(|m| m.kind().rank());
72
73    let mut failures: Vec<MethodError> = Vec::with_capacity(ordered.len());
74
75    for method in ordered {
76        let kind = method.kind();
77        // Stage 1: the method produces a reachable address (bounded).
78        let outcome = match run_bounded(per_method_timeout, kind, method.attempt(peer)).await {
79            Ok(outcome) => outcome,
80            Err(e) => {
81                tracing::debug!(?kind, reason = %e.reason, "traversal method did not produce an address");
82                failures.push(e);
83                continue;
84            }
85        };
86        // Stage 2: dial + mTLS-verify to that address (bounded).
87        match run_bounded(per_method_timeout, kind, dialer.dial(peer, &outcome)).await {
88            Ok(conn) => {
89                tracing::info!(?kind, remote = %conn.remote_addr, "peer connection established");
90                return Ok(conn);
91            }
92            Err(e) => {
93                tracing::debug!(?kind, reason = %e.reason, "dial failed; falling through");
94                failures.push(e);
95            }
96        }
97    }
98
99    Err(NatError::AllMethodsFailed(failures))
100}
101
102/// Run one method/dial future under a hard timeout, mapping a timeout to [`MethodError::timeout`].
103/// This is the guarantee that a stuck method can never hang `connect`.
104async fn run_bounded<T, F>(timeout: Duration, kind: TraversalKind, fut: F) -> Result<T, MethodError>
105where
106    F: std::future::Future<Output = Result<T, MethodError>>,
107{
108    match tokio::time::timeout(timeout, fut).await {
109        Ok(res) => res,
110        Err(_) => Err(MethodError::timeout(kind)),
111    }
112}