//! socks proxy use anyhow::{Context, Result, anyhow}; use http_client::Url; use tokio_socks::tcp::{Socks4Stream, Socks5Stream}; /// Identification to a Socks V4 Proxy struct Socks4Identification<'a> { user_id: &'a str, } /// Authorization to a Socks V5 Proxy struct Socks5Authorization<'a> { username: &'a str, password: &'a str, } /// Socks Proxy Protocol Version /// /// V4 allows idenfication using a user_id /// V5 allows authorization using a username and password enum SocksVersion<'a> { V4(Option>), V5(Option>), } pub(crate) async fn connect_socks_proxy_stream( proxy: &Url, rpc_host: (&str, u16), ) -> Result> { let Some((socks_proxy, version)) = parse_socks_proxy(proxy) else { // If parsing the proxy URL fails, we must avoid falling back to an insecure connection. // SOCKS proxies are often used in contexts where security and privacy are critical, // so any fallback could expose users to significant risks. return Err(anyhow!("Parsing proxy url failed")); }; // Connect to proxy and wrap protocol later let stream = tokio::net::TcpStream::connect(socks_proxy) .await .context("Failed to connect to socks proxy")?; let socks: Box = match version { SocksVersion::V4(None) => { let socks = Socks4Stream::connect_with_socket(stream, rpc_host) .await .context("error connecting to socks")?; Box::new(socks) } SocksVersion::V4(Some(Socks4Identification { user_id })) => { let socks = Socks4Stream::connect_with_userid_and_socket(stream, rpc_host, user_id) .await .context("error connecting to socks")?; Box::new(socks) } SocksVersion::V5(None) => { let socks = Socks5Stream::connect_with_socket(stream, rpc_host) .await .context("error connecting to socks")?; Box::new(socks) } SocksVersion::V5(Some(Socks5Authorization { username, password })) => { let socks = Socks5Stream::connect_with_password_and_socket( stream, rpc_host, username, password, ) .await .context("error connecting to socks")?; Box::new(socks) } }; Ok(socks) } fn parse_socks_proxy(proxy: &Url) -> Option<((String, u16), SocksVersion<'_>)> { let scheme = proxy.scheme(); let socks_version = if scheme.starts_with("socks4") { let identification = match proxy.username() { "" => None, username => Some(Socks4Identification { user_id: username }), }; SocksVersion::V4(identification) } else if scheme.starts_with("socks") { let authorization = proxy.password().map(|password| Socks5Authorization { username: proxy.username(), password, }); SocksVersion::V5(authorization) } else { return None; }; let host = proxy.host()?.to_string(); let port = proxy.port_or_known_default()?; Some(((host, port), socks_version)) } pub(crate) trait AsyncReadWrite: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static { } impl AsyncReadWrite for T { } #[cfg(test)] mod tests { use url::Url; use super::*; #[test] fn parse_socks4() { let proxy = Url::parse("socks4://proxy.example.com:1080").unwrap(); let ((host, port), version) = parse_socks_proxy(&proxy).unwrap(); assert_eq!(host, "proxy.example.com"); assert_eq!(port, 1080); assert!(matches!(version, SocksVersion::V4(None))) } #[test] fn parse_socks4_with_identification() { let proxy = Url::parse("socks4://userid@proxy.example.com:1080").unwrap(); let ((host, port), version) = parse_socks_proxy(&proxy).unwrap(); assert_eq!(host, "proxy.example.com"); assert_eq!(port, 1080); assert!(matches!( version, SocksVersion::V4(Some(Socks4Identification { user_id: "userid" })) )) } #[test] fn parse_socks5() { let proxy = Url::parse("socks5://proxy.example.com:1080").unwrap(); let ((host, port), version) = parse_socks_proxy(&proxy).unwrap(); assert_eq!(host, "proxy.example.com"); assert_eq!(port, 1080); assert!(matches!(version, SocksVersion::V5(None))) } #[test] fn parse_socks5_with_authorization() { let proxy = Url::parse("socks5://username:password@proxy.example.com:1080").unwrap(); let ((host, port), version) = parse_socks_proxy(&proxy).unwrap(); assert_eq!(host, "proxy.example.com"); assert_eq!(port, 1080); assert!(matches!( version, SocksVersion::V5(Some(Socks5Authorization { username: "username", password: "password" })) )) } /// If parsing the proxy URL fails, we must avoid falling back to an insecure connection. /// SOCKS proxies are often used in contexts where security and privacy are critical, /// so any fallback could expose users to significant risks. #[tokio::test] async fn fails_on_bad_proxy() { // Should fail connecting because http is not a valid Socks proxy scheme let proxy = Url::parse("http://localhost:2313").unwrap(); let result = connect_socks_proxy_stream(&proxy, ("test", 1080)).await; match result { Err(e) => assert_eq!(e.to_string(), "Parsing proxy url failed"), Ok(_) => panic!("Connecting on bad proxy should fail"), }; } }