From dbe0188285fcbddb385aa119bc49ca622e557ade Mon Sep 17 00:00:00 2001 From: link2xt Date: Sat, 23 May 2026 04:21:46 +0200 Subject: [PATCH] fix: do not try to resolve proxy IPv6 addresses in square brackets "[::1]" is not a hostname, but an IPv6 address, and should be passed as "::1" to tokio::net functions. Otherwise connecting to proxy addresses such as `socks5://[::1]:9150` fails by trying to resolve IPv6 address with DNS. --- src/net/proxy.rs | 105 +++++++++++++++++++++++++++++------------------ 1 file changed, 64 insertions(+), 41 deletions(-) diff --git a/src/net/proxy.rs b/src/net/proxy.rs index 021f15109..50ce26353 100644 --- a/src/net/proxy.rs +++ b/src/net/proxy.rs @@ -50,7 +50,7 @@ impl ShadowsocksConfig { #[derive(Debug, Clone, PartialEq, Eq)] pub struct HttpConfig { /// HTTP proxy host. - pub host: String, + pub host: url::Host, /// HTTP proxy port. pub port: u16, @@ -63,10 +63,7 @@ pub struct HttpConfig { impl HttpConfig { fn from_url(url: Url) -> Result { - let host = url - .host_str() - .context("HTTP proxy URL has no host")? - .to_string(); + let host = url.host().context("HTTP proxy URL has no host")?.to_owned(); let port = url .port_or_known_default() .context("HTTP(S) URLs are guaranteed to return Some port")?; @@ -104,7 +101,8 @@ impl HttpConfig { #[derive(Debug, Clone, PartialEq, Eq)] pub struct Socks5Config { - pub host: String, + /// Hostname or IP address. + pub host: url::Host, pub port: u16, pub user_password: Option<(String, String)>, } @@ -117,7 +115,13 @@ impl Socks5Config { target_port: u16, load_dns_cache: bool, ) -> Result>>>> { - let tcp_stream = connect_tcp(context, &self.host, self.port, load_dns_cache) + let hostname = match &self.host { + url::Host::Domain(domain) => domain.to_string(), + url::Host::Ipv4(addr) => addr.to_string(), + url::Host::Ipv6(addr) => addr.to_string(), + }; + + let tcp_stream = connect_tcp(context, &hostname, self.port, load_dns_cache) .await .context("Failed to connect to SOCKS5 proxy")?; @@ -273,10 +277,7 @@ impl ProxyConfig { // Because of this we do not distinguish // between `socks5` and `socks5h`. "socks5" => { - let host = url - .host_str() - .context("socks5 URL has no host")? - .to_string(); + let host = url.host().context("socks5 URL has no host")?.to_owned(); let port = url.port().unwrap_or(DEFAULT_SOCKS_PORT); let user_password = if let Some(password) = url.password() { let username = percent_encoding::percent_decode_str(url.username()) @@ -402,13 +403,14 @@ impl ProxyConfig { match self { ProxyConfig::Http(http_config) => { let load_cache = false; - let tcp_stream = crate::net::connect_tcp( - context, - &http_config.host, - http_config.port, - load_cache, - ) - .await?; + let hostname = match &http_config.host { + url::Host::Domain(domain) => domain.to_string(), + url::Host::Ipv4(addr) => addr.to_string(), + url::Host::Ipv6(addr) => addr.to_string(), + }; + let tcp_stream = + crate::net::connect_tcp(context, &hostname, http_config.port, load_cache) + .await?; let auth = if let Some((username, password)) = &http_config.user_password { Some((username.as_str(), password.as_str())) } else { @@ -419,16 +421,18 @@ impl ProxyConfig { } ProxyConfig::Https(https_config) => { let load_cache = true; - let tcp_stream = crate::net::connect_tcp( - context, - &https_config.host, - https_config.port, - load_cache, - ) - .await?; + let hostname = match &https_config.host { + url::Host::Domain(domain) => domain.to_string(), + url::Host::Ipv4(addr) => addr.to_string(), + url::Host::Ipv6(addr) => addr.to_string(), + }; + + let tcp_stream = + crate::net::connect_tcp(context, &hostname, https_config.port, load_cache) + .await?; let use_sni = true; let tls_stream = wrap_rustls( - &https_config.host, + &hostname, https_config.port, use_sni, "", @@ -500,6 +504,7 @@ mod tests { use super::*; use crate::config::Config; use crate::test_utils::TestContext; + use std::net::{Ipv4Addr, Ipv6Addr}; #[test] fn test_socks5_url() { @@ -507,17 +512,35 @@ mod tests { assert_eq!( proxy_config, ProxyConfig::Socks5(Socks5Config { - host: "127.0.0.1".to_string(), + // IPv4 address is parsed as a domain and not url::Host::Ipv4. + // This is expected: . + // We only need a distinction for IPv6 to remove square brackets + // before passing the address to `lookup_host()`. + host: url::Host::Domain("127.0.0.1".to_string()), port: 9050, user_password: None }) ); + assert_eq!(proxy_config.to_url(), "socks5://127.0.0.1:9050".to_string()); + + let proxy_config = ProxyConfig::from_url("socks5://[::1]:9050").unwrap(); + assert_eq!( + proxy_config, + ProxyConfig::Socks5(Socks5Config { + // IPv6 address should be recognized as IPv6 address and not "[::1]" hostname. + // Otherwise we may try to resolve "[::1]" and fail to connect. + host: url::Host::Ipv6(Ipv6Addr::LOCALHOST), + port: 9050, + user_password: None + }) + ); + assert_eq!(proxy_config.to_url(), "socks5://[::1]:9050".to_string()); let proxy_config = ProxyConfig::from_url("socks5://foo:bar@127.0.0.1:9150").unwrap(); assert_eq!( proxy_config, ProxyConfig::Socks5(Socks5Config { - host: "127.0.0.1".to_string(), + host: url::Host::Domain("127.0.0.1".to_string()), port: 9150, user_password: Some(("foo".to_string(), "bar".to_string())) }) @@ -527,7 +550,7 @@ mod tests { assert_eq!( proxy_config, ProxyConfig::Socks5(Socks5Config { - host: "127.0.0.1".to_string(), + host: url::Host::Domain("127.0.0.1".to_string()), port: 9150, user_password: Some(("foo".to_string(), "bar".to_string())) }) @@ -537,7 +560,7 @@ mod tests { assert_eq!( proxy_config, ProxyConfig::Socks5(Socks5Config { - host: "127.0.0.1".to_string(), + host: url::Host::Domain("127.0.0.1".to_string()), port: 80, user_password: None }) @@ -547,7 +570,7 @@ mod tests { assert_eq!( proxy_config, ProxyConfig::Socks5(Socks5Config { - host: "127.0.0.1".to_string(), + host: url::Host::Domain("127.0.0.1".to_string()), port: 1080, user_password: None }) @@ -557,7 +580,7 @@ mod tests { assert_eq!( proxy_config, ProxyConfig::Socks5(Socks5Config { - host: "127.0.0.1".to_string(), + host: url::Host::Domain("127.0.0.1".to_string()), port: 1080, user_password: None }) @@ -567,7 +590,7 @@ mod tests { assert_eq!( proxy_config, ProxyConfig::Socks5(Socks5Config { - host: "my-proxy.example.org".to_string(), + host: url::Host::Domain("my-proxy.example.org".to_string()), port: 1080, user_password: None }) @@ -584,7 +607,7 @@ mod tests { assert_eq!( proxy_config, ProxyConfig::Http(HttpConfig { - host: "127.0.0.1".to_string(), + host: url::Host::Ipv4(Ipv4Addr::LOCALHOST), port: 80, user_password: None }) @@ -594,7 +617,7 @@ mod tests { assert_eq!( proxy_config, ProxyConfig::Http(HttpConfig { - host: "127.0.0.1".to_string(), + host: url::Host::Ipv4(Ipv4Addr::LOCALHOST), port: 80, user_password: None }) @@ -604,7 +627,7 @@ mod tests { assert_eq!( proxy_config, ProxyConfig::Http(HttpConfig { - host: "127.0.0.1".to_string(), + host: url::Host::Ipv4(Ipv4Addr::LOCALHOST), port: 443, user_password: None }) @@ -614,7 +637,7 @@ mod tests { assert_eq!( proxy_config, ProxyConfig::Http(HttpConfig { - host: "my-proxy.example.org".to_string(), + host: url::Host::Domain("my-proxy.example.org".to_string()), port: 80, user_password: None }) @@ -631,7 +654,7 @@ mod tests { assert_eq!( proxy_config, ProxyConfig::Https(HttpConfig { - host: "127.0.0.1".to_string(), + host: url::Host::Ipv4(Ipv4Addr::LOCALHOST), port: 443, user_password: None }) @@ -641,7 +664,7 @@ mod tests { assert_eq!( proxy_config, ProxyConfig::Https(HttpConfig { - host: "127.0.0.1".to_string(), + host: url::Host::Ipv4(Ipv4Addr::LOCALHOST), port: 80, user_password: None }) @@ -651,7 +674,7 @@ mod tests { assert_eq!( proxy_config, ProxyConfig::Https(HttpConfig { - host: "127.0.0.1".to_string(), + host: url::Host::Ipv4(Ipv4Addr::LOCALHOST), port: 443, user_password: None }) @@ -661,7 +684,7 @@ mod tests { assert_eq!( proxy_config, ProxyConfig::Https(HttpConfig { - host: "my-proxy.example.org".to_string(), + host: url::Host::Domain("my-proxy.example.org".to_string()), port: 443, user_password: None })