1
//! Implement a simple proxy that relays connections over Tor.
2
//!
3
//! A proxy is launched with [`bind_proxy()`], which opens listener ports.
4
//! `StreamProxy::run_proxy` then listens for new
5
//! connections, handles an appropriate handshake,
6
//! and then relays traffic as appropriate.
7

            
8
semipublic_mod! {
9
    #[cfg(feature="http-connect")]
10
    mod http_connect;
11
    mod socks;
12
    pub(crate) mod port_info;
13
}
14

            
15
use derive_more::Display;
16
use extend::ext;
17
use futures::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, Error as IoError};
18
use futures::stream::StreamExt;
19
use std::net::IpAddr;
20
use std::sync::Arc;
21
use tor_basic_utils::error_sources::ErrorSources;
22
use tor_log_ratelim::log_ratelim;
23
use tor_rtcompat::{NetStreamProvider, SpawnExt, TcpListenOptions};
24
use tracing::{debug, error, info, instrument, warn};
25

            
26
#[allow(unused)]
27
use arti_client::HasKind;
28
use arti_client::TorClient;
29
#[cfg(feature = "rpc")]
30
use arti_rpcserver::RpcMgr;
31
use tor_config::Listen;
32
use tor_error::{debug_report, warn_report};
33
use tor_rtcompat::{NetStreamListener, Runtime};
34
use tor_socksproto::SocksAuth;
35

            
36
use anyhow::{Context, Result, anyhow};
37

            
38
/// Placeholder type when RPC is disabled at compile time.
39
#[cfg(not(feature = "rpc"))]
40
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
41
#[allow(clippy::exhaustive_enums)] // experimental, so exhaustive is OK
42
pub(crate) enum RpcMgr {}
43

            
44
/// A set of proxy protocols to support on a listener.
45
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
46
#[derive(Copy, Clone, Debug)]
47
#[non_exhaustive]
48
pub(crate) enum ListenProtocols {
49
    /// Only the socks protocol.
50
    SocksOnly,
51
    /// Socks _and_ HTTP CONNECT.
52
    SocksAndHttpConnect,
53
}
54

            
55
impl ListenProtocols {
56
    /// Return true if http connect is included in this set of protocols.
57
    fn http_connect_supported(self) -> bool {
58
        matches!(self, Self::SocksAndHttpConnect)
59
    }
60
}
61

            
62
impl std::fmt::Display for ListenProtocols {
63
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64
        match self {
65
            ListenProtocols::SocksOnly => write!(f, "SOCKS"),
66
            ListenProtocols::SocksAndHttpConnect => write!(f, "SOCKS+HTTP"),
67
        }
68
    }
