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}