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 crate::method::TraversalKind;
10
11/// Peer-RPC JSON-RPC error codes from the L7 peer-network spec (§7, §9). Exposed so a node building
12/// its RPC surface over dig-nat maps transport outcomes to the exact catalogued codes.
13pub mod rpc_error_codes {
14    /// `-32004` RESOURCE_UNAVAILABLE — the peer does not hold the resource/capsule at the requested
15    /// root (try another source).
16    pub const RESOURCE_UNAVAILABLE: i32 = -32004;
17    /// `-32006` PEER_UNREACHABLE — no connection to the named peer could be established (every
18    /// traversal strategy failed) or the peer is not registered on this network. Maps from
19    /// [`super::NatError::AllMethodsFailed`].
20    pub const PEER_UNREACHABLE: i32 = -32006;
21    /// `-32007` RANGE_NOT_SATISFIABLE — the requested `offset`/`length` lies outside the resource, or
22    /// the range is otherwise unsatisfiable.
23    pub const RANGE_NOT_SATISFIABLE: i32 = -32007;
24}
25
26/// The single error type returned by the public [`crate::connect`] API.
27///
28/// A connection attempt degrades gracefully: each method is tried with bounded timeouts and, if
29/// *all* enabled methods fail, [`NatError::AllMethodsFailed`] is returned with the ordered list of
30/// per-method failures. `connect` never panics and never hangs — a stuck method is bounded by its
31/// timeout and surfaces here as a [`MethodError::Timeout`].
32#[derive(Debug, thiserror::Error)]
33pub enum NatError {
34    /// Every enabled traversal method failed. Carries the ordered per-method reasons (the order is
35    /// the attempt order: direct → UPnP → NAT-PMP → PCP → hole-punch → relayed).
36    #[error("all NAT traversal methods failed: {0:?}")]
37    AllMethodsFailed(Vec<MethodError>),
38
39    /// No traversal methods were enabled in the config, so there was nothing to try.
40    #[error("no traversal methods enabled")]
41    NoMethodsEnabled,
42
43    /// The mTLS session was established but the peer's identity did not match the expected
44    /// `peer_id` (SHA-256 of its TLS SubjectPublicKeyInfo DER). This is a hard security failure:
45    /// the transport connected but to the wrong (or an unverifiable) peer.
46    #[error("peer identity mismatch: expected {expected}, got {actual}")]
47    PeerIdentityMismatch {
48        /// The `peer_id` the caller asked to connect to (hex).
49        expected: String,
50        /// The `peer_id` derived from the certificate the remote actually presented (hex).
51        actual: String,
52    },
53
54    /// Configuration was invalid (e.g. an unparseable relay endpoint or a bad local identity).
55    #[error("invalid configuration: {0}")]
56    InvalidConfig(String),
57}
58
59impl NatError {
60    /// The peer-RPC error code a node should surface when this connect failure bubbles up to its RPC
61    /// layer. A failed traversal (all methods failed / peer identity mismatch / nothing enabled) is
62    /// [`rpc_error_codes::PEER_UNREACHABLE`] (`-32006`) per the L7 spec.
63    pub fn rpc_error_code(&self) -> i32 {
64        rpc_error_codes::PEER_UNREACHABLE
65    }
66}
67
68/// One traversal method's failure, tagged with which method produced it.
69///
70/// Aggregated into [`NatError::AllMethodsFailed`] in attempt order. The `kind` lets an agent see
71/// *which* path failed and the `reason` is a stable human string.
72#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
73#[error("{kind:?}: {reason}")]
74pub struct MethodError {
75    /// Which traversal method produced this failure.
76    pub kind: TraversalKind,
77    /// A stable, human-readable reason (also machine-greppable).
78    pub reason: String,
79    /// Whether the failure was a timeout (vs an outright refusal / protocol error). Lets the
80    /// strategy + observers distinguish "peer/gateway unreachable in time" from "actively rejected".
81    pub timeout: bool,
82}
83
84impl MethodError {
85    /// A non-timeout method failure.
86    pub fn failed(kind: TraversalKind, reason: impl Into<String>) -> Self {
87        MethodError {
88            kind,
89            reason: reason.into(),
90            timeout: false,
91        }
92    }
93
94    /// A timeout method failure (the method did not complete within its bounded deadline).
95    pub fn timeout(kind: TraversalKind) -> Self {
96        MethodError {
97            kind,
98            reason: format!("{kind:?} timed out"),
99            timeout: true,
100        }
101    }
102}