socks.rs

  1//! socks proxy
  2use anyhow::{Context as _, Result};
  3use http_client::Url;
  4use tokio_socks::tcp::{Socks4Stream, Socks5Stream};
  5
  6/// Identification to a Socks V4 Proxy
  7struct Socks4Identification<'a> {
  8    user_id: &'a str,
  9}
 10
 11/// Authorization to a Socks V5 Proxy
 12struct Socks5Authorization<'a> {
 13    username: &'a str,
 14    password: &'a str,
 15}
 16
 17/// Socks Proxy Protocol Version
 18///
 19/// V4 allows idenfication using a user_id
 20/// V5 allows authorization using a username and password
 21enum SocksVersion<'a> {
 22    V4(Option<Socks4Identification<'a>>),
 23    V5(Option<Socks5Authorization<'a>>),
 24}
 25
 26pub(crate) async fn connect_socks_proxy_stream(
 27    proxy: &Url,
 28    rpc_host: (&str, u16),
 29) -> Result<Box<dyn AsyncReadWrite>> {
 30    let Some((socks_proxy, version)) = parse_socks_proxy(proxy) else {
 31        // If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
 32        // SOCKS proxies are often used in contexts where security and privacy are critical,
 33        // so any fallback could expose users to significant risks.
 34        anyhow::bail!("Parsing proxy url failed");
 35    };
 36
 37    // Connect to proxy and wrap protocol later
 38    let stream = tokio::net::TcpStream::connect(socks_proxy)
 39        .await
 40        .context("Failed to connect to socks proxy")?;
 41
 42    let socks: Box<dyn AsyncReadWrite> = match version {
 43        SocksVersion::V4(None) => {
 44            let socks = Socks4Stream::connect_with_socket(stream, rpc_host)
 45                .await
 46                .context("error connecting to socks")?;
 47            Box::new(socks)
 48        }
 49        SocksVersion::V4(Some(Socks4Identification { user_id })) => {
 50            let socks = Socks4Stream::connect_with_userid_and_socket(stream, rpc_host, user_id)
 51                .await
 52                .context("error connecting to socks")?;
 53            Box::new(socks)
 54        }
 55        SocksVersion::V5(None) => {
 56            let socks = Socks5Stream::connect_with_socket(stream, rpc_host)
 57                .await
 58                .context("error connecting to socks")?;
 59            Box::new(socks)
 60        }
 61        SocksVersion::V5(Some(Socks5Authorization { username, password })) => {
 62            let socks = Socks5Stream::connect_with_password_and_socket(
 63                stream, rpc_host, username, password,
 64            )
 65            .await
 66            .context("error connecting to socks")?;
 67            Box::new(socks)
 68        }
 69    };
 70
 71    Ok(socks)
 72}
 73
 74fn parse_socks_proxy(proxy: &Url) -> Option<((String, u16), SocksVersion<'_>)> {
 75    let scheme = proxy.scheme();
 76    let socks_version = if scheme.starts_with("socks4") {
 77        let identification = match proxy.username() {
 78            "" => None,
 79            username => Some(Socks4Identification { user_id: username }),
 80        };
 81        SocksVersion::V4(identification)
 82    } else if scheme.starts_with("socks") {
 83        let authorization = proxy.password().map(|password| Socks5Authorization {
 84            username: proxy.username(),
 85            password,
 86        });
 87        SocksVersion::V5(authorization)
 88    } else {
 89        return None;
 90    };
 91
 92    let host = proxy.host()?.to_string();
 93    let port = proxy.port_or_known_default()?;
 94
 95    Some(((host, port), socks_version))
 96}
 97
 98pub(crate) trait AsyncReadWrite:
 99    tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static
100{
101}
102impl<T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static> AsyncReadWrite
103    for T
104{
105}
106
107#[cfg(test)]
108mod tests {
109    use url::Url;
110
111    use super::*;
112
113    #[test]
114    fn parse_socks4() {
115        let proxy = Url::parse("socks4://proxy.example.com:1080").unwrap();
116
117        let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
118        assert_eq!(host, "proxy.example.com");
119        assert_eq!(port, 1080);
120        assert!(matches!(version, SocksVersion::V4(None)))
121    }
122
123    #[test]
124    fn parse_socks4_with_identification() {
125        let proxy = Url::parse("socks4://userid@proxy.example.com:1080").unwrap();
126
127        let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
128        assert_eq!(host, "proxy.example.com");
129        assert_eq!(port, 1080);
130        assert!(matches!(
131            version,
132            SocksVersion::V4(Some(Socks4Identification { user_id: "userid" }))
133        ))
134    }
135
136    #[test]
137    fn parse_socks5() {
138        let proxy = Url::parse("socks5://proxy.example.com:1080").unwrap();
139
140        let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
141        assert_eq!(host, "proxy.example.com");
142        assert_eq!(port, 1080);
143        assert!(matches!(version, SocksVersion::V5(None)))
144    }
145
146    #[test]
147    fn parse_socks5_with_authorization() {
148        let proxy = Url::parse("socks5://username:password@proxy.example.com:1080").unwrap();
149
150        let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
151        assert_eq!(host, "proxy.example.com");
152        assert_eq!(port, 1080);
153        assert!(matches!(
154            version,
155            SocksVersion::V5(Some(Socks5Authorization {
156                username: "username",
157                password: "password"
158            }))
159        ))
160    }
161
162    /// If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
163    /// SOCKS proxies are often used in contexts where security and privacy are critical,
164    /// so any fallback could expose users to significant risks.
165    #[tokio::test]
166    async fn fails_on_bad_proxy() {
167        // Should fail connecting because http is not a valid Socks proxy scheme
168        let proxy = Url::parse("http://localhost:2313").unwrap();
169
170        let result = connect_socks_proxy_stream(&proxy, ("test", 1080)).await;
171        match result {
172            Err(e) => assert_eq!(e.to_string(), "Parsing proxy url failed"),
173            Ok(_) => panic!("Connecting on bad proxy should fail"),
174        };
175    }
176}