69
}
70

            
71
/// A Key used to isolate connections.
72
///
73
/// Composed of an usize (representing which listener socket accepted
74
/// the connection, the source IpAddr of the client, and the
75
/// authentication string provided by the client).
76
#[derive(Debug, Clone, PartialEq, Eq)]
77
struct StreamIsolationKey(ListenerIsolation, ProvidedIsolation);
78

            
79
/// Isolation information provided through the proxy connection
80
#[derive(Debug, Clone, PartialEq, Eq)]
81
enum ProvidedIsolation {
82
    /// The socks isolation itself.
83
    LegacySocks(SocksAuth),
84
    /// A bytestring provided as isolation with the extended Socks5 username/password protocol.
85
    ExtendedSocks {
86
        /// Which format was negotiated?
87
        ///
88
        /// (At present, different format codes can't share a circuit.)
89
        format_code: u8,
90
        /// What's the isolation string?
91
        isolation: Box<[u8]>,
92
    },
93
    #[cfg(feature = "http-connect")]
94
    /// An HTTP token, taken from headers.
95
    Http(http_connect::Isolation),
96
}
97

            
98
impl arti_client::isolation::IsolationHelper for StreamIsolationKey {
99
    fn compatible_same_type(&self, other: &Self) -> bool {
100
        self == other
101
    }
102

            
103
    fn join_same_type(&self, other: &Self) -> Option<Self> {
104
        if self == other {
105
            Some(self.clone())
106
        } else {
107
            None
108
        }
109
    }
110

            
111
    fn enables_long_lived_circuits(&self) -> bool {
112
        use ProvidedIsolation as PI;
113
        use SocksAuth as SA;
114
        match &self.1 {
115
            PI::LegacySocks(SA::Socks4(auth)) => !auth.is_empty(),
116
            PI::LegacySocks(SA::Username(uname, pass)) => !(uname.is_empty() && pass.is_empty()),
117
            PI::LegacySocks(_) => false,
118
            PI::ExtendedSocks { isolation, .. } => !isolation.is_empty(),
119
            #[cfg(feature = "http-connect")]
120
            PI::Http(isolation) => !isolation.is_empty(),
121
        }
122
    }
123
}
124

            
125
/// Size of read buffer to apply to application data streams
126
/// and Tor data streams when copying.
127
//
128
// This particular value is chosen more or less arbitrarily.
129
// Larger values let us do fewer reads from the application,
130
// but consume more memory.
131
//
132
// (The default value for BufReader is 8k as of this writing.)
133
const APP_STREAM_BUF_LEN: usize = 4096;
134

            
135
const _: () = {
136
    assert!(APP_STREAM_BUF_LEN >= tor_socksproto::SOCKS_BUF_LEN);
137
};
138

            
139
/// NOTE: The following documentation belongs in a spec.
140
/// But for now, it's our best attempt to document the design and protocol
141
/// implemented here
142
/// for integrating proxies with our RPC system. --nickm
143
///
144
/// Roughly speaking:
145
///
146
/// ## Key concepts
147
///
148
/// A data stream is "RPC-visible" if, when it is created via a proxy connection,
149
/// the RPC system is told about it.
150
///
151
/// Every RPC-visible stream is associated with a given RPC object when it is created.
152
/// (Since the RPC object is being specified in the proxy protocol,
153
/// it must be one with an externally visible Object ID.
154
/// Such Object IDs are cryptographically unguessable and unforgeable,
155
/// and are qualified with a unique identifier for their associated RPC session.)
156
/// Call this RPC Object the "target" object for now.
157
/// This target RPC object must implement
158
/// the [`ConnectWithPrefs`](arti_client::rpc::ConnectWithPrefs) special method.
159
///
160
/// Right now, there are two general kinds of objects that implement this method:
161
/// client-like objects, and one-shot clients.
162
///
163
/// A client-like object is either a `TorClient` or an RPC `Session`.
164
/// It knows about and it is capable of opening multiple data streams.
165
/// Using it as the target object for a proxy connection tells Arti
166
/// that the resulting data stream (if any)
167
/// should be built by it, and associated with its RPC session.
168
///
169
/// An application gets a TorClient by asking the session for one,
170
/// or for asking a TorClient to give you a new variant clone of itself.
171
///
172
/// A one-shot client is an `arti_rpcserver::stream::OneshotClient`.
173
/// It is created from a client-like object, but can only be used for a single data stream.
174
/// When created, it it not yet connected or trying to connect to anywhere:
175
/// the act of using it as the target Object for a proxy connection causes
176
/// it to begin connecting.
177
///
178
/// An application gets a `OneShotClient` by calling `arti:new_oneshot_client`
179
/// on any client-like object.
180
///
181
/// ## The Proxy protocol
182
///
183
/// See the specification for
184
/// [SOCKS extended authentication](https://spec.torproject.org/socks-extensions.html#extended-auth)
185
/// for full details on integrating RPC with SOCKS.
186
/// For HTTP integration, see
187
/// [the relevant section of prop365](https://spec.torproject.org/proposals/365-http-connect-ext.html#x-tor-rpc-target-arti-rpc-support).
188
///
189
/// ### Further restrictions on Object IDs and isolation
190
///
191
/// In some cases,
192
/// the RPC Object ID may denote an object
193
/// that already includes information about its intended stream isolation.
194
/// In such cases, the stream isolation MUST be blank.
195
/// Implementations MUST reject non-blank stream isolation in such cases.
196
///
197
/// In some cases, the RPC object ID may denote an object
198
/// that already includes information
199
/// about its intended destination address and port.
200
/// In such cases, the destination address MUST be `0.0.0.0` or `::`
201
/// (encoded either as an IPv4 address, an IPv6 address, or a hostname)
202
/// and the destination port MUST be 0.
203
/// Implementations MUST reject other addresses in such cases.
204
///
205
/// ### Another proposed change
206
///
207
/// We could add a new method to clients, with a name like
208
/// "open_stream" or "connect_stream".
209
/// This method would include all target and isolation information in its parameters.
210
/// It would actually create a DataStream immediately, tell it to begin connecting,
211
/// and return an externally visible object ID.
212
/// The RPC protocol could be used to watch the DataStream object,
213
/// to see when it was connected.
214
///
215
/// The resulting DataStream object could also be used as the target of a proxy connection.
216
/// We would require in such a case that no isolation be provided in the proxy handshake,
217
/// and that the target address was (e.g.) INADDR_ANY.
218
///
219
/// ## Intended use cases (examples)
220
///
221
/// (These examples assume that the application
222
/// already knows the proxy port it should use.
223
/// I'm leaving out the isolation strings as orthogonal.)
224
///
225
/// These are **NOT** the only possible use cases;
226
/// they're just the two that help understand this system best (I hope).
227
///
228
/// ### Case 1: Using a client-like object directly.
229
///
230
/// Here the application has authenticated to RPC
231
/// and gotten the session ID `SESSION-1`.
232
/// (In reality, this would be a longer ID, and full of crypto).
233
///
234
/// The application wants to open a new stream to www.example.com.
235
/// They don't particularly care about isolation,
236
/// but they do want their stream to use their RPC session.
237
/// They don't want an Object ID for the stream.
238
///
239
/// To do this, they make a SOCKS connection to arti,
240
/// with target address www.example.com.
241
/// They set the username to `<torS0X>0SESSION-1`,
242
/// and the password to the empty string.
243
///
244
/// (Alternatively, it could use HTTP CONNECT, setting
245
/// Tor-Rpc-Target to SESSION-1.)
246
///
247
/// Arti looks up the Session object via the `SESSION-1` object ID
248
/// and tells it (via the ConnectWithPrefs special method)
249
/// to connect to www.example.com.
250
/// The session creates a new DataStream using its internal TorClient,
251
/// but does not register the stream with an RPC Object ID.
252
/// Arti proxies the application's connection through this DataStream.
253
///
254
///
255
/// ### Case 2: Creating an identifiable stream.
256
///
257
/// Here the application wants to be able to refer to its DataStream
258
/// after the stream is created.
259
/// As before, we assume that it's on an RPC session
260
/// where the Session ID is `SESSION-1`.
261
///
262
/// The application sends an RPC request of the form:
263
/// `{"id": 123, "obj": "SESSION-1", "method": "arti:new_oneshot_client", "params": {}}`
264
///
265
/// It receives a reply like:
266
/// `{"id": 123, "result": {"id": "STREAM-1"} }`
267
///
268
/// (In reality, `STREAM-1` would also be longer and full of crypto.)
269
///
270
/// Now the application has an object called `STREAM-1` that is not yet a connected
271
/// stream, but which may become one.
272
///
273
/// This time, it wants to set its isolation string to "xyzzy".
274
///
275
/// The application opens a socks connection as before.
276
/// For the username it sends `<torS0X>0STREAM-1`,
277
/// and for the password it sends `xyzzy`.
278
///
279
/// (Alternatively, it could use HTTP CONNECT, setting Tor-Isolation to xyzzy,
280
/// and Tor-Rpc-Target to STREAM-1.)
281
///
282
/// Now Arti looks up the `RpcDataStream` object via `STREAM-1`,
283
/// and tells it (via the ConnectWithPrefs special method)
284
/// to connect to www.example.com.
285
/// This causes the `RpcDataStream` internally to create a new `DataStream`,
286
/// and to store that `DataStream` in itself.
287
/// The `RpcDataStream` with Object ID `STREAM-1`
288
/// is now an alias for the newly created `DataStream`.
289
/// Arti proxies the application's connection through that `DataStream`.
290
///
291
#[cfg(feature = "rpc")]
292
#[allow(dead_code)]
293
mod socks_and_rpc {}
294

            
295
/// Information used to implement a proxy listener.
296
struct ProxyContext<R: Runtime> {
297
    /// A TorClient to use (by default) to anonymize requests.
298
    tor_client: Arc<TorClient<R>>,
299
    /// If present, an RpcMgr to use when for attaching requests to RPC
300
    /// sessions.
301
    #[cfg(feature = "rpc")]
302
    rpc_mgr: Option<Arc<arti_rpcserver::RpcMgr>>,
303
    /// The protocols that we support.
304
    protocols: ListenProtocols,
305
}
306

            
307
/// Type alias for the isolation information associated with a given proxy
308
/// connection _before_ any negotiation occurs.
309
///
310
/// Currently this is an index for which listener accepted the connection, plus
311
/// the address of the client that connected to the proxy port.
312
type ListenerIsolation = (usize, IpAddr);
313

            
314
/// write_all the data to the writer & flush the writer if write_all is successful.
315
async fn write_all_and_flush<W>(writer: &mut W, buf: &[u8]) -> Result<()>
316
where
317
    W: AsyncWrite + Unpin,
