From e0ebc3ff134902b7ab2e1dcaa7bc18a561caa8d8 Mon Sep 17 00:00:00 2001 From: Daniel Stenberg Date: Tue, 22 Apr 2025 14:51:49 +0200 Subject: [PATCH] lib: provide a getaddrinfo wrapper This uses c-ares under the hood and supports the CURL_DNS_SERVER environment variable - for debug builds only. The getaddrinfo() replacement function is only used if CURL_DNS_SERVER is set to make a debug build work more like a release version without the variable set. 'override-dns' is a new feature for the test suite when curl can be told to use a dedicated DNS server, and test 2102 is the first to require this. Requires c-ares 1.26.0 or later. Closes #17134 --- lib/Makefile.inc | 4 +- lib/curl_addrinfo.c | 24 ++++- lib/fake_addrinfo.c | 210 ++++++++++++++++++++++++++++++++++++++++++++ lib/fake_addrinfo.h | 54 ++++++++++++ src/curlinfo.c | 10 +++ tests/FILEFORMAT.md | 1 + tests/README.md | 40 ++++++++- tests/data/test2102 | 3 +- 8 files changed, 335 insertions(+), 11 deletions(-) create mode 100644 lib/fake_addrinfo.c create mode 100644 lib/fake_addrinfo.h diff --git a/lib/Makefile.inc b/lib/Makefile.inc index 414c955945..87e6f8c43b 100644 --- a/lib/Makefile.inc +++ b/lib/Makefile.inc @@ -123,10 +123,10 @@ LIB_CFILES = \ cf-socket.c \ cfilters.c \ conncache.c \ - cshutdn.c \ connect.c \ content_encoding.c \ cookie.c \ + cshutdn.c \ curl_addrinfo.c \ curl_des.c \ curl_endian.c \ @@ -154,6 +154,7 @@ LIB_CFILES = \ easygetopt.c \ easyoptions.c \ escape.c \ + fake_addrinfo.c \ file.c \ fileinfo.c \ fopen.c \ @@ -304,6 +305,7 @@ LIB_HFILES = \ easyif.h \ easyoptions.h \ escape.h \ + fake_addrinfo.h \ file.h \ fileinfo.h \ fopen.h \ diff --git a/lib/curl_addrinfo.c b/lib/curl_addrinfo.c index d7b98f468c..a984508b6e 100644 --- a/lib/curl_addrinfo.c +++ b/lib/curl_addrinfo.c @@ -50,6 +50,7 @@ #include #include "curl_addrinfo.h" +#include "fake_addrinfo.h" #include "inet_pton.h" #include "warnless.h" /* The last 3 #include files should be in this order */ @@ -508,6 +509,14 @@ curl_dbg_freeaddrinfo(struct addrinfo *freethis, source, line, (void *)freethis); #ifdef USE_LWIPSOCK lwip_freeaddrinfo(freethis); +#elif defined(USE_FAKE_GETADDRINFO) + { + const char *env = getenv("CURL_DNS_SERVER"); + if(env) + r_freeaddrinfo(freethis); + else + freeaddrinfo(freethis); + } #else freeaddrinfo(freethis); #endif @@ -526,13 +535,20 @@ curl_dbg_freeaddrinfo(struct addrinfo *freethis, int curl_dbg_getaddrinfo(const char *hostname, - const char *service, - const struct addrinfo *hints, - struct addrinfo **result, - int line, const char *source) + const char *service, + const struct addrinfo *hints, + struct addrinfo **result, + int line, const char *source) { #ifdef USE_LWIPSOCK int res = lwip_getaddrinfo(hostname, service, hints, result); +#elif defined(USE_FAKE_GETADDRINFO) + int res; + const char *env = getenv("CURL_DNS_SERVER"); + if(env) + res = r_getaddrinfo(hostname, service, hints, result); + else + res = getaddrinfo(hostname, service, hints, result); #else int res = getaddrinfo(hostname, service, hints, result); #endif diff --git a/lib/fake_addrinfo.c b/lib/fake_addrinfo.c new file mode 100644 index 0000000000..20d55ba2d1 --- /dev/null +++ b/lib/fake_addrinfo.c @@ -0,0 +1,210 @@ +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) Daniel Stenberg, , et al. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at https://curl.se/docs/copyright.html. + * + * You may opt to use, copy, modify, merge, publish, distribute and/or sell + * copies of the Software, and permit persons to whom the Software is + * furnished to do so, under the terms of the COPYING file. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + * SPDX-License-Identifier: curl + * + ***************************************************************************/ + +#include "curl_setup.h" +#include "fake_addrinfo.h" + +#ifdef USE_FAKE_GETADDRINFO + +#include +#include +#include + +/* The last 3 #include files should be in this order */ +#include "curl_printf.h" +#include "curl_memory.h" +#include "memdebug.h" + +void r_freeaddrinfo(struct addrinfo *cahead) +{ + struct addrinfo *canext; + struct addrinfo *ca; + + for(ca = cahead; ca; ca = canext) { + canext = ca->ai_next; + free(ca); + } +} + +struct context { + struct ares_addrinfo *result; +}; + +static void async_addrinfo_cb(void *userp, int status, int timeouts, + struct ares_addrinfo *result) +{ + struct context *ctx = (struct context *)userp; + (void)timeouts; + if(ARES_SUCCESS == status) { + ctx->result = result; + } +} + +/* convert the c-ares version into the "native" version */ +static struct addrinfo *mk_getaddrinfo(const struct ares_addrinfo *aihead) +{ + const struct ares_addrinfo_node *ai; + struct addrinfo *ca; + struct addrinfo *cafirst = NULL; + struct addrinfo *calast = NULL; + const char *name = aihead->name; + + /* traverse the addrinfo list */ + for(ai = aihead->nodes; ai != NULL; ai = ai->ai_next) { + size_t ss_size; + size_t namelen = name ? strlen(name) + 1 : 0; + /* ignore elements with unsupported address family, */ + /* settle family-specific sockaddr structure size. */ + if(ai->ai_family == AF_INET) + ss_size = sizeof(struct sockaddr_in); + else if(ai->ai_family == AF_INET6) + ss_size = sizeof(struct sockaddr_in6); + else + continue; + + /* ignore elements without required address info */ + if(!ai->ai_addr || !(ai->ai_addrlen > 0)) + continue; + + /* ignore elements with bogus address size */ + if((size_t)ai->ai_addrlen < ss_size) + continue; + + ca = malloc(sizeof(struct addrinfo) + ss_size + namelen); + if(!ca) { + r_freeaddrinfo(cafirst); + return NULL; + } + + /* copy each structure member individually, member ordering, */ + /* size, or padding might be different for each platform. */ + + ca->ai_flags = ai->ai_flags; + ca->ai_family = ai->ai_family; + ca->ai_socktype = ai->ai_socktype; + ca->ai_protocol = ai->ai_protocol; + ca->ai_addrlen = (curl_socklen_t)ss_size; + ca->ai_addr = NULL; + ca->ai_canonname = NULL; + ca->ai_next = NULL; + + ca->ai_addr = (void *)((char *)ca + sizeof(struct addrinfo)); + memcpy(ca->ai_addr, ai->ai_addr, ss_size); + + if(namelen) { + ca->ai_canonname = (void *)((char *)ca->ai_addr + ss_size); + memcpy(ca->ai_canonname, name, namelen); + + /* the name is only pointed to by the first entry in the "real" + addrinfo chain, so stop now */ + name = NULL; + } + + /* if the return list is empty, this becomes the first element */ + if(!cafirst) + cafirst = ca; + + /* add this element last in the return list */ + if(calast) + calast->ai_next = ca; + calast = ca; + } + + return cafirst; +} + +/* + RETURN VALUE + + getaddrinfo() returns 0 if it succeeds, or one of the following nonzero + error codes: + + ... +*/ +int r_getaddrinfo(const char *node, + const char *service, + const struct addrinfo *hints, + struct addrinfo **res) +{ + int status; + struct context ctx; + struct ares_options options; + int optmask = 0; + struct ares_addrinfo_hints ahints; + ares_channel channel; + int rc = 0; + + memset(&options, 0, sizeof(options)); + optmask |= ARES_OPT_EVENT_THREAD; + options.evsys = ARES_EVSYS_DEFAULT; + + memset(&ahints, 0, sizeof(ahints)); + memset(&ctx, 0, sizeof(ctx)); + + if(hints) { + ahints.ai_flags = hints->ai_flags; + ahints.ai_family = hints->ai_family; + ahints.ai_socktype = hints->ai_socktype; + ahints.ai_protocol = hints->ai_protocol; + } + + status = ares_init_options(&channel, &options, optmask); + if(status) + return EAI_MEMORY; /* major problem */ + + else { + const char *env = getenv("CURL_DNS_SERVER"); + if(env) { + rc = ares_set_servers_ports_csv(channel, env); + if(rc) { + fprintf(stderr, "ares_set_servers_ports_csv failed: %d", rc); + /* Cleanup */ + ares_destroy(channel); + return EAI_MEMORY; /* we can't run */ + } + } + } + + ares_getaddrinfo(channel, node, service, &ahints, + async_addrinfo_cb, &ctx); + + /* Wait until no more requests are left to be processed */ + ares_queue_wait_empty(channel, -1); + + if(ctx.result) { + /* convert the c-ares version */ + *res = mk_getaddrinfo(ctx.result); + /* free the old */ + ares_freeaddrinfo(ctx.result); + } + else + rc = EAI_NONAME; /* got nothing */ + + /* Cleanup */ + ares_destroy(channel); + + return rc; +} + +#endif /* USE_FAKE_GETADDRINFO */ diff --git a/lib/fake_addrinfo.h b/lib/fake_addrinfo.h new file mode 100644 index 0000000000..13b0d71dba --- /dev/null +++ b/lib/fake_addrinfo.h @@ -0,0 +1,54 @@ +#ifndef HEADER_FAKE_ADDRINFO_H +#define HEADER_FAKE_ADDRINFO_H +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) Daniel Stenberg, , et al. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at https://curl.se/docs/copyright.html. + * + * You may opt to use, copy, modify, merge, publish, distribute and/or sell + * copies of the Software, and permit persons to whom the Software is + * furnished to do so, under the terms of the COPYING file. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + * SPDX-License-Identifier: curl + * + ***************************************************************************/ + +#include "curl_setup.h" + +#ifdef USE_ARES +#include +#endif + +#if defined(CURLDEBUG) && defined(USE_ARES) && defined(HAVE_GETADDRINFO) && \ + (ARES_VERSION >= 0x011a00) /* >= 1.26. 0 */ +#define USE_FAKE_GETADDRINFO 1 +#endif + +#ifdef USE_FAKE_GETADDRINFO + +#ifdef HAVE_NETDB_H +# include +#endif +#ifdef HAVE_ARPA_INET_H +# include +#endif + +void r_freeaddrinfo(struct addrinfo *res); +int r_getaddrinfo(const char *node, + const char *service, + const struct addrinfo *hints, + struct addrinfo **res); +#endif /* USE_FAKE_GETADDRINFO */ + +#endif /* HEADER_FAKE_ADDRINFO_H */ diff --git a/src/curlinfo.c b/src/curlinfo.c index 3c8aa62df2..0274b0a2d5 100644 --- a/src/curlinfo.c +++ b/src/curlinfo.c @@ -35,6 +35,8 @@ #include "multihandle.h" /* for ENABLE_WAKEUP */ #include "tool_xattr.h" /* for USE_XATTR */ #include "curl_sha512_256.h" /* for CURL_HAVE_SHA512_256 */ +#include "asyn.h" /* for CURLRES_ARES */ +#include "fake_addrinfo.h" /* for USE_FAKE_GETADDRINFO */ #include static const char *disabled[]={ @@ -225,6 +227,14 @@ static const char *disabled[]={ "OFF" #else "ON" +#endif + , + "override-dns: " +#if defined(CURLDEBUG) && \ + (defined(CURLRES_ARES) || defined(USE_FAKE_GETADDRINFO)) + "ON" +#else + "OFF" #endif , NULL diff --git a/tests/FILEFORMAT.md b/tests/FILEFORMAT.md index a325b1a1e5..436c4ce29b 100644 --- a/tests/FILEFORMAT.md +++ b/tests/FILEFORMAT.md @@ -476,6 +476,7 @@ Features testable here are: - `NTLM` - `NTLM_WB` - `OpenSSL` +- `override-dns` - this build can use a "fake" DNS server - `parsedate` - `proxy` - `PSL` diff --git a/tests/README.md b/tests/README.md index a0634ea1ea..e6fce14f0a 100644 --- a/tests/README.md +++ b/tests/README.md @@ -118,10 +118,42 @@ SPDX-License-Identifier: curl The HTTP server supports listening on a Unix domain socket, the default location is 'http.sock'. - For HTTP/2 and HTTP/3 testing an installed `nghttpx` is used. HTTP/3 - tests check if nghttpx supports the protocol. To override the nghttpx - used, set the environment variable `NGHTTPX`. The default can also be - changed by specifying `--with-test-nghttpx=` as argument to `configure`. + For HTTP/2 and HTTP/3 testing an installed `nghttpx` is used. HTTP/3 tests + check if nghttpx supports the protocol. To override the nghttpx used, set + the environment variable `NGHTTPX`. The default can also be changed by + specifying `--with-test-nghttpx=` as argument to `configure`. + +### DNS server + + There is a test DNS server to allow tests to resolve hostnames to verify + those code paths. This server is started like all the other servers within + the `` section. + + To make a curl build actually use the test DNS server requires a debug + build. When such a test runs, the environment variable `CURL_DNS_SERVER` is + set to identify the IP address and port number of the DNS server to use. + + - curl built to use c-ares for resolving automatically asks that server for + host information + + - curl built to use `getaddrinfo()` for resolving *and* is built with c-ares + 1.26.0 or later, gets a special work-around. In such builds, when the + environment variable is set, curl instead invokes a getaddrinfo wrapper + that emulates the function and acknowledges the DNS server environment + variable. This way, the getaddrinfo-using code paths in curl are verified, + and yet the custom responses from the test DNS server are used. + + curl that is built to support a custom DNS server in a test gets the + `override-dns` feature set. + + When curl ask for HTTPS-RR, c-ares is always used and in debug builds such + asks respects the dns server environment variable as well. + + The test DNS server only has a few limited responses. When asked for + + - type `A` response, it returns the address `127.0.0.1` three times + - type `AAAA` response, it returns the address `::1` three times + - other types, it returns a blank response without answers ### Shell startup scripts diff --git a/tests/data/test2102 b/tests/data/test2102 index 7e77bbbe2c..32b29658dc 100644 --- a/tests/data/test2102 +++ b/tests/data/test2102 @@ -33,8 +33,7 @@ http dns -Debug -c-ares +override-dns HTTP GET with host name -- 2.47.3