1use std::fmt;
4
5use rings_core::dht::Did;
6use serde::Deserialize;
7use serde::Serialize;
8
9use super::OnionExitTransport;
10use crate::error::Error;
11
12#[derive(Clone, Debug, Eq, PartialEq)]
14pub enum OnionRouteError {
15 RouteHasNoHops,
17 EmptyRouteService,
19 HopCountOutOfBounds {
21 hop_count: usize,
23 max_hops: u8,
25 },
26 NotEnoughRelays {
28 hop_count: usize,
30 },
31 NoPermittedFirstHop,
33 NoLiveExit {
35 service: String,
37 },
38 NoExitWithTransport {
40 service: String,
42 transport: OnionExitTransport,
44 },
45 NoExitForProxyProtocol {
47 service: String,
49 protocol: String,
51 },
52 NoExitAllowsTarget {
54 service: String,
56 target: String,
58 },
59 DuplicateRouteHops,
61 ExitHopMismatch,
63 ExitServiceMismatch,
65 PayloadServiceMismatch {
67 payload_service: String,
69 route_service: String,
71 },
72 MissingNextHop,
74 CircuitPathLengthMismatch {
76 hop_count: usize,
78 edge_count: usize,
80 },
81 CellPayloadTooLarge,
83 InvalidCell,
85 ReturnEdgeConflict,
87 RelayTableFull,
89 RelayPeerTableFull,
91 BackwardSignerMismatch,
93 BackwardAccountKeyMismatch,
95 BackwardSessionKeyMismatch,
97 InvalidBackwardSignature,
99 ForwardReplay,
101 ForwardPayloadExpired,
103 BackwardReplay,
105 SequenceExhausted,
107 BackwardReturnIdMismatch,
109 UnexpectedBackwardPayload,
111 CircuitIdAllocationFailed,
113 LinkSendCancelled,
115 TcpOpenResponseClosed,
117 TcpOpenTimedOut,
119 UnknownTcpStream,
121 TcpStreamClosed,
123 TcpStreamBackpressure,
125 DuplicateTcpOpen,
127 UnexpectedTcpReturnPeer {
129 expected: Did,
131 actual: Did,
133 },
134 UnexpectedTcpForwardPeer {
136 expected: Did,
138 actual: Did,
140 },
141 ExitFailure(OnionExitFailure),
143 #[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#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
266pub enum OnionExitFailure {
267 ExitUnavailable,
269 PermissionDenied,
271 ResolveTarget,
273 ConnectTarget,
275 ReadTarget,
277 Replay,
279 InvalidTarget(String),
281 DuplicateCircuit,
283 Internal,
285}
286
287impl OnionExitFailure {
288 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}