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.
This commit is contained in:
link2xt
2026-05-23 04:21:46 +02:00
parent bafb5f71ad
commit dbe0188285

View File

@@ -50,7 +50,7 @@ impl ShadowsocksConfig {
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct HttpConfig { pub struct HttpConfig {
/// HTTP proxy host. /// HTTP proxy host.
pub host: String, pub host: url::Host,
/// HTTP proxy port. /// HTTP proxy port.
pub port: u16, pub port: u16,
@@ -63,10 +63,7 @@ pub struct HttpConfig {
impl HttpConfig { impl HttpConfig {
fn from_url(url: Url) -> Result<Self> { fn from_url(url: Url) -> Result<Self> {
let host = url let host = url.host().context("HTTP proxy URL has no host")?.to_owned();
.host_str()
.context("HTTP proxy URL has no host")?
.to_string();
let port = url let port = url
.port_or_known_default() .port_or_known_default()
.context("HTTP(S) URLs are guaranteed to return Some port")?; .context("HTTP(S) URLs are guaranteed to return Some port")?;
@@ -104,7 +101,8 @@ impl HttpConfig {
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Socks5Config { pub struct Socks5Config {
pub host: String, /// Hostname or IP address.
pub host: url::Host,
pub port: u16, pub port: u16,
pub user_password: Option<(String, String)>, pub user_password: Option<(String, String)>,
} }
@@ -117,7 +115,13 @@ impl Socks5Config {
target_port: u16, target_port: u16,
load_dns_cache: bool, load_dns_cache: bool,
) -> Result<Socks5Stream<Pin<Box<TimeoutStream<TcpStream>>>>> { ) -> Result<Socks5Stream<Pin<Box<TimeoutStream<TcpStream>>>>> {
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 .await
.context("Failed to connect to SOCKS5 proxy")?; .context("Failed to connect to SOCKS5 proxy")?;
@@ -273,10 +277,7 @@ impl ProxyConfig {
// Because of this we do not distinguish // Because of this we do not distinguish
// between `socks5` and `socks5h`. // between `socks5` and `socks5h`.
"socks5" => { "socks5" => {
let host = url let host = url.host().context("socks5 URL has no host")?.to_owned();
.host_str()
.context("socks5 URL has no host")?
.to_string();
let port = url.port().unwrap_or(DEFAULT_SOCKS_PORT); let port = url.port().unwrap_or(DEFAULT_SOCKS_PORT);
let user_password = if let Some(password) = url.password() { let user_password = if let Some(password) = url.password() {
let username = percent_encoding::percent_decode_str(url.username()) let username = percent_encoding::percent_decode_str(url.username())
@@ -402,12 +403,13 @@ impl ProxyConfig {
match self { match self {
ProxyConfig::Http(http_config) => { ProxyConfig::Http(http_config) => {
let load_cache = false; let load_cache = false;
let tcp_stream = crate::net::connect_tcp( let hostname = match &http_config.host {
context, url::Host::Domain(domain) => domain.to_string(),
&http_config.host, url::Host::Ipv4(addr) => addr.to_string(),
http_config.port, url::Host::Ipv6(addr) => addr.to_string(),
load_cache, };
) let tcp_stream =
crate::net::connect_tcp(context, &hostname, http_config.port, load_cache)
.await?; .await?;
let auth = if let Some((username, password)) = &http_config.user_password { let auth = if let Some((username, password)) = &http_config.user_password {
Some((username.as_str(), password.as_str())) Some((username.as_str(), password.as_str()))
@@ -419,16 +421,18 @@ impl ProxyConfig {
} }
ProxyConfig::Https(https_config) => { ProxyConfig::Https(https_config) => {
let load_cache = true; let load_cache = true;
let tcp_stream = crate::net::connect_tcp( let hostname = match &https_config.host {
context, url::Host::Domain(domain) => domain.to_string(),
&https_config.host, url::Host::Ipv4(addr) => addr.to_string(),
https_config.port, url::Host::Ipv6(addr) => addr.to_string(),
load_cache, };
)
let tcp_stream =
crate::net::connect_tcp(context, &hostname, https_config.port, load_cache)
.await?; .await?;
let use_sni = true; let use_sni = true;
let tls_stream = wrap_rustls( let tls_stream = wrap_rustls(
&https_config.host, &hostname,
https_config.port, https_config.port,
use_sni, use_sni,
"", "",
@@ -500,6 +504,7 @@ mod tests {
use super::*; use super::*;
use crate::config::Config; use crate::config::Config;
use crate::test_utils::TestContext; use crate::test_utils::TestContext;
use std::net::{Ipv4Addr, Ipv6Addr};
#[test] #[test]
fn test_socks5_url() { fn test_socks5_url() {
@@ -507,17 +512,35 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Socks5(Socks5Config { 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: <https://github.com/servo/rust-url/issues/767>.
// 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, port: 9050,
user_password: None 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(); let proxy_config = ProxyConfig::from_url("socks5://foo:bar@127.0.0.1:9150").unwrap();
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Socks5(Socks5Config { ProxyConfig::Socks5(Socks5Config {
host: "127.0.0.1".to_string(), host: url::Host::Domain("127.0.0.1".to_string()),
port: 9150, port: 9150,
user_password: Some(("foo".to_string(), "bar".to_string())) user_password: Some(("foo".to_string(), "bar".to_string()))
}) })
@@ -527,7 +550,7 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Socks5(Socks5Config { ProxyConfig::Socks5(Socks5Config {
host: "127.0.0.1".to_string(), host: url::Host::Domain("127.0.0.1".to_string()),
port: 9150, port: 9150,
user_password: Some(("foo".to_string(), "bar".to_string())) user_password: Some(("foo".to_string(), "bar".to_string()))
}) })
@@ -537,7 +560,7 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Socks5(Socks5Config { ProxyConfig::Socks5(Socks5Config {
host: "127.0.0.1".to_string(), host: url::Host::Domain("127.0.0.1".to_string()),
port: 80, port: 80,
user_password: None user_password: None
}) })
@@ -547,7 +570,7 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Socks5(Socks5Config { ProxyConfig::Socks5(Socks5Config {
host: "127.0.0.1".to_string(), host: url::Host::Domain("127.0.0.1".to_string()),
port: 1080, port: 1080,
user_password: None user_password: None
}) })
@@ -557,7 +580,7 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Socks5(Socks5Config { ProxyConfig::Socks5(Socks5Config {
host: "127.0.0.1".to_string(), host: url::Host::Domain("127.0.0.1".to_string()),
port: 1080, port: 1080,
user_password: None user_password: None
}) })
@@ -567,7 +590,7 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Socks5(Socks5Config { ProxyConfig::Socks5(Socks5Config {
host: "my-proxy.example.org".to_string(), host: url::Host::Domain("my-proxy.example.org".to_string()),
port: 1080, port: 1080,
user_password: None user_password: None
}) })
@@ -584,7 +607,7 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Http(HttpConfig { ProxyConfig::Http(HttpConfig {
host: "127.0.0.1".to_string(), host: url::Host::Ipv4(Ipv4Addr::LOCALHOST),
port: 80, port: 80,
user_password: None user_password: None
}) })
@@ -594,7 +617,7 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Http(HttpConfig { ProxyConfig::Http(HttpConfig {
host: "127.0.0.1".to_string(), host: url::Host::Ipv4(Ipv4Addr::LOCALHOST),
port: 80, port: 80,
user_password: None user_password: None
}) })
@@ -604,7 +627,7 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Http(HttpConfig { ProxyConfig::Http(HttpConfig {
host: "127.0.0.1".to_string(), host: url::Host::Ipv4(Ipv4Addr::LOCALHOST),
port: 443, port: 443,
user_password: None user_password: None
}) })
@@ -614,7 +637,7 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Http(HttpConfig { ProxyConfig::Http(HttpConfig {
host: "my-proxy.example.org".to_string(), host: url::Host::Domain("my-proxy.example.org".to_string()),
port: 80, port: 80,
user_password: None user_password: None
}) })
@@ -631,7 +654,7 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Https(HttpConfig { ProxyConfig::Https(HttpConfig {
host: "127.0.0.1".to_string(), host: url::Host::Ipv4(Ipv4Addr::LOCALHOST),
port: 443, port: 443,
user_password: None user_password: None
}) })
@@ -641,7 +664,7 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Https(HttpConfig { ProxyConfig::Https(HttpConfig {
host: "127.0.0.1".to_string(), host: url::Host::Ipv4(Ipv4Addr::LOCALHOST),
port: 80, port: 80,
user_password: None user_password: None
}) })
@@ -651,7 +674,7 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Https(HttpConfig { ProxyConfig::Https(HttpConfig {
host: "127.0.0.1".to_string(), host: url::Host::Ipv4(Ipv4Addr::LOCALHOST),
port: 443, port: 443,
user_password: None user_password: None
}) })
@@ -661,7 +684,7 @@ mod tests {
assert_eq!( assert_eq!(
proxy_config, proxy_config,
ProxyConfig::Https(HttpConfig { ProxyConfig::Https(HttpConfig {
host: "my-proxy.example.org".to_string(), host: url::Host::Domain("my-proxy.example.org".to_string()),
port: 443, port: 443,
user_password: None user_password: None
}) })