From 3363d9dc5ff7c71bef8f4f4863df33b8a889ebe6 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Fri, 18 Sep 2026 05:19:06 +0800 Subject: [PATCH] docs: explain DNS and proxy invariants --- moli-curl/src/dns_adapter.rs | 6 ++++++ moli-curl/src/host_resolve.rs | 6 ++++++ moli-curl/src/http/registry.rs | 3 +++ moli-curl/src/proxy.rs | 11 +++++++++++ moli-curl/src/websocket/registry.rs | 2 ++ moli-websocket/src/transport.rs | 2 ++ 6 files changed, 30 insertions(+) diff --git a/moli-curl/src/dns_adapter.rs b/moli-curl/src/dns_adapter.rs index ca8b95bf1..7e361e8ea 100644 --- a/moli-curl/src/dns_adapter.rs +++ b/moli-curl/src/dns_adapter.rs @@ -105,6 +105,9 @@ impl CurlDnsResolution { resolution .address_policy .check_addresses(addresses, &resolution.policy_target)?; + // Preserve the complete checked answer. Filtering forbidden addresses + // would change curl's selection/fallback semantics, while resolving a + // second time would reopen the DNS-rebinding TOCTOU window. let mut resolve = List::new(); for entry in &resolution.static_entries { resolve @@ -129,6 +132,9 @@ impl CurlDnsResolution { })?; easy.resolve(resolve) .context("failed to install shared DNS result on curl request")?; + // CURLOPT_RESOLVE entries without `+` are permanent for this easy + // handle. Consuming the endpoint also prevents a requeued job from + // issuing a second lookup before it enters curl. self.endpoint = None; Ok(()) } diff --git a/moli-curl/src/host_resolve.rs b/moli-curl/src/host_resolve.rs index c952ca5fe..d20138310 100644 --- a/moli-curl/src/host_resolve.rs +++ b/moli-curl/src/host_resolve.rs @@ -15,6 +15,12 @@ struct HostResolveEntry { } impl HostResolveOverrides { + /// Parse only permanent address overrides owned by Moli. + /// + /// libcurl also accepts `+` temporary cache entries and `-` removals. Moli + /// deliberately excludes both: expiry or removal could return a direct + /// request hostname to libcurl's resolver after the routing and address + /// admission decision was made. pub fn parse(entries: &[String]) -> Result { entries .iter() diff --git a/moli-curl/src/http/registry.rs b/moli-curl/src/http/registry.rs index 356d5a5e8..7639c0a57 100644 --- a/moli-curl/src/http/registry.rs +++ b/moli-curl/src/http/registry.rs @@ -264,6 +264,9 @@ impl HttpRegistry { enqueue_existing_pending_job(&mut self.pending, pending); } Err(error) => { + // DNS failure is terminal while the easy handle still resides + // outside the Multi. Do not requeue it: that would let curl + // resolve or connect independently of the failed shared lookup. let CurlPendingJob { transfer_id, job, .. } = pending; diff --git a/moli-curl/src/proxy.rs b/moli-curl/src/proxy.rs index 97b057e96..c052228db 100644 --- a/moli-curl/src/proxy.rs +++ b/moli-curl/src/proxy.rs @@ -78,6 +78,12 @@ impl SelectedProxy { }; let url = Url::parse(&normalized) .with_context(|| format!("failed to parse proxy URL `{raw}`"))?; + // Chromium's public `socks`/`socks5` spelling always sends the target + // hostname to the proxy. libcurl assigns local-DNS semantics to + // `socks5`, so use its explicit `socks5h` spelling internally. The + // analogous SOCKS4 hostname form is `socks4a`. Keeping `url` unchanged + // preserves useful diagnostics while `curl_url` carries this semantic + // translation to the transport. let (scheme, curl_scheme) = match url.scheme() { "http" => (ProxyScheme::Http, "http"), "https" => (ProxyScheme::Https, "https"), @@ -200,6 +206,11 @@ impl ProxyRoute { Ok(None) } Self::Proxy(proxy) => { + // A target override cannot influence a remote-DNS proxy. Fail + // instead of silently giving the caller a false pinning + // guarantee. The one exception is when target and proxy are + // literally the same endpoint, where the entry pins the local + // proxy connection itself. let proxy_is_request_endpoint = proxy.endpoint_host().eq_ignore_ascii_case(request_host) && proxy.endpoint_port() == request_port; diff --git a/moli-curl/src/websocket/registry.rs b/moli-curl/src/websocket/registry.rs index 1b8bf23fb..77b959271 100644 --- a/moli-curl/src/websocket/registry.rs +++ b/moli-curl/src/websocket/registry.rs @@ -142,6 +142,8 @@ impl WebSocketRegistry { }); match result { Ok(()) => self.start(pending, multi), + // As with HTTP, a failed lookup or address-policy check ends + // the request before Session::attach can add curl to Multi. Err(error) => pending.io.finish(Err(error)), } } diff --git a/moli-websocket/src/transport.rs b/moli-websocket/src/transport.rs index 7733ec4f8..29515cd38 100644 --- a/moli-websocket/src/transport.rs +++ b/moli-websocket/src/transport.rs @@ -30,6 +30,8 @@ pub(crate) async fn open_websocket_connection( let proxy_route = websocket_proxy_route(&request.url, context)?; let host_resolve = HostResolveOverrides::parse(&context.http_host_resolve) .map_err(|error| error.to_string())?; + // Resolve exactly the endpoint this process will connect to: the target + // when direct, or the proxy when target DNS belongs to that proxy. let dns_endpoint = proxy_route .connection_dns_endpoint(&request.url, &host_resolve) .map_err(|error| error.to_string())?;