From 40d57c9f588c42ed3f75fe0ba9b12aa18170a404 Mon Sep 17 00:00:00 2001 From: Stefan Eissing Date: Mon, 13 Apr 2026 10:32:48 +0200 Subject: [PATCH] hostip: resolve user supplied ip addresses When a user supplied an ip address in a URL as hostname, use that even when address family restrictions like -4 or -6 are set. Add test_10_15/16 to verify with a local proxy server. Fixes #21146 Reported-by: Terrance Wong How: - cf-dns: on see the hostname is an ip(v6) address, add the respective A/AAAA to the dns query bits - cf-dns/hostip: only hand out addrinfos for a family if that family is part of the DNS queries. That prevents for example ipv6 addresses to show up from dns cache entries - change cf-ip-happy to no longer check for "ip_version" and instead use all addresses that cf-dns hands out Closes #21295 --- lib/cf-dns.c | 20 +++++++++++++++++--- lib/cf-ip-happy.c | 18 ++++-------------- lib/hostip.c | 9 ++++++++- tests/http/test_10_proxy.py | 28 ++++++++++++++++++++++++++++ tests/http/testenv/curl.py | 11 +++++++---- tests/http/testenv/httpd.py | 1 + 6 files changed, 65 insertions(+), 22 deletions(-) diff --git a/lib/cf-dns.c b/lib/cf-dns.c index c9df2ff93f..436d08a1ed 100644 --- a/lib/cf-dns.c +++ b/lib/cf-dns.c @@ -167,7 +167,13 @@ static CURLcode cf_dns_start(struct Curl_cfilter *cf, #endif /* Resolve target host right on */ - CURL_TRC_CF(data, cf, "resolve host %s:%u", ctx->hostname, ctx->port); + CURL_TRC_CF(data, cf, "cf_dns_start host %s:%u", ctx->hostname, ctx->port); + if(Curl_is_ipv4addr(ctx->hostname)) + ctx->dns_queries |= CURL_DNSQ_A; +#ifdef USE_IPV6 + else if(Curl_is_ipaddr(ctx->hostname)) /* not ipv4, must be ipv6 then */ + ctx->dns_queries |= CURL_DNSQ_AAAA; +#endif result = Curl_resolv(data, ctx->dns_queries, ctx->hostname, ctx->port, ctx->transport, timeout_ms, &ctx->resolv_id, pdns); @@ -495,10 +501,18 @@ CURLcode Curl_conn_dns_result(struct connectdata *conn, int sockindex) } static const struct Curl_addrinfo * -cf_dns_get_nth_ai(const struct Curl_addrinfo *ai, +cf_dns_get_nth_ai(struct Curl_cfilter *cf, const struct Curl_addrinfo *ai, int ai_family, unsigned int index) { + struct cf_dns_ctx *ctx = cf->ctx; unsigned int i = 0; + + if((ai_family == AF_INET) && !(ctx->dns_queries & CURL_DNSQ_A)) + return NULL; +#ifdef USE_IPV6 + if((ai_family == AF_INET6) && !(ctx->dns_queries & CURL_DNSQ_AAAA)) + return NULL; +#endif for(i = 0; ai; ai = ai->ai_next) { if(ai->ai_family == ai_family) { if(i == index) @@ -550,7 +564,7 @@ Curl_cf_dns_get_ai(struct Curl_cfilter *cf, if(ctx->resolv_result) return NULL; else if(ctx->dns) - return cf_dns_get_nth_ai(ctx->dns->addr, ai_family, index); + return cf_dns_get_nth_ai(cf, ctx->dns->addr, ai_family, index); else return Curl_resolv_get_ai(data, ctx->resolv_id, ai_family, index); } diff --git a/lib/cf-ip-happy.c b/lib/cf-ip-happy.c index 2558053b47..c587f66d94 100644 --- a/lib/cf-ip-happy.c +++ b/lib/cf-ip-happy.c @@ -297,7 +297,7 @@ static void cf_ip_ballers_clear(struct Curl_cfilter *cf, bs->winner = NULL; } -static CURLcode cf_ip_ballers_init(struct cf_ip_ballers *bs, int ip_version, +static CURLcode cf_ip_ballers_init(struct cf_ip_ballers *bs, struct Curl_cfilter *cf, cf_ip_connect_create *cf_create, uint8_t transport, @@ -320,19 +320,9 @@ static CURLcode cf_ip_ballers_init(struct cf_ip_ballers *bs, int ip_version, } else { /* TCP/UDP/QUIC */ #ifdef USE_IPV6 - if(ip_version == CURL_IPRESOLVE_V6) - cf_ai_iter_init(&bs->addr_iter, NULL, AF_INET); - else - cf_ai_iter_init(&bs->addr_iter, cf, AF_INET); - - if(ip_version == CURL_IPRESOLVE_V4) - cf_ai_iter_init(&bs->ipv6_iter, NULL, AF_INET6); - else - cf_ai_iter_init(&bs->ipv6_iter, cf, AF_INET6); -#else - (void)ip_version; - cf_ai_iter_init(&bs->addr_iter, cf, AF_INET); + cf_ai_iter_init(&bs->ipv6_iter, cf, AF_INET6); #endif + cf_ai_iter_init(&bs->addr_iter, cf, AF_INET); } return CURLE_OK; } @@ -748,7 +738,7 @@ static CURLcode cf_ip_happy_init(struct Curl_cfilter *cf, CURL_TRC_CF(data, cf, "init ip ballers for transport %u", ctx->transport); ctx->started = *Curl_pgrs_now(data); - return cf_ip_ballers_init(&ctx->ballers, cf->conn->ip_version, cf, + return cf_ip_ballers_init(&ctx->ballers, cf, ctx->cf_create, ctx->transport, data->set.happy_eyeballs_timeout, IP_HE_MAX_CONCURRENT_ATTEMPTS); diff --git a/lib/hostip.c b/lib/hostip.c index 106763798f..87896cc5d6 100644 --- a/lib/hostip.c +++ b/lib/hostip.c @@ -448,8 +448,15 @@ Curl_resolv_get_ai(struct Curl_easy *data, uint32_t resolv_id, { #ifdef CURLRES_ASYNCH struct Curl_resolv_async *async = Curl_async_get(data, resolv_id); - if(async) + if(async) { + if((ai_family == AF_INET) && !(async->dns_queries & CURL_DNSQ_A)) + return NULL; +#ifdef USE_IPV6 + if((ai_family == AF_INET6) && !(async->dns_queries & CURL_DNSQ_AAAA)) + return NULL; +#endif return Curl_async_get_ai(data, async, ai_family, index); + } #else (void)data; (void)resolv_id; diff --git a/tests/http/test_10_proxy.py b/tests/http/test_10_proxy.py index 1a850312d9..b1840b484d 100644 --- a/tests/http/test_10_proxy.py +++ b/tests/http/test_10_proxy.py @@ -385,3 +385,31 @@ class TestProxy: else: r.check_response(count=1, http_status=200, protocol='HTTP/2' if proto == 'h2' else 'HTTP/1.1') + + # download via http: ipv4 proxy (no tunnel) using IP address, IPv6 only + @pytest.mark.skipif(condition=not Env.curl_has_feature('IPv6'), + reason='no ipv6 support') + def test_10_15_proxy_ip_addr(self, env: Env, httpd): + proto = 'http/1.1' + curl = CurlClient(env=env, force_resolv=False) + url = f'http://localhost:{env.http_port}/data.json' + xargs = curl.get_proxy_args(proto=proto, use_ip=True, proxys=False) + xargs.append('-6') + r = curl.http_download(urls=[url], alpn_proto='http/1.1', with_stats=True, + extra_args=xargs) + r.check_exit_code(0), f'{r}' + r.check_response(count=1, http_status=200, protocol='HTTP/1.1') + + # download via http: ipv6 proxy (no tunnel) using IP address, IPv4 only + @pytest.mark.skipif(condition=not Env.curl_has_feature('IPv6'), + reason='no ipv6 support') + def test_10_16_proxy_ip_addr(self, env: Env, httpd): + proto = 'http/1.1' + curl = CurlClient(env=env, force_resolv=False) + url = f'http://localhost:{env.http_port}/data.json' + xargs = curl.get_proxy_args(proto=proto, use_ipv6=True, proxys=False) + xargs.append('-4') + r = curl.http_download(urls=[url], alpn_proto='http/1.1', with_stats=True, + extra_args=xargs) + r.check_exit_code(0), f'{r}' + r.check_response(count=1, http_status=200, protocol='HTTP/1.1') diff --git a/tests/http/testenv/curl.py b/tests/http/testenv/curl.py index afa3c404e2..99aa649bc0 100644 --- a/tests/http/testenv/curl.py +++ b/tests/http/testenv/curl.py @@ -684,22 +684,25 @@ class CurlClient: def get_proxy_args(self, proto: str = 'http/1.1', proxys: bool = True, tunnel: bool = False, - use_ip: bool = False): - proxy_name = self._server_addr if use_ip else self.env.proxy_domain + use_ip: bool = False, use_ipv6: bool = False): + proxy_name = '[::1]' if use_ipv6 else \ + self._server_addr if use_ip else self.env.proxy_domain if proxys: pport = self.env.pts_port(proto) if tunnel else self.env.proxys_port xargs = [ '--proxy', f'https://{proxy_name}:{pport}/', - '--resolve', f'{proxy_name}:{pport}:{self._server_addr}', '--proxy-cacert', self.env.ca.cert_file, ] + if self._force_resolv and not use_ip and not use_ipv6: + xargs.extend(['--resolve', f'{proxy_name}:{pport}:{self._server_addr}']) if proto == 'h2': xargs.append('--proxy-http2') else: xargs = [ '--proxy', f'http://{proxy_name}:{self.env.proxy_port}/', - '--resolve', f'{proxy_name}:{self.env.proxy_port}:{self._server_addr}', ] + if self._force_resolv and not use_ip and not use_ipv6: + xargs.extend(['--resolve', f'{proxy_name}:{self.env.proxy_port}:{self._server_addr}']) if tunnel: xargs.append('--proxytunnel') return xargs diff --git a/tests/http/testenv/httpd.py b/tests/http/testenv/httpd.py index 655bfc3b7f..06fbf25f06 100644 --- a/tests/http/testenv/httpd.py +++ b/tests/http/testenv/httpd.py @@ -506,6 +506,7 @@ class Httpd: return [ ' ', ' Require ip 127.0.0.1', + ' Require ip ::1', ' ', ]