Skip to main content

dig_nat/
error.rs

1//! Error + status types for NAT traversal.
2//!
3//! The public surface returns a single [`NatError`] so a caller never has to match on
4//! transport-internal error zoo. Each traversal method's failure is captured as a
5//! [`MethodError`] and, when *every* method fails, aggregated into
6//! [`NatError::AllMethodsFailed`] carrying the per-method reasons — so an operator/agent can see
7//! exactly why each path was rejected without scraping logs.
8
9use std::fmt;
10
11use crate::method::TraversalKind;
12use crate::safe_text::SafeText;
13
14/// Peer-RPC JSON-RPC error codes from the L7 peer-network spec (§7, §9). Exposed so a node building
15/// its RPC surface over dig-nat maps transport outcomes to the exact catalogued codes.
16pub mod rpc_error_codes {
17    /// `-32004` RESOURCE_UNAVAILABLE — the peer does not hold the resource/capsule at the requested
18    /// root (try another source).
19    pub const RESOURCE_UNAVAILABLE: i32 = -32004;
20    /// `-32006` PEER_UNREACHABLE — no connection to the named peer could be established (every
21    /// traversal strategy failed) or the peer is not registered on this network. Maps from
22    /// [`super::NatError::AllMethodsFailed`].
23    pub const PEER_UNREACHABLE: i32 = -32006;
24    /// `-32007` RANGE_NOT_SATISFIABLE — the requested `offset`/`length` lies outside the resource, or
25    /// the range is otherwise unsatisfiable.
26    pub const RANGE_NOT_SATISFIABLE: i32 = -32007;
27}
28
29/// The single error type returned by the public [`crate::connect`] API.
30///
31/// A connection attempt degrades gracefully: each method is tried with bounded timeouts and, if
32/// *all* enabled methods fail, [`NatError::AllMethodsFailed`] is returned with the ordered list of
33/// per-method failures. `connect` never panics and never hangs — a stuck method is bounded by its
34/// timeout and surfaces here as a [`MethodError::Timeout`].
35#[derive(Debug, thiserror::Error)]
36pub enum NatError {
37    /// Every enabled traversal method failed. Carries the ordered per-method reasons (the order is
38    /// the attempt order: direct → UPnP → NAT-PMP → PCP → hole-punch → relayed).
39    #[error("all NAT traversal methods failed: {0:?}")]
40    AllMethodsFailed(Vec<MethodError>),
41
42    /// No traversal methods were enabled in the config, so there was nothing to try.
43    #[error("no traversal methods enabled")]
44    NoMethodsEnabled,
45
46    /// The mTLS session was established but the peer's identity did not match the expected
47    /// `peer_id` (SHA-256 of its TLS SubjectPublicKeyInfo DER). This is a hard security failure:
48    /// the transport connected but to the wrong (or an unverifiable) peer.
49    #[error("peer identity mismatch: expected {expected}, got {actual}")]
50    PeerIdentityMismatch {
51        /// The `peer_id` the caller asked to connect to (hex).
52        expected: String,
53        /// The `peer_id` derived from the certificate the remote actually presented (hex).
54        actual: String,
55    },
56
57    /// Configuration was invalid (e.g. an unparseable relay endpoint or a bad local identity).
58    #[error("invalid configuration: {0}")]
59    InvalidConfig(String),
60}
61
62impl NatError {
63    /// The peer-RPC error code a node should surface when this connect failure bubbles up to its RPC
64    /// layer. A failed traversal (all methods failed / peer identity mismatch / nothing enabled) is
65    /// [`rpc_error_codes::PEER_UNREACHABLE`] (`-32006`) per the L7 spec.
66    pub fn rpc_error_code(&self) -> i32 {
67        rpc_error_codes::PEER_UNREACHABLE
68    }
69}
70
71/// One traversal method's failure, tagged with which method produced it.
72///
73/// Aggregated into [`NatError::AllMethodsFailed`] in attempt order. The `kind` lets an agent see
74/// *which* path failed and the `reason` is a stable human string.
75#[derive(Clone, PartialEq, Eq)]
76pub struct MethodError {
77    /// Which traversal method produced this failure.
78    pub kind: TraversalKind,
79    /// A stable, human-readable reason (also machine-greppable).
80    ///
81    /// **Peer-influenced.** A failed mTLS handshake's reason is derived from a rustls error, and a
82    /// rustls error can carry text derived from the certificate the remote presented. It is stored
83    /// raw (so a consumer can match on it) but NEUTRALIZED WHEN RENDERED — see the `Display` and
84    /// `Debug` impls below.
85    pub reason: String,
86    /// Whether the failure was a timeout (vs an outright refusal / protocol error). Lets the
87    /// strategy + observers distinguish "peer/gateway unreachable in time" from "actively rejected".
88    pub timeout: bool,
89}
90
91/// Renders the `kind` then the `reason`, with the reason NEUTRALIZED (#1674).
92///
93/// Sanitizing lives here rather than at each construction site because that is what makes it hold:
94/// `reason` is a `String` reachable from `MethodError::failed`, from struct literal syntax, and from
95/// a consumer mutating the public field, and a guard applied at any one of those is a guard the next
96/// caller skips. Applied in `Display` it is unrepresentable in a rendered error however the error
97/// was built.
98impl fmt::Display for MethodError {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        write!(
101            f,
102            "{:?}: {}",
103            self.kind,
104            SafeText::from_untrusted(&self.reason)
105        )
106    }
107}
108
109/// Hand-written for the same reason as `Display`, and because `Debug` is the rendering that actually
110/// reaches a log here: [`NatError::AllMethodsFailed`] formats its members with `{:?}`.
111///
112/// A DERIVED `Debug` happens to be safe today — it renders `reason` with `{:?}`, which escapes a
113/// newline as a side effect of quoting. That is luck, not a guarantee: it does not neutralize bidi
114/// overrides, it applies no length bound, and it would silently stop protecting anything the day the
115/// aggregate switched to `{}`. So the neutralization is stated rather than inherited.
116impl fmt::Debug for MethodError {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        f.debug_struct("MethodError")
119            .field("kind", &self.kind)
120            .field("reason", &SafeText::from_untrusted(&self.reason))
121            .field("timeout", &self.timeout)
122            .finish()
123    }
124}
125
126impl std::error::Error for MethodError {}
127
128impl MethodError {
129    /// A non-timeout method failure.
130    pub fn failed(kind: TraversalKind, reason: impl Into<String>) -> Self {
131        MethodError {
132            kind,
133            reason: reason.into(),
134            timeout: false,
135        }
136    }
137
138    /// A timeout method failure (the method did not complete within its bounded deadline).
139    pub fn timeout(kind: TraversalKind) -> Self {
140        MethodError {
141            kind,
142            reason: format!("{kind:?} timed out"),
143            timeout: true,
144        }
145    }
146}