tor_chanmgr/transport/
proxied.rs

1//! Connect to relays via a proxy.
2//!
3//! This code is here for two reasons:
4//!   1. To connect via external pluggable transports (for which we use SOCKS to
5//!      build our connections).
6//!   2. To support users who are behind a firewall that requires them to use a
7//!      SOCKS proxy to connect.
8//!
9//! Currently only SOCKS proxies are supported.
10//
11// TODO: Add support for `HTTP(S) CONNECT` someday?
12//
13// TODO: Maybe refactor this so that tor-ptmgr can exist in a more freestanding
14// way, with fewer arti dependencies.
15#![allow(dead_code)]
16
17use std::{
18    net::{IpAddr, SocketAddr},
19    sync::Arc,
20};
21
22use futures::{AsyncReadExt, AsyncWriteExt};
23use tor_linkspec::PtTargetAddr;
24use tor_rtcompat::NetStreamProvider;
25use tor_socksproto::{
26    Handshake as _, SocksAddr, SocksAuth, SocksClientHandshake, SocksCmd, SocksRequest,
27    SocksStatus, SocksVersion,
28};
29use tracing::trace;
30
31#[cfg(feature = "pt-client")]
32use super::TransportImplHelper;
33#[cfg(feature = "pt-client")]
34use async_trait::async_trait;
35#[cfg(feature = "pt-client")]
36use tor_error::bad_api_usage;
37#[cfg(feature = "pt-client")]
38use tor_linkspec::{ChannelMethod, HasChanMethod, OwnedChanTarget};
39
40/// Information about what proxy protocol to use, and how to use it.
41#[derive(Clone, Debug, Eq, PartialEq)]
42#[non_exhaustive]
43pub enum Protocol {
44    /// Connect via SOCKS 4, SOCKS 4a, or SOCKS 5.
45    Socks(SocksVersion, SocksAuth),
46}
47
48/// An address to use when told to connect to "no address."
49const NO_ADDR: IpAddr = IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 1));
50
51/// Open a connection to `target` via the proxy at `proxy`, using the protocol
52/// at `protocol`.
53///
54/// # Limitations
55///
56/// We will give an error if the proxy sends us any data on the connection along
57/// with its final handshake: due to our implementation, any such data will be
58/// discarded, and so we give an error rather than fail silently.
59///
60/// This limitation doesn't matter when the underlying protocol is Tor, or
61/// anything else where the initiator is expected to speak before the responder
62/// says anything.  To lift it, we would have to make this function's return
63/// type become something buffered.
64//
65// TODO: Perhaps we should refactor this someday so it can be a general-purpose
66// proxy function, not only for Arti.
67pub(crate) async fn connect_via_proxy<R: NetStreamProvider + Send + Sync>(
68    runtime: &R,
69    proxy: &SocketAddr,
70    protocol: &Protocol,
71    target: &PtTargetAddr,
72) -> Result<R::Stream, ProxyError> {
73    trace!(
74        "Launching a proxied connection to {} via proxy at {} using {:?}",
75        target, proxy, protocol
76    );
77    let mut stream = runtime
78        .connect(proxy)
79        .await
80        .map_err(|e| ProxyError::ProxyConnect(Arc::new(e)))?;
81
82    let Protocol::Socks(version, auth) = protocol;
83
84    let (target_addr, target_port): (tor_socksproto::SocksAddr, u16) = match target {
85        PtTargetAddr::IpPort(a) => (SocksAddr::Ip(a.ip()), a.port()),
86        #[cfg(feature = "pt-client")]
87        PtTargetAddr::HostPort(host, port) => (
88            SocksAddr::Hostname(
89                host.clone()
90                    .try_into()
91                    .map_err(ProxyError::InvalidSocksAddr)?,
92            ),
93            *port,
94        ),
95        #[cfg(feature = "pt-client")]
96        PtTargetAddr::None => (SocksAddr::Ip(NO_ADDR), 1),
97        _ => return Err(ProxyError::UnrecognizedAddr),
98    };
99
100    let request = SocksRequest::new(
101        *version,
102        SocksCmd::CONNECT,
103        target_addr,
104        target_port,
105        auth.clone(),
106    )
107    .map_err(ProxyError::InvalidSocksRequest)?;
108    let mut handshake = SocksClientHandshake::new(request);
109
110    // TODO: This code is largely copied from the socks server wrapper code in
111    // arti::proxy. Perhaps we should condense them into a single thing, if we
112    // don't just revise the SOCKS code completely.
113    let mut buf = tor_socksproto::Buffer::new();
114    let reply = loop {
115        use tor_socksproto::NextStep as NS;
116        match handshake.step(&mut buf).map_err(ProxyError::SocksProto)? {
117            NS::Send(send) => {
118                stream.write_all(&send).await?;
119                stream.flush().await?;
120            }
121            NS::Finished(fin) => {
122                break fin
123                    .into_output_forbid_pipelining()
124                    .map_err(ProxyError::SocksProto)?;
125            }
126            NS::Recv(mut recv) => {
127                let n = stream.read(recv.buf()).await?;
128                recv.note_received(n).map_err(ProxyError::SocksProto)?;
129            }
130        }
131    };
132
133    let status = reply.status();
134    trace!(
135        "SOCKS handshake with {} succeeded, with status {:?}",
136        proxy, status
137    );
138
139    if status != SocksStatus::SUCCEEDED {
140        return Err(ProxyError::SocksError(status));
141    }
142
143    Ok(stream)
144}
145
146/// An error that occurs while negotiating a connection with a proxy.
147#[derive(Clone, Debug, thiserror::Error)]
148#[non_exhaustive]
149pub enum ProxyError {
150    /// We had an IO error while trying to open a connection to the proxy.
151    #[error("Problem while connecting to proxy")]
152    ProxyConnect(#[source] Arc<std::io::Error>),
153
154    /// We had an IO error while talking to the proxy.
155    #[error("Problem while communicating with proxy")]
156    ProxyIo(#[source] Arc<std::io::Error>),
157
158    /// We tried to use an address which socks doesn't support.
159    #[error("SOCKS proxy does not support target address")]
160    InvalidSocksAddr(#[source] tor_socksproto::Error),
161
162    /// We tried to use an address type which _we_ don't recognize.
163    #[error("Got an address type we don't recognize")]
164    UnrecognizedAddr,
165
166    /// Our SOCKS implementation told us that this request cannot be encoded.
167    #[error("Tried to make an invalid SOCKS request")]
168    InvalidSocksRequest(#[source] tor_socksproto::Error),
169
170    /// The peer refused our request, or spoke SOCKS incorrectly.
171    #[error("Protocol error while communicating with SOCKS proxy")]
172    SocksProto(#[source] tor_socksproto::Error),
173
174    /// We encountered an internal programming error.
175    #[error("Internal error")]
176    Bug(#[from] tor_error::Bug),
177
178    /// We got extra data immediately after our handshake, before we actually
179    /// sent anything.
180    ///
181    /// This is not a bug in the calling code or in the peer protocol: it just
182    /// means that the remote peer sent us data before we actually sent it any
183    /// data. Unfortunately, there's a limitation in our code that makes it
184    /// discard any such data, and therefore we have to give this error to
185    /// prevent bugs.
186    ///
187    /// We could someday remove this limitation.
188    #[error("Received unexpected early data from peer")]
189    UnexpectedData,
190
191    /// The proxy told us that our attempt failed.
192    #[error("SOCKS proxy reported an error: {0}")]
193    SocksError(SocksStatus),
194}
195
196impl From<std::io::Error> for ProxyError {
197    fn from(e: std::io::Error) -> Self {
198        ProxyError::ProxyIo(Arc::new(e))
199    }
200}
201
202impl tor_error::HasKind for ProxyError {
203    fn kind(&self) -> tor_error::ErrorKind {
204        use ProxyError as E;
205        use tor_error::ErrorKind as EK;
206        match self {
207            E::ProxyConnect(_) | E::ProxyIo(_) => EK::LocalNetworkError,
208            E::InvalidSocksAddr(_) | E::InvalidSocksRequest(_) => EK::BadApiUsage,
209            E::UnrecognizedAddr => EK::NotImplemented,
210            E::SocksProto(_) => EK::LocalProtocolViolation,
211            E::Bug(e) => e.kind(),
212            E::UnexpectedData => EK::NotImplemented,
213            E::SocksError(_) => EK::LocalProtocolViolation,
214        }
215    }
216}
217
218impl tor_error::HasRetryTime for ProxyError {
219    fn retry_time(&self) -> tor_error::RetryTime {
220        use ProxyError as E;
221        use SocksStatus as S;
222        use tor_error::RetryTime as RT;
223        match self {
224            E::ProxyConnect(_) | E::ProxyIo(_) => RT::AfterWaiting,
225            E::InvalidSocksAddr(_) => RT::Never,
226            E::UnrecognizedAddr => RT::Never,
227            E::InvalidSocksRequest(_) => RT::Never,
228            E::SocksProto(_) => RT::AfterWaiting,
229            E::Bug(_) => RT::Never,
230            E::UnexpectedData => RT::Never,
231            E::SocksError(e) => match *e {
232                S::CONNECTION_REFUSED
233                | S::GENERAL_FAILURE
234                | S::HOST_UNREACHABLE
235                | S::NETWORK_UNREACHABLE
236                | S::TTL_EXPIRED => RT::AfterWaiting,
237                _ => RT::Never,
238            },
239        }
240    }
241}
242
243#[cfg(feature = "pt-client")]
244/// An object that connects to a Tor bridge via an external pluggable transport
245/// that provides a proxy.
246#[derive(Clone, Debug)]
247pub struct ExternalProxyPlugin<R> {
248    /// The runtime to use for connections.
249    runtime: R,
250    /// The location of the proxy.
251    proxy_addr: SocketAddr,
252    /// The SOCKS protocol version to use.
253    proxy_version: SocksVersion,
254}
255
256#[cfg(feature = "pt-client")]
257impl<R: NetStreamProvider + Send + Sync> ExternalProxyPlugin<R> {
258    /// Make a new `ExternalProxyPlugin`.
259    pub fn new(rt: R, proxy_addr: SocketAddr, proxy_version: SocksVersion) -> Self {
260        Self {
261            runtime: rt,
262            proxy_addr,
263            proxy_version,
264        }
265    }
266}
267
268#[cfg(feature = "pt-client")]
269#[async_trait]
270impl<R: NetStreamProvider + Send + Sync> TransportImplHelper for ExternalProxyPlugin<R> {
271    type Stream = R::Stream;
272
273    async fn connect(
274        &self,
275        target: &OwnedChanTarget,
276    ) -> crate::Result<(OwnedChanTarget, R::Stream)> {
277        let pt_target = match target.chan_method() {
278            ChannelMethod::Direct(_) => {
279                return Err(crate::Error::UnusableTarget(bad_api_usage!(
280                    "Used pluggable transport for a TCP connection."
281                )));
282            }
283            ChannelMethod::Pluggable(target) => target,
284            other => {
285                return Err(crate::Error::UnusableTarget(bad_api_usage!(
286                    "Used unknown, unsupported, transport {:?} for a TCP connection.",
287                    other,
288                )));
289            }
290        };
291
292        let protocol =
293            settings_to_protocol(self.proxy_version, encode_settings(pt_target.settings()))?;
294
295        Ok((
296            target.clone(),
297            connect_via_proxy(&self.runtime, &self.proxy_addr, &protocol, pt_target.addr()).await?,
298        ))
299    }
300}
301
302/// Encode the PT settings from `IT` in a format that a pluggable transport can use.
303#[cfg(feature = "pt-client")]
304fn encode_settings<'a, IT>(settings: IT) -> String
305where
306    IT: Iterator<Item = (&'a str, &'a str)>,
307{
308    /// Escape a character in the way expected by pluggable transports.
309    ///
310    /// This escape machinery is a mirror of that in the standard library.
311    enum EscChar {
312        /// Return a backslash then a character.
313        Backslash(char),
314        /// Return a character.
315        Literal(char),
316        /// Return nothing.
317        Done,
318    }
319    impl EscChar {
320        /// Create an iterator to escape one character.
321        fn new(ch: char, in_key: bool) -> Self {
322            match ch {
323                '\\' | ';' => EscChar::Backslash(ch),
324                '=' if in_key => EscChar::Backslash(ch),
325                _ => EscChar::Literal(ch),
326            }
327        }
328    }
329    impl Iterator for EscChar {
330        type Item = char;
331
332        fn next(&mut self) -> Option<Self::Item> {
333            match *self {
334                EscChar::Backslash(ch) => {
335                    *self = EscChar::Literal(ch);
336                    Some('\\')
337                }
338                EscChar::Literal(ch) => {
339                    *self = EscChar::Done;
340                    Some(ch)
341                }
342                EscChar::Done => None,
343            }
344        }
345    }
346
347    /// escape a key or value string.
348    fn esc(s: &str, in_key: bool) -> impl Iterator<Item = char> + '_ {
349        s.chars().flat_map(move |c| EscChar::new(c, in_key))
350    }
351
352    let mut result = String::new();
353    for (k, v) in settings {
354        result.extend(esc(k, true));
355        result.push('=');
356        result.extend(esc(v, false));
357        result.push(';');
358    }
359    result.pop(); // remove the final ';' if any. Yes this is ugly.
360
361    result
362}
363
364/// Transform a string into a representation that can be sent as SOCKS
365/// authentication.
366// NOTE(eta): I am very unsure of the logic in here.
367#[cfg(feature = "pt-client")]
368pub fn settings_to_protocol(vers: SocksVersion, s: String) -> Result<Protocol, ProxyError> {
369    let mut bytes: Vec<_> = s.into();
370    Ok(if bytes.is_empty() {
371        Protocol::Socks(vers, SocksAuth::NoAuth)
372    } else if vers == SocksVersion::V4 {
373        if bytes.contains(&0) {
374            return Err(ProxyError::InvalidSocksRequest(
375                tor_socksproto::Error::NotImplemented(
376                    "SOCKS 4 doesn't support internal NUL bytes (for PT settings list)".into(),
377                ),
378            ));
379        } else {
380            Protocol::Socks(SocksVersion::V4, SocksAuth::Socks4(bytes))
381        }
382    } else if bytes.len() <= 255 {
383        // The [0] here is mandatory according to the pt-spec.
384        Protocol::Socks(SocksVersion::V5, SocksAuth::Username(bytes, vec![0]))
385    } else if bytes.len() <= (255 * 2) {
386        let password = bytes.split_off(255);
387        Protocol::Socks(SocksVersion::V5, SocksAuth::Username(bytes, password))
388    } else {
389        return Err(ProxyError::InvalidSocksRequest(
390            tor_socksproto::Error::NotImplemented("PT settings list too long for SOCKS 5".into()),
391        ));
392    })
393}
394
395#[cfg(test)]
396mod test {
397    // @@ begin test lint list maintained by maint/add_warning @@
398    #![allow(clippy::bool_assert_comparison)]
399    #![allow(clippy::clone_on_copy)]
400    #![allow(clippy::dbg_macro)]
401    #![allow(clippy::mixed_attributes_style)]
402    #![allow(clippy::print_stderr)]
403    #![allow(clippy::print_stdout)]
404    #![allow(clippy::single_char_pattern)]
405    #![allow(clippy::unwrap_used)]
406    #![allow(clippy::unchecked_time_subtraction)]
407    #![allow(clippy::useless_vec)]
408    #![allow(clippy::needless_pass_by_value)]
409    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
410    #[allow(unused_imports)]
411    use super::*;
412
413    #[cfg(feature = "pt-client")]
414    #[test]
415    fn setting_encoding() {
416        fn check(settings: Vec<(&str, &str)>, expected: &str) {
417            assert_eq!(encode_settings(settings.into_iter()), expected);
418        }
419
420        // Easy cases, no escapes.
421        check(vec![], "");
422        check(vec![("hello", "world")], "hello=world");
423        check(
424            vec![("hey", "verden"), ("hello", "world")],
425            "hey=verden;hello=world",
426        );
427        check(
428            vec![("hey", "verden"), ("hello", "world"), ("selv", "tak")],
429            "hey=verden;hello=world;selv=tak",
430        );
431
432        check(
433            vec![("semi;colon", "equals=sign")],
434            r"semi\;colon=equals=sign",
435        );
436        check(
437            vec![("equals=sign", "semi;colon")],
438            r"equals\=sign=semi\;colon",
439        );
440        check(
441            vec![("semi;colon", "equals=sign"), ("also", "back\\slash")],
442            r"semi\;colon=equals=sign;also=back\\slash",
443        );
444    }
445
446    #[cfg(feature = "pt-client")]
447    #[test]
448    fn split_settings() {
449        use SocksVersion::*;
450        let long_string = "examplestrg".to_owned().repeat(50);
451        assert_eq!(long_string.len(), 550);
452        let sv = |v, a, b| settings_to_protocol(v, long_string[a..b].to_owned()).unwrap();
453        let s = |a, b| sv(V5, a, b);
454        let v = |a, b| long_string.as_bytes()[a..b].to_vec();
455
456        assert_eq!(s(0, 0), Protocol::Socks(V5, SocksAuth::NoAuth));
457        assert_eq!(
458            s(0, 50),
459            Protocol::Socks(V5, SocksAuth::Username(v(0, 50), vec![0]))
460        );
461        assert_eq!(
462            s(0, 255),
463            Protocol::Socks(V5, SocksAuth::Username(v(0, 255), vec![0]))
464        );
465        assert_eq!(
466            s(0, 256),
467            Protocol::Socks(V5, SocksAuth::Username(v(0, 255), v(255, 256)))
468        );
469        assert_eq!(
470            s(0, 300),
471            Protocol::Socks(V5, SocksAuth::Username(v(0, 255), v(255, 300)))
472        );
473        assert_eq!(
474            s(0, 510),
475            Protocol::Socks(V5, SocksAuth::Username(v(0, 255), v(255, 510)))
476        );
477
478        // This one needs to use socks4, or it won't fit. :P
479        assert_eq!(
480            sv(V4, 0, 511),
481            Protocol::Socks(V4, SocksAuth::Socks4(v(0, 511)))
482        );
483
484        // Small requests with "0" bytes work fine...
485        assert_eq!(
486            settings_to_protocol(V5, "\0".to_owned()).unwrap(),
487            Protocol::Socks(V5, SocksAuth::Username(vec![0], vec![0]))
488        );
489        assert_eq!(
490            settings_to_protocol(V5, "\0".to_owned().repeat(510)).unwrap(),
491            Protocol::Socks(V5, SocksAuth::Username(vec![0; 255], vec![0; 255]))
492        );
493
494        // Huge requests with "0" simply can't be encoded.
495        assert!(settings_to_protocol(V5, "\0".to_owned().repeat(511)).is_err());
496
497        // Huge requests without "0" can't be encoded as V5
498        assert!(settings_to_protocol(V5, long_string[0..512].to_owned()).is_err());
499
500        // Requests with "0" can't be encoded as V4.
501        assert!(settings_to_protocol(V4, "\0".to_owned()).is_err());
502    }
503}