docs: explain DNS and proxy invariants

This commit is contained in:
ldm0
2026-09-18 14:13:58 +08:00
committed by Donough Liu
parent f220ffc53a
commit 3363d9dc5f
6 changed files with 30 additions and 0 deletions
+6
View File
@@ -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(())
}
+6
View File
@@ -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<Self> {
entries
.iter()
+3
View File
@@ -264,6 +264,9 @@ impl<H: Handler, C> HttpRegistry<H, C> {
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;
+11
View File
@@ -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;
+2
View File
@@ -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)),
}
}
+2
View File
@@ -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())?;