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