318
{
319
    writer
320
        .write_all(buf)
321
        .await
322
        .context("Error while writing proxy reply")?;
323
    writer
324
        .flush()
325
        .await
326
        .context("Error while flushing proxy stream")
327
}
328

            
329
/// write_all the data to the writer & close the writer if write_all is successful.
330
async fn write_all_and_close<W>(writer: &mut W, buf: &[u8]) -> Result<()>
331
where
332
    W: AsyncWrite + Unpin,
333
{
334
    writer
335
        .write_all(buf)
336
        .await
337
        .context("Error while writing proxy reply")?;
338
    writer
339
        .close()
340
        .await
341
        .context("Error while closing proxy stream")
342
}
343

            
344
/// Return true if a given IoError, when received from accept, is a fatal
345
/// error.
346
fn accept_err_is_fatal(err: &IoError) -> bool {
347
    #![allow(clippy::match_like_matches_macro)]
348

            
349
    /// Re-declaration of WSAEMFILE with the right type to match
350
    /// `raw_os_error()`.
351
    #[cfg(windows)]
352
    const WSAEMFILE: i32 = winapi::shared::winerror::WSAEMFILE as i32;
353

            
354
    // Currently, EMFILE and ENFILE aren't distinguished by ErrorKind;
355
    // we need to use OS-specific errors. :P
356
    match err.raw_os_error() {
357
        #[cfg(unix)]
358
        Some(libc::EMFILE) | Some(libc::ENFILE) => false,
359
        #[cfg(windows)]
360
        Some(WSAEMFILE) => false,
361
        _ => true,
362
    }
363
}
364

            
365
/// A stream proxy listening on one or more local ports, ready to relay traffic.
366
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
367
#[must_use]
368
pub(crate) struct StreamProxy<R: Runtime> {
369
    /// A tor client to use when relaying traffic.
370
    tor_client: Arc<TorClient<R>>,
371
    /// The listeners that we've actually bound to.
372
    listeners: Vec<<R as NetStreamProvider>::Listener>,
373
    /// The protocols we respond to.
374
    protocols: ListenProtocols,
375
    /// An RPC manager to use when incoming requests are tied to streams.
376
    rpc_mgr: Option<Arc<RpcMgr>>,
377
}
378

            
379
/// Launch a proxy to listen on a given set of ports.
380
///
381
/// Requires a `runtime` to use for launching tasks and handling
382
/// timeouts, and a `tor_client` to use in connecting over the Tor
383
/// network.
384
///
385
/// Returns the proxy, and a list of the ports that we have
386
/// bound to.
387
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
388
#[instrument(skip_all, level = "trace")]
389
pub(crate) async fn bind_proxy<R: Runtime>(
390
    runtime: R,
391
    tor_client: Arc<TorClient<R>>,
392
    listen: Listen,
393
    listen_options: TcpListenOptions,
394
    protocols: ListenProtocols,
395
    rpc_mgr: Option<Arc<RpcMgr>>,
396
) -> Result<StreamProxy<R>> {
397
    if !listen.is_loopback_only() {
398
        warn!(
399
            "Configured to listen for proxy connections on non-local addresses. \
400
            This is usually insecure! We recommend listening on localhost only."
401
        );
402
    }
403

            
404
    let mut listeners = Vec::new();
405

            
406
    // Try to bind to the listener ports.
407
    match listen.ip_addrs() {
408
        Ok(addrgroups) => {
409
            for addrgroup in addrgroups {
410
                for addr in addrgroup {
411
                    match runtime.listen(&addr, &listen_options).await {
412
                        Ok(listener) => {
413
                            let bound_addr = listener.local_addr()?;
414
                            info!("Listening on {:?}", bound_addr);
415
                            listeners.push(listener);
416
                        }
417
                        #[cfg(unix)]
418
                        Err(ref e) if e.raw_os_error() == Some(libc::EAFNOSUPPORT) => {
419
                            warn_report!(e, "Address family not supported {}", addr);
420
                        }
421
                        Err(ref e) => {
422
                            return Err(anyhow!("Can't listen on {}: {e}", addr));
423
                        }
424
                    }
425
                }
426
                // TODO: We are supposed to fail if every address in the group failed!
427
            }
428
        }
429
        Err(e) => warn_report!(e, "Invalid listen spec"),
430
    }
431

            
432
    // We weren't able to bind any ports: There's nothing to do.
433
    if listeners.is_empty() {
434
        error!("Couldn't open any listeners.");
435
        return Err(anyhow!("Couldn't open listeners"));
436
    }
437

            
438
    Ok(StreamProxy {
439
        tor_client,
440
        listeners,
441
        protocols,
442
        rpc_mgr,
443
    })
444
}
445

            
446
impl<R: Runtime> StreamProxy<R> {
447
    /// Run indefinitely, processing incoming connections and relaying traffic.
448
    pub(crate) async fn run_proxy(self) -> Result<()> {
449
        let StreamProxy {
450
            tor_client,
451
            listeners,
452
            protocols,
453
            rpc_mgr,
454
        } = self;
455
        run_proxy_with_listeners(tor_client, listeners, protocols, rpc_mgr).await
456
    }
457

            
458
    /// Return a list of the ports that we've bound to.
459
    pub(crate) fn port_info(&self) -> Result<Vec<port_info::Port>> {
460
        let mut ports = Vec::new();
461
        for listener in &self.listeners {
462
            let address = listener.local_addr()?;
463
            ports.push(port_info::Port {
464
                protocol: port_info::SupportedProtocol::Socks,
465
                address: address.into(),
466
            });
467
            #[cfg(feature = "http-connect")]
468
            if self.protocols.http_connect_supported() {
469
                ports.push(port_info::Port {
470
                    protocol: port_info::SupportedProtocol::Http,
471
                    address: address.into(),
472
                });
473
            }
474
        }
475

            
476
        Ok(ports)
477
    }
478
}
479

            
480
/// Launch a proxy from a given set of already bound listeners.
481
#[cfg_attr(feature = "experimental-api", visibility::make(pub))]
482
#[instrument(skip_all, level = "trace")]
483
pub(crate) async fn run_proxy_with_listeners<R: Runtime>(
484
    tor_client: Arc<TorClient<R>>,
485
    listeners: Vec<<R as tor_rtcompat::NetStreamProvider>::Listener>,
486
    protocols: ListenProtocols,
487
    rpc_mgr: Option<Arc<RpcMgr>>,
488
) -> Result<()> {
489
    // Create a stream of (incoming socket, listener_id) pairs, selected
490
    // across all the listeners.
491
    let mut incoming = futures::stream::select_all(
492
        listeners
493
            .into_iter()
494
            .map(NetStreamListener::incoming)
495
            .enumerate()
496
            .map(|(listener_id, incoming_conns)| {
497
                incoming_conns.map(move |socket| (socket, listener_id))
498
            }),
499
    );
500

            
501
    // Loop over all incoming connections.  For each one, call
502
    // handle_proxy_conn() in a new task.
503
    while let Some((stream, sock_id)) = incoming.next().await {
504
        let (stream, addr) = match stream {
505
            Ok((s, a)) => (s, a),
506
            Err(err) => {
507
                if accept_err_is_fatal(&err) {
508
                    return Err(err).context("Failed to receive incoming stream on proxy port");
509
                } else {
510
                    warn_report!(err, "Incoming stream failed");
511
                    continue;
512
                }
513
            }
514
        };
515
        let proxy_context = ProxyContext {
516
            tor_client: tor_client.clone(),
517
            #[cfg(feature = "rpc")]
518
            rpc_mgr: rpc_mgr.clone(),
519
            protocols,
520
        };
521
        tor_client.runtime().spawn(async move {
522
            let res = handle_proxy_conn(proxy_context, stream, (sock_id, addr.ip())).await;
523
            if let Err(e) = res {
524
                report_proxy_error(e);
525
            }
526
        })?;
527
    }
528

            
529
    Ok(())
530
}
531

            
532
/// A (possibly) supported proxy protocol.
533
enum ProxyProtocols {
534
    /// Some HTTP/1 command or other.
535
    ///
536
    /// (We only support CONNECT and OPTIONS, but we reject other commands in [`http_connect`].)
537
    Http1,
538
    /// SOCKS4 or SOCKS5.
539
    Socks,
540
}
541

            
542
/// Look at the first byte of a proxy connection, and guess what protocol
543
/// what protocol it is trying to speak.
544
fn classify_protocol_from_first_byte(byte: u8) -> Option<ProxyProtocols> {
545
    match byte {
546
        b'a'..=b'z' | b'A'..=b'Z' => Some(ProxyProtocols::Http1),
547
        4 | 5 => Some(ProxyProtocols::Socks),
548
        _ => None,
549
    }
550
}
551

            
552
/// Handle a single connection `stream` from an application.
553
///
554
/// Depending on what protocol the application is speaking
555
/// (and what protocols we support!), negotiate an appropriate set of options,
556
/// and relay traffic to and from the application.
557
async fn handle_proxy_conn<R, S>(
558
    context: ProxyContext<R>,
559
    stream: S,
560
    isolation_info: ListenerIsolation,
561
) -> Result<()>
562
where
563
    R: Runtime,
