client: Implement Socks identification and authorization (#29591)
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
This commit is contained in:
parent
da3a696a60
commit
6b37646179
2 changed files with 158 additions and 46 deletions
|
@ -47,6 +47,7 @@ use std::{
|
||||||
};
|
};
|
||||||
use telemetry::Telemetry;
|
use telemetry::Telemetry;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use tokio::net::TcpStream;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use util::{ResultExt, TryFutureExt};
|
use util::{ResultExt, TryFutureExt};
|
||||||
|
|
||||||
|
@ -1127,7 +1128,10 @@ impl Client {
|
||||||
let stream = {
|
let stream = {
|
||||||
let handle = cx.update(|cx| gpui_tokio::Tokio::handle(cx)).ok().unwrap();
|
let handle = cx.update(|cx| gpui_tokio::Tokio::handle(cx)).ok().unwrap();
|
||||||
let _guard = handle.enter();
|
let _guard = handle.enter();
|
||||||
connect_socks_proxy_stream(proxy.as_ref(), rpc_host).await?
|
match proxy {
|
||||||
|
Some(proxy) => connect_socks_proxy_stream(&proxy, rpc_host).await?,
|
||||||
|
None => Box::new(TcpStream::connect(rpc_host).await?),
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
log::info!("connected to rpc endpoint {}", rpc_url);
|
log::info!("connected to rpc endpoint {}", rpc_url);
|
||||||
|
|
|
@ -1,61 +1,98 @@
|
||||||
//! socks proxy
|
//! socks proxy
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use http_client::Url;
|
use http_client::Url;
|
||||||
use tokio_socks::tcp::{Socks4Stream, Socks5Stream};
|
use tokio_socks::tcp::{Socks4Stream, Socks5Stream};
|
||||||
|
|
||||||
pub(crate) async fn connect_socks_proxy_stream(
|
/// Identification to a Socks V4 Proxy
|
||||||
proxy: Option<&Url>,
|
struct Socks4Identification<'a> {
|
||||||
rpc_host: (&str, u16),
|
user_id: &'a str,
|
||||||
) -> Result<Box<dyn AsyncReadWrite>> {
|
|
||||||
let stream = match parse_socks_proxy(proxy) {
|
|
||||||
Some((socks_proxy, SocksVersion::V4)) => {
|
|
||||||
let stream = Socks4Stream::connect_with_socket(
|
|
||||||
tokio::net::TcpStream::connect(socks_proxy).await?,
|
|
||||||
rpc_host,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|err| anyhow!("error connecting to socks {}", err))?;
|
|
||||||
Box::new(stream) as Box<dyn AsyncReadWrite>
|
|
||||||
}
|
|
||||||
Some((socks_proxy, SocksVersion::V5)) => Box::new(
|
|
||||||
Socks5Stream::connect_with_socket(
|
|
||||||
tokio::net::TcpStream::connect(socks_proxy).await?,
|
|
||||||
rpc_host,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|err| anyhow!("error connecting to socks {}", err))?,
|
|
||||||
) as Box<dyn AsyncReadWrite>,
|
|
||||||
None => {
|
|
||||||
Box::new(tokio::net::TcpStream::connect(rpc_host).await?) as Box<dyn AsyncReadWrite>
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(stream)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_socks_proxy(proxy: Option<&Url>) -> Option<((String, u16), SocksVersion)> {
|
/// Authorization to a Socks V5 Proxy
|
||||||
let proxy_url = proxy?;
|
struct Socks5Authorization<'a> {
|
||||||
let scheme = proxy_url.scheme();
|
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 socks_version = if scheme.starts_with("socks4") {
|
||||||
// socks4
|
let identification = match proxy.username() {
|
||||||
SocksVersion::V4
|
"" => None,
|
||||||
|
username => Some(Socks4Identification { user_id: username }),
|
||||||
|
};
|
||||||
|
SocksVersion::V4(identification)
|
||||||
} else if scheme.starts_with("socks") {
|
} else if scheme.starts_with("socks") {
|
||||||
// socks, socks5
|
let authorization = proxy.password().map(|password| Socks5Authorization {
|
||||||
SocksVersion::V5
|
username: proxy.username(),
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
SocksVersion::V5(authorization)
|
||||||
} else {
|
} else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
if let Some((host, port)) = proxy_url.host().zip(proxy_url.port_or_known_default()) {
|
|
||||||
Some(((host.to_string(), port), socks_version))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// private helper structs and traits
|
let host = proxy.host()?.to_string();
|
||||||
|
let port = proxy.port_or_known_default()?;
|
||||||
|
|
||||||
enum SocksVersion {
|
Some(((host, port), socks_version))
|
||||||
V4,
|
|
||||||
V5,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait AsyncReadWrite:
|
pub(crate) trait AsyncReadWrite:
|
||||||
|
@ -66,3 +103,74 @@ impl<T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static> A
|
||||||
for T
|
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"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue