Skip to main content

rings_node/onion/
failure.rs

1//! Algebraic failure values for local onion routing and exit wire responses.
2
3use std::fmt;
4
5use rings_core::dht::Did;
6use serde::Deserialize;
7use serde::Serialize;
8
9use super::OnionExitTransport;
10use crate::error::Error;
11
12/// Local route/circuit failure before any user-facing rendering.
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub enum OnionRouteError {
15    /// A route or circuit was unexpectedly empty.
16    RouteHasNoHops,
17    /// The requested route service is empty after normalization.
18    EmptyRouteService,
19    /// The requested or constructed hop count is outside the circuit bound.
20    HopCountOutOfBounds {
21        /// Requested or constructed hop count.
22        hop_count: usize,
23        /// Maximum hop count accepted by this circuit implementation.
24        max_hops: u8,
25    },
26    /// Route construction could not select enough relay hops.
27    NotEnoughRelays {
28        /// Requested hop count including the exit.
29        hop_count: usize,
30    },
31    /// Route construction could not select a first hop accepted by the caller.
32    NoPermittedFirstHop,
33    /// No live exit descriptor offers the requested service.
34    NoLiveExit {
35        /// Requested service name.
36        service: String,
37    },
38    /// Live exits advertise the service name but none use the required transport.
39    NoExitWithTransport {
40        /// Requested service name.
41        service: String,
42        /// Required transport class.
43        transport: OnionExitTransport,
44    },
45    /// Live exits advertise the service transport, but none can serve the requested proxy protocol.
46    NoExitForProxyProtocol {
47        /// Requested service name.
48        service: String,
49        /// Requested proxy protocol label.
50        protocol: String,
51    },
52    /// Live exits advertise the service and transport, but no policy allows the target.
53    NoExitAllowsTarget {
54        /// Requested service name.
55        service: String,
56        /// Requested target authority.
57        target: String,
58    },
59    /// Route construction found duplicate DIDs.
60    DuplicateRouteHops,
61    /// The selected exit descriptor does not match the final encrypted hop.
62    ExitHopMismatch,
63    /// The selected exit does not offer the route service.
64    ExitServiceMismatch,
65    /// A payload service does not match its route service.
66    PayloadServiceMismatch {
67        /// Service label authenticated in the payload.
68        payload_service: String,
69        /// Service label selected by the route.
70        route_service: String,
71    },
72    /// A relay layer references a missing next hop.
73    MissingNextHop,
74    /// A constructed circuit path does not have exactly one edge id per hop.
75    CircuitPathLengthMismatch {
76        /// Number of encrypted hops in the route.
77        hop_count: usize,
78        /// Number of edge ids carried by the circuit path.
79        edge_count: usize,
80    },
81    /// A message cannot fit in the largest supported encrypted cell class.
82    CellPayloadTooLarge,
83    /// A decrypted encrypted cell has an invalid length or internal framing.
84    InvalidCell,
85    /// A live relay return edge already belongs to another previous hop.
86    ReturnEdgeConflict,
87    /// The relay return table is full.
88    RelayTableFull,
89    /// One authenticated previous hop exhausted its share of the relay return table.
90    RelayPeerTableFull,
91    /// A backward payload signer is not the selected exit DID.
92    BackwardSignerMismatch,
93    /// A backward payload signer account key is not the selected exit key.
94    BackwardAccountKeyMismatch,
95    /// A backward payload session key is not the selected exit session key.
96    BackwardSessionKeyMismatch,
97    /// A backward payload signature or freshness proof is invalid.
98    InvalidBackwardSignature,
99    /// A forward nonce has already authorized an exit-side action.
100    ForwardReplay,
101    /// A forward payload reached the exit after its authenticated expiry.
102    ForwardPayloadExpired,
103    /// A backward sequence number has already delivered a client-side action.
104    BackwardReplay,
105    /// A circuit direction exhausted its monotonic sequence space.
106    SequenceExhausted,
107    /// A backward payload carries a return id that does not belong to the local client state.
108    BackwardReturnIdMismatch,
109    /// A backward payload decoded to a shape that no client adapter may accept.
110    UnexpectedBackwardPayload,
111    /// The runtime could not allocate a unique circuit id.
112    CircuitIdAllocationFailed,
113    /// A queued endpoint cell lost its drain task before the overlay reported a result.
114    LinkSendCancelled,
115    /// A TCP open response channel closed before an answer.
116    TcpOpenResponseClosed,
117    /// A TCP open request timed out before the exit answered.
118    TcpOpenTimedOut,
119    /// A TCP stream key is unknown to this runtime.
120    UnknownTcpStream,
121    /// A TCP stream channel has already closed.
122    TcpStreamClosed,
123    /// A TCP stream's bounded inbound queue cannot accept another frame.
124    TcpStreamBackpressure,
125    /// A duplicate TCP open targeted a live circuit.
126    DuplicateTcpOpen,
127    /// A received TCP return peer differs from the selected route peer.
128    UnexpectedTcpReturnPeer {
129        /// Return peer selected by the client route.
130        expected: Did,
131        /// Peer that delivered the backward payload.
132        actual: Did,
133    },
134    /// A received TCP forward peer differs from the selected route peer.
135    UnexpectedTcpForwardPeer {
136        /// Forward peer recorded when the exit accepted the circuit.
137        expected: Did,
138        /// Peer that delivered the forward payload.
139        actual: Did,
140    },
141    /// An exit-reported failure reached the local route client.
142    ExitFailure(OnionExitFailure),
143    /// A test-only route fixture was missing an expected relay.
144    #[cfg(test)]
145    MissingTestRelay,
146}
147
148impl fmt::Display for OnionRouteError {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        match self {
151            Self::RouteHasNoHops => f.write_str("onion route has no hops"),
152            Self::EmptyRouteService => f.write_str("onion route service must not be empty"),
153            Self::HopCountOutOfBounds {
154                hop_count,
155                max_hops,
156            } => write!(f, "onion route hop count {hop_count} exceeds limit {max_hops}"),
157            Self::NotEnoughRelays { hop_count } => {
158                write!(f, "not enough relay candidates for {hop_count}-hop onion route")
159            }
160            Self::NoPermittedFirstHop => {
161                f.write_str("no onion route has a permitted first hop")
162            }
163            Self::NoLiveExit { service } => {
164                write!(f, "no live onion exit offers service {service:?}")
165            }
166            Self::NoExitWithTransport { service, transport } => write!(
167                f,
168                "no live onion exit offers service {service:?} over {transport:?}"
169            ),
170            Self::NoExitForProxyProtocol { service, protocol } => write!(
171                f,
172                "no live onion exit offers service {service:?} for proxy protocol {protocol:?}"
173            ),
174            Self::NoExitAllowsTarget { service, target } => write!(
175                f,
176                "no live onion exit for service {service:?} allows target {target:?}"
177            ),
178            Self::DuplicateRouteHops => f.write_str("onion route contains duplicate hops"),
179            Self::ExitHopMismatch => {
180                f.write_str("onion route exit hop does not match exit descriptor")
181            }
182            Self::ExitServiceMismatch => {
183                f.write_str("onion route exit does not offer selected service")
184            }
185            Self::PayloadServiceMismatch {
186                payload_service,
187                route_service,
188            } => write!(
189                f,
190                "onion payload service {payload_service:?} does not match route service {route_service:?}"
191            ),
192            Self::MissingNextHop => f.write_str("missing next onion hop"),
193            Self::CircuitPathLengthMismatch {
194                hop_count,
195                edge_count,
196            } => write!(
197                f,
198                "onion circuit path has {edge_count} edge ids for {hop_count} route hops"
199            ),
200            Self::CellPayloadTooLarge => {
201                f.write_str("onion message exceeds the largest encrypted cell class")
202            }
203            Self::InvalidCell => f.write_str("invalid encrypted onion cell"),
204            Self::ReturnEdgeConflict => {
205                f.write_str("onion relay return edge already belongs to another previous hop")
206            }
207            Self::RelayTableFull => f.write_str("onion relay circuit table is full"),
208            Self::RelayPeerTableFull => {
209                f.write_str("onion relay circuit table quota for previous hop is full")
210            }
211            Self::BackwardSignerMismatch => {
212                f.write_str("onion backward payload signer is not the selected exit")
213            }
214            Self::BackwardAccountKeyMismatch => {
215                f.write_str("onion backward payload account key is not the selected exit")
216            }
217            Self::BackwardSessionKeyMismatch => {
218                f.write_str("onion backward payload session key is not the selected exit")
219            }
220            Self::InvalidBackwardSignature => {
221                f.write_str("invalid onion backward payload signature")
222            }
223            Self::ForwardReplay => f.write_str("replayed onion forward payload"),
224            Self::ForwardPayloadExpired => f.write_str("expired onion forward payload"),
225            Self::BackwardReplay => f.write_str("replayed onion TCP backward payload"),
226            Self::SequenceExhausted => f.write_str("onion circuit sequence exhausted"),
227            Self::BackwardReturnIdMismatch => {
228                f.write_str("onion backward payload return id mismatch")
229            }
230            Self::UnexpectedBackwardPayload => {
231                f.write_str("unexpected onion backward payload for client adapter")
232            }
233            Self::CircuitIdAllocationFailed => {
234                f.write_str("failed to allocate unique onion circuit id")
235            }
236            Self::LinkSendCancelled => {
237                f.write_str("onion link send was cancelled before overlay completion")
238            }
239            Self::TcpOpenResponseClosed => {
240                f.write_str("onion TCP open response channel closed")
241            }
242            Self::TcpOpenTimedOut => f.write_str("onion TCP open timed out"),
243            Self::UnknownTcpStream => f.write_str("unknown onion TCP stream"),
244            Self::TcpStreamClosed => f.write_str("onion TCP stream is closed"),
245            Self::TcpStreamBackpressure => {
246                f.write_str("onion TCP stream inbound queue is saturated")
247            }
248            Self::DuplicateTcpOpen => f.write_str("duplicate onion TCP open for live circuit"),
249            Self::UnexpectedTcpReturnPeer { expected, actual } => write!(
250                f,
251                "unexpected onion TCP return peer: expected {expected:?}, got {actual:?}"
252            ),
253            Self::UnexpectedTcpForwardPeer { expected, actual } => write!(
254                f,
255                "unexpected onion TCP forward peer: expected {expected:?}, got {actual:?}"
256            ),
257            Self::ExitFailure(failure) => failure.fmt(f),
258            #[cfg(test)]
259            Self::MissingTestRelay => f.write_str("missing test relay"),
260        }
261    }
262}
263
264/// Recoverable failure reported by an onion exit to its client.
265#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
266pub enum OnionExitFailure {
267    /// The requested exit service is not enabled on the selected node.
268    ExitUnavailable,
269    /// The exit policy or local limiter denied the operation.
270    PermissionDenied,
271    /// The target name could not be resolved.
272    ResolveTarget,
273    /// The exit could not connect to the target.
274    ConnectTarget,
275    /// The exit failed while reading from the target.
276    ReadTarget,
277    /// The exit rejected a replayed payload.
278    Replay,
279    /// The client supplied a malformed target for this exit protocol.
280    InvalidTarget(String),
281    /// The exit rejected a duplicate live circuit.
282    DuplicateCircuit,
283    /// The exit hit a local internal failure while answering the request.
284    Internal,
285}
286
287impl OnionExitFailure {
288    /// Convert a local node error into a wire failure at the adapter boundary.
289    pub fn from_error(error: &Error) -> Self {
290        match error {
291            Error::NoPermission => Self::PermissionDenied,
292            Error::OnionRouteError(OnionRouteError::ForwardReplay)
293            | Error::OnionRouteError(OnionRouteError::ForwardPayloadExpired)
294            | Error::OnionRouteError(OnionRouteError::BackwardReplay) => Self::Replay,
295            Error::OnionRouteError(OnionRouteError::DuplicateTcpOpen) => Self::DuplicateCircuit,
296            _ => Self::Internal,
297        }
298    }
299}
300
301impl fmt::Display for OnionExitFailure {
302    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
303        match self {
304            Self::ExitUnavailable => f.write_str("onion exit service is not enabled locally"),
305            Self::PermissionDenied => Error::NoPermission.fmt(f),
306            Self::ResolveTarget => f.write_str("onion exit could not resolve target"),
307            Self::ConnectTarget => f.write_str("onion exit could not connect to target"),
308            Self::ReadTarget => f.write_str("onion exit could not read target"),
309            Self::InvalidTarget(message) => f.write_str(message),
310            Self::Replay => f.write_str("replayed onion payload"),
311            Self::DuplicateCircuit => f.write_str("duplicate onion TCP open for live circuit"),
312            Self::Internal => f.write_str("onion exit internal failure"),
313        }
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::OnionExitFailure;
320    use crate::error::Error;
321
322    #[test]
323    fn test_wire_internal_failure_does_not_expose_local_diagnostic() {
324        let diagnostic = "secret local filesystem and resolver detail";
325        let failure = OnionExitFailure::from_error(&Error::InvalidConfig(diagnostic.to_string()));
326        let encoded = rings_codec::serialize(&failure).expect("encode wire failure");
327
328        assert_eq!(failure, OnionExitFailure::Internal);
329        assert!(!failure.to_string().contains(diagnostic));
330        assert!(!encoded
331            .windows(diagnostic.len())
332            .any(|window| window == diagnostic.as_bytes()));
333    }
334}