564
    S: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static,
565
{
566
    let mut stream = BufReader::with_capacity(APP_STREAM_BUF_LEN, stream);
567
    use futures::AsyncBufReadExt as _;
568

            
569
    let buf: &[u8] = stream.fill_buf().await?;
570
    if buf.is_empty() {
571
        // connection closed
572
        return Ok(());
573
    }
574
    match classify_protocol_from_first_byte(buf[0]) {
575
        Some(ProxyProtocols::Http1) => {
576
            #[cfg(feature = "http-connect")]
577
            if context.protocols.http_connect_supported() {
578
                return http_connect::handle_http_conn(context, stream, isolation_info).await;
579
            }
580

            
581
            write_all_and_close(&mut stream, socks::WRONG_PROTOCOL_PAYLOAD).await?;
582
            Ok(())
583
        }
584
        Some(ProxyProtocols::Socks) => {
585
            socks::handle_socks_conn(context, stream, isolation_info).await
586
        }
587
        None => {
588
            // We have no idea what protocol the client expects,
589
            // so we have no idea how to tell it so.
590
            warn!(
591
                "Unrecognized protocol on proxy listener (first byte {:x})",
592
                buf[0]
593
            );
594
            Ok(())
595
        }
596
    }
597
}
598

            
599
/// If any source of the provided `error` is a [`tor_proto::Error`], return a reference to that
600
/// [`tor_proto::Error`].
601
fn extract_proto_err<'a>(
602
    error: &'a (dyn std::error::Error + 'static),
