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}