
Closes #28792 supersedes #28854 - Adds support for Socks V4 Identification using a userid, and Authorization using a username and password on Socks V5. - Added tests for parsing various Socks proxy urls. - Added a test for making sure a misconfigured socks proxy url doesn't expose the user by connecting directly as a fallback. Release Notes: - Added support for identification and authorization when using a sock proxy
176 lines
5.7 KiB
Rust
176 lines
5.7 KiB
Rust
//! 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<Socks4Identification<'a>>),
|
|
V5(Option<Socks5Authorization<'a>>),
|
|
}
|
|
|
|
pub(crate) async fn connect_socks_proxy_stream(
|
|
proxy: &Url,
|
|
rpc_host: (&str, u16),
|
|
) -> Result<Box<dyn AsyncReadWrite>> {
|
|
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<dyn AsyncReadWrite> = 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<T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static> 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"),
|
|
};
|
|
}
|
|
}
|