603
) -> Option<&'a tor_proto::Error> {
604
    for error in ErrorSources::new(error) {
605
        if let Some(downcast) = error.downcast_ref::<tor_proto::Error>() {
606
            return Some(downcast);
607
        }
608
    }
609

            
610
    None
611
}
612

            
613
/// A wrapper that makes anyhow::Error Sized and Cloneable
614
/// by wrapping it in an Arc.
615
// TODO Add better handling for anyhow errors in tor-log-ratelim
616
// create tor-log-ratelim/src/anyhow.rs and put them there
617
#[derive(Debug, Clone, Display)]
618
#[display("{}", _0)]
619
pub(crate) struct RateLimitError(Arc<anyhow::Error>);
620

            
621
impl RateLimitError {
622
    /// Creates a new `RateLimitError` from an `anyhow::Error`.
623
    pub(crate) fn new(e: anyhow::Error) -> Self {
624
        Self(Arc::new(e))
625
    }
626
}
627

            
628
impl std::error::Error for RateLimitError {}
629

            
630
/// Wraps a `Result` containing an `anyhow::Error` into a `Result`
631
/// containing a `Sized` `RateLimitError` to allow usage with `log_ratelim!`.
632
#[ext]
633
impl<T> Result<T, anyhow::Error> {
634
    /// Wraps an `anyhow::Error` into a `Sized` `RateLimitError`
635
    fn wrap_for_ratelimit(self) -> Result<T, RateLimitError> {
636
        self.map_err(RateLimitError::new)
637
    }
638
}
639

            
640
/// Report an error that occurred within a single proxy task.
641
fn report_proxy_error(e: anyhow::Error) {
642
    use tor_proto::Error as PE;
643
    // TODO: In the long run it might be a good idea to use an ErrorKind here if we can get one.
644
    // This is a bit of a kludge based on the fact that we're using anyhow.
645
    //
646
    // TODO: It might be handy to have a way to collapse CircuitClosed into EOF earlier.
647
    // But that loses information, so it should be optional.
648
    //
649
    // TODO: Maybe we should look at io::ErrorKind as well, if it's there.  That's another reason
650
    // to discard or restrict our anyhow usage.
651
    match extract_proto_err(e.as_ref()) {
652
        Some(e @ PE::CircuitClosed) => debug_report!(e, "Connection exited"),
653
        // We can't use `debug_report!` here because `NotConnected`s error kind is `BadApiUsage`,
654
        // which upgrades this to a warning.
655
        // https://gitlab.torproject.org/tpo/core/arti/-/issues/2439
656
        Some(e @ PE::NotConnected) => debug!(error = (e as &dyn std::error::Error), "Connection exited"),
657
        _ => {
658
            // TODO: See https://gitlab.torproject.org/tpo/core/arti/-/work_items/2632
659
            // We use the root cause of the error as the activity key for rate-limiting
660
            // because it provides only the original OS error without any wrappers on the text.
661
            // This avoids a manual if-else chain while maintaining separate buckets
662
            // for different errors.
663
            let bucket = e.root_cause().to_string();
664
            let r: Result<(), RateLimitError> = Err(e).wrap_for_ratelimit();
665

            
666
            log_ratelim!(
667
                "Connection exited (cause: {})", bucket;
668
                r;
669
            );
670
        }
671
    }
672
}