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