client: Fix an issue where non-IP proxy URLs didn’t resolve correctly (#32664)

张小白 created

If the proxy URL is in the form of `example.com` instead of a raw IP
address, and `example.com` isn't a well-known domain, then the default
URL resolution can fail.

The test setup:

A Linux machine runs a CoreDNS server with a custom entry: `10.254.7.38
example.com`. On a Windows machine, if the proxy URL is set to
`example.com`, the resolved address does **not** end up being
`10.254.7.38`.

Using `hickory_resolver` for more advanced DNS resolution fixes this
issue.


Release Notes:

- Fixed proxy URL resolution when using custom DNS entries.

Change summary

Cargo.lock                             | 98 ++++++++++++++++++++++++++++
Cargo.toml                             |  1 
crates/client/Cargo.toml               |  3 
crates/client/src/proxy.rs             | 50 ++++++++++++-
crates/client/src/proxy/socks_proxy.rs | 10 ++
5 files changed, 154 insertions(+), 8 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2826,6 +2826,7 @@ dependencies = [
  "futures 0.3.31",
  "gpui",
  "gpui_tokio",
+ "hickory-resolver",
  "http_client",
  "http_client_tls",
  "httparse",
@@ -4905,6 +4906,18 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
 
+[[package]]
+name = "enum-as-inner"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.101",
+]
+
 [[package]]
 name = "enumflags2"
 version = "0.7.11"
@@ -7483,6 +7496,51 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
 
+[[package]]
+name = "hickory-proto"
+version = "0.24.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248"
+dependencies = [
+ "async-trait",
+ "cfg-if",
+ "data-encoding",
+ "enum-as-inner",
+ "futures-channel",
+ "futures-io",
+ "futures-util",
+ "idna",
+ "ipnet",
+ "once_cell",
+ "rand 0.8.5",
+ "thiserror 1.0.69",
+ "tinyvec",
+ "tokio",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "hickory-resolver"
+version = "0.24.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "hickory-proto",
+ "ipconfig",
+ "lru-cache",
+ "once_cell",
+ "parking_lot",
+ "rand 0.8.5",
+ "resolv-conf",
+ "smallvec",
+ "thiserror 1.0.69",
+ "tokio",
+ "tracing",
+]
+
 [[package]]
 name = "hidden-trait"
 version = "0.1.2"
@@ -8352,6 +8410,18 @@ dependencies = [
  "windows 0.58.0",
 ]
 
+[[package]]
+name = "ipconfig"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
+dependencies = [
+ "socket2",
+ "widestring",
+ "windows-sys 0.48.0",
+ "winreg 0.50.0",
+]
+
 [[package]]
 name = "ipnet"
 version = "2.11.0"
@@ -9236,6 +9306,12 @@ dependencies = [
  "cc",
 ]
 
+[[package]]
+name = "linked-hash-map"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
+
 [[package]]
 name = "linkify"
 version = "0.10.0"
@@ -9496,6 +9572,15 @@ dependencies = [
  "hashbrown 0.15.3",
 ]
 
+[[package]]
+name = "lru-cache"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
+dependencies = [
+ "linked-hash-map",
+]
+
 [[package]]
 name = "lsp"
 version = "0.1.0"
@@ -13327,6 +13412,7 @@ dependencies = [
  "futures-core",
  "futures-util",
  "h2 0.4.9",
+ "hickory-resolver",
  "http 1.3.1",
  "http-body 1.0.1",
  "http-body-util",
@@ -13383,6 +13469,12 @@ dependencies = [
  "workspace-hack",
 ]
 
+[[package]]
+name = "resolv-conf"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3"
+
 [[package]]
 name = "resvg"
 version = "0.45.1"
@@ -18304,6 +18396,12 @@ dependencies = [
  "wasite",
 ]
 
+[[package]]
+name = "widestring"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
+
 [[package]]
 name = "wiggle"
 version = "29.0.1"

Cargo.toml 🔗

@@ -524,6 +524,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77
     "rustls-tls-native-roots",
     "socks",
     "stream",
+    "hickory-dns",
 ] }
 rsa = "0.9.6"
 runtimelib = {  git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [

crates/client/Cargo.toml 🔗

@@ -28,6 +28,9 @@ feature_flags.workspace = true
 futures.workspace = true
 gpui.workspace = true
 gpui_tokio.workspace = true
+# Don't update `hickory-resolver`, it has a bug that causes it to not resolve DNS queries correctly.
+# See https://github.com/hickory-dns/hickory-dns/issues/3048
+hickory-resolver = { version = "0.24", features = ["tokio-runtime"] }
 http_client.workspace = true
 http_client_tls.workspace = true
 httparse = "1.10"

crates/client/src/proxy.rs 🔗

@@ -3,20 +3,30 @@
 mod http_proxy;
 mod socks_proxy;
 
+use std::sync::LazyLock;
+
 use anyhow::{Context as _, Result};
+use hickory_resolver::{
+    AsyncResolver, TokioAsyncResolver,
+    config::LookupIpStrategy,
+    name_server::{GenericConnector, TokioRuntimeProvider},
+    system_conf,
+};
 use http_client::Url;
 use http_proxy::{HttpProxyType, connect_http_proxy_stream, parse_http_proxy};
 use socks_proxy::{SocksVersion, connect_socks_proxy_stream, parse_socks_proxy};
+use tokio_socks::{IntoTargetAddr, TargetAddr};
+use util::ResultExt;
 
 pub(crate) async fn connect_proxy_stream(
     proxy: &Url,
     rpc_host: (&str, u16),
 ) -> Result<Box<dyn AsyncReadWrite>> {
-    let Some(((proxy_domain, proxy_port), proxy_type)) = parse_proxy_type(proxy) else {
+    let Some(((proxy_domain, proxy_port), proxy_type)) = parse_proxy_type(proxy).await 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.
-        anyhow::bail!("Parsing proxy url failed");
+        anyhow::bail!("Parsing proxy url type failed");
     };
 
     // Connect to proxy and wrap protocol later
@@ -39,10 +49,8 @@ enum ProxyType<'t> {
     HttpProxy(HttpProxyType<'t>),
 }
 
-fn parse_proxy_type(proxy: &Url) -> Option<((String, u16), ProxyType<'_>)> {
+async fn parse_proxy_type(proxy: &Url) -> Option<((String, u16), ProxyType<'_>)> {
     let scheme = proxy.scheme();
-    let host = proxy.host()?.to_string();
-    let port = proxy.port_or_known_default()?;
     let proxy_type = match scheme {
         scheme if scheme.starts_with("socks") => {
             Some(ProxyType::SocksProxy(parse_socks_proxy(scheme, proxy)))
@@ -52,8 +60,38 @@ fn parse_proxy_type(proxy: &Url) -> Option<((String, u16), ProxyType<'_>)> {
         }
         _ => None,
     }?;
+    let (ip, port) = {
+        let host = proxy.host()?.to_string();
+        let port = proxy.port_or_known_default()?;
+        resolve_proxy_url_if_needed((host, port)).await.log_err()?
+    };
+
+    Some(((ip, port), proxy_type))
+}
+
+static SYSTEM_DNS_RESOLVER: LazyLock<AsyncResolver<GenericConnector<TokioRuntimeProvider>>> =
+    LazyLock::new(|| {
+        let (config, mut opts) = system_conf::read_system_conf().unwrap();
+        opts.ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
+        TokioAsyncResolver::tokio(config, opts)
+    });
 
-    Some(((host, port), proxy_type))
+async fn resolve_proxy_url_if_needed(proxy: (String, u16)) -> Result<(String, u16)> {
+    let proxy = proxy
+        .into_target_addr()
+        .context("Failed to parse proxy addr")?;
+    match proxy {
+        TargetAddr::Domain(domain, port) => {
+            let ip = SYSTEM_DNS_RESOLVER
+                .lookup_ip(domain.as_ref())
+                .await?
+                .into_iter()
+                .next()
+                .ok_or_else(|| anyhow::anyhow!("No IP found for proxy domain {domain}"))?;
+            Ok((ip.to_string(), port))
+        }
+        TargetAddr::Ip(ip_addr) => Ok((ip_addr.ip().to_string(), ip_addr.port())),
+    }
 }
 
 pub(crate) trait AsyncReadWrite:

crates/client/src/proxy/socks_proxy.rs 🔗

@@ -1,5 +1,7 @@
 //! socks proxy
 
+use std::net::SocketAddr;
+
 use anyhow::{Context as _, Result};
 use http_client::Url;
 use tokio::net::TcpStream;
@@ -8,6 +10,8 @@ use tokio_socks::{
     tcp::{Socks4Stream, Socks5Stream},
 };
 
+use crate::proxy::SYSTEM_DNS_RESOLVER;
+
 use super::AsyncReadWrite;
 
 /// Identification to a Socks V4 Proxy
@@ -73,12 +77,14 @@ pub(super) async fn connect_socks_proxy_stream(
     };
     let rpc_host = match (rpc_host, local_dns) {
         (TargetAddr::Domain(domain, port), true) => {
-            let ip_addr = tokio::net::lookup_host((domain.as_ref(), port))
+            let ip_addr = SYSTEM_DNS_RESOLVER
+                .lookup_ip(domain.as_ref())
                 .await
                 .with_context(|| format!("Failed to lookup domain {}", domain))?
+                .into_iter()
                 .next()
                 .ok_or_else(|| anyhow::anyhow!("Failed to lookup domain {}", domain))?;
-            TargetAddr::Ip(ip_addr)
+            TargetAddr::Ip(SocketAddr::new(ip_addr, port))
         }
         (rpc_host, _) => rpc_host,
     };