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#[cfg_attr(docsrs, doc(cfg(feature = "pt-client")))]
245/// An object that connects to a Tor bridge via an external pluggable transport
246/// that provides a proxy.
247#[derive(Clone, Debug)]
248pub struct ExternalProxyPlugin<R> {
249    /// The runtime to use for connections.
250    runtime: R,
251    /// The location of the proxy.
252    proxy_addr: SocketAddr,
253    /// The SOCKS protocol version to use.
254    proxy_version: SocksVersion,
255}
256
257#[cfg(feature = "pt-client")]
258#[cfg_attr(docsrs, doc(cfg(feature = "pt-client")))]
259impl<R: NetStreamProvider + Send + Sync> ExternalProxyPlugin<R> {
260    /// Make a new `ExternalProxyPlugin`.
261    pub fn new(rt: R, proxy_addr: SocketAddr, proxy_version: SocksVersion) -> Self {
262        Self {
263            runtime: rt,
264            proxy_addr,
265            proxy_version,
266        }
267    }
268}
269
270#[cfg(feature = "pt-client")]
271#[async_trait]
272impl<R: NetStreamProvider + Send + Sync> TransportImplHelper for ExternalProxyPlugin<R> {
273    type Stream = R::Stream;
274
275    async fn connect(
276        &self,
277        target: &OwnedChanTarget,
278    ) -> crate::Result<(OwnedChanTarget, R::Stream)> {
279        let pt_target = match target.chan_method() {
280            ChannelMethod::Direct(_) => {
281                return Err(crate::Error::UnusableTarget(bad_api_usage!(
282                    "Used pluggable transport for a TCP connection."
283                )));
284            }
285            ChannelMethod::Pluggable(target) => target,
286            other => {
287                return Err(crate::Error::UnusableTarget(bad_api_usage!(
288                    "Used unknown, unsupported, transport {:?} for a TCP connection.",
289                    other,
290                )));
291            }
292        };
293
294        let protocol =
295            settings_to_protocol(self.proxy_version, encode_settings(pt_target.settings()))?;
296
297        Ok((
298            target.clone(),
299            connect_via_proxy(&self.runtime, &self.proxy_addr, &protocol, pt_target.addr()).await?,
300        ))
301    }
302}
303
304/// Encode the PT settings from `IT` in a format that a pluggable transport can use.
305#[cfg(feature = "pt-client")]
306fn encode_settings<'a, IT>(settings: IT) -> String
307where
308    IT: Iterator<Item = (&'a str, &'a str)>,
309{
310    /// Escape a character in the way expected by pluggable transports.
311    ///
312    /// This escape machinery is a mirror of that in the standard library.
313    enum EscChar {
314        /// Return a backslash then a character.
315        Backslash(char),
316        /// Return a character.
317        Literal(char),
318        /// Return nothing.
319        Done,
320    }
321    impl EscChar {
322        /// Create an iterator to escape one character.
323        fn new(ch: char, in_key: bool) -> Self {
324            match ch {
325                '\\' | ';' => EscChar::Backslash(ch),
326                '=' if in_key => EscChar::Backslash(ch),
327                _ => EscChar::Literal(ch),
328            }
329        }
330    }
331    impl Iterator for EscChar {
332        type Item = char;
333
334        fn next(&mut self) -> Option<Self::Item> {
335            match *self {
336                EscChar::Backslash(ch) => {
337                    *self = EscChar::Literal(ch);
338                    Some('\\')
339                }
340                EscChar::Literal(ch) => {
341                    *self = EscChar::Done;
342                    Some(ch)
343                }
344                EscChar::Done => None,
345            }
346        }
347    }
348
349    /// escape a key or value string.
350    fn esc(s: &str, in_key: bool) -> impl Iterator<Item = char> + '_ {
351        s.chars().flat_map(move |c| EscChar::new(c, in_key))
352    }
353
354    let mut result = String::new();
355    for (k, v) in settings {
356        result.extend(esc(k, true));
357        result.push('=');
358        result.extend(esc(v, false));
359        result.push(';');
360    }
361    result.pop(); // remove the final ';' if any. Yes this is ugly.
362
363    result
364}
365
366/// Transform a string into a representation that can be sent as SOCKS
367/// authentication.
368// NOTE(eta): I am very unsure of the logic in here.
369#[cfg(feature = "pt-client")]
370pub fn settings_to_protocol(vers: SocksVersion, s: String) -> Result<Protocol, ProxyError> {
371    let mut bytes: Vec<_> = s.into();
372    Ok(if bytes.is_empty() {
373        Protocol::Socks(vers, SocksAuth::NoAuth)
374    } else if vers == SocksVersion::V4 {
375        if bytes.contains(&0) {
376            return Err(ProxyError::InvalidSocksRequest(
377                tor_socksproto::Error::NotImplemented(
378                    "SOCKS 4 doesn't support internal NUL bytes (for PT settings list)".into(),
379                ),
380            ));
381        } else {
382            Protocol::Socks(SocksVersion::V4, SocksAuth::Socks4(bytes))
383        }
384    } else if bytes.len() <= 255 {
385        // The [0] here is mandatory according to the pt-spec.
386        Protocol::Socks(SocksVersion::V5, SocksAuth::Username(bytes, vec![0]))
387    } else if bytes.len() <= (255 * 2) {
388        let password = bytes.split_off(255);
389        Protocol::Socks(SocksVersion::V5, SocksAuth::Username(bytes, password))
390    } else {
391        return Err(ProxyError::InvalidSocksRequest(
392            tor_socksproto::Error::NotImplemented("PT settings list too long for SOCKS 5".into()),
393        ));
394    })
395}
396
397#[cfg(test)]
398mod test {
399    // @@ begin test lint list maintained by maint/add_warning @@
400    #![allow(clippy::bool_assert_comparison)]
401    #![allow(clippy::clone_on_copy)]
402    #![allow(clippy::dbg_macro)]
403    #![allow(clippy::mixed_attributes_style)]
404    #![allow(clippy::print_stderr)]
405    #![allow(clippy::print_stdout)]
406    #![allow(clippy::single_char_pattern)]
407    #![allow(clippy::unwrap_used)]
408    #![allow(clippy::unchecked_duration_subtraction)]
409    #![allow(clippy::useless_vec)]
410    #![allow(clippy::needless_pass_by_value)]
411    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
412    #[allow(unused_imports)]
413    use super::*;
414
415    #[cfg(feature = "pt-client")]
416    #[test]
417    fn setting_encoding() {
418        fn check(settings: Vec<(&str, &str)>, expected: &str) {
419            assert_eq!(encode_settings(settings.into_iter()), expected);
420        }
421
422        // Easy cases, no escapes.
423        check(vec![], "");
424        check(vec![("hello", "world")], "hello=world");
425        check(
426            vec![("hey", "verden"), ("hello", "world")],
427            "hey=verden;hello=world",
428        );
429        check(
430            vec![("hey", "verden"), ("hello", "world"), ("selv", "tak")],
431            "hey=verden;hello=world;selv=tak",
432        );
433
434        check(
435            vec![("semi;colon", "equals=sign")],
436            r"semi\;colon=equals=sign",
437        );
438        check(
439            vec![("equals=sign", "semi;colon")],
440            r"equals\=sign=semi\;colon",
441        );
442        check(
443            vec![("semi;colon", "equals=sign"), ("also", "back\\slash")],
444            r"semi\;colon=equals=sign;also=back\\slash",
445        );
446    }
447
448    #[cfg(feature = "pt-client")]
449    #[test]
450    fn split_settings() {
451        use SocksVersion::*;
452        let long_string = "examplestrg".to_owned().repeat(50);
453        assert_eq!(long_string.len(), 550);
454        let sv = |v, a, b| settings_to_protocol(v, long_string[a..b].to_owned()).unwrap();
455        let s = |a, b| sv(V5, a, b);
456        let v = |a, b| long_string.as_bytes()[a..b].to_vec();
457
458        assert_eq!(s(0, 0), Protocol::Socks(V5, SocksAuth::NoAuth));
459        assert_eq!(
460            s(0, 50),
461            Protocol::Socks(V5, SocksAuth::Username(v(0, 50), vec![0]))
462        );
463        assert_eq!(
464            s(0, 255),
465            Protocol::Socks(V5, SocksAuth::Username(v(0, 255), vec![0]))
466        );
467        assert_eq!(
468            s(0, 256),
469            Protocol::Socks(V5, SocksAuth::Username(v(0, 255), v(255, 256)))
470        );
471        assert_eq!(
472            s(0, 300),
473            Protocol::Socks(V5, SocksAuth::Username(v(0, 255), v(255, 300)))
474        );
475        assert_eq!(
476            s(0, 510),
477            Protocol::Socks(V5, SocksAuth::Username(v(0, 255), v(255, 510)))
478        );
479
480        // This one needs to use socks4, or it won't fit. :P
481        assert_eq!(
482            sv(V4, 0, 511),
483            Protocol::Socks(V4, SocksAuth::Socks4(v(0, 511)))
484        );
485
486        // Small requests with "0" bytes work fine...
487        assert_eq!(
488            settings_to_protocol(V5, "\0".to_owned()).unwrap(),
489            Protocol::Socks(V5, SocksAuth::Username(vec![0], vec![0]))
490        );
491        assert_eq!(
492            settings_to_protocol(V5, "\0".to_owned().repeat(510)).unwrap(),
493            Protocol::Socks(V5, SocksAuth::Username(vec![0; 255], vec![0; 255]))
494        );
495
496        // Huge requests with "0" simply can't be encoded.
497        assert!(settings_to_protocol(V5, "\0".to_owned().repeat(511)).is_err());
498
499        // Huge requests without "0" can't be encoded as V5
500        assert!(settings_to_protocol(V5, long_string[0..512].to_owned()).is_err());
501
502        // Requests with "0" can't be encoded as V4.
503        assert!(settings_to_protocol(V4, "\0".to_owned()).is_err());
504    }
505}