test dnsd: implement delayed responses

Add "Delay-A: ms", "Delay-AAAA: ms" and "Delay-HTTPS: ms" to the test
dnsd config and implement delayed response handling.

Add test_21_09 and test_21_10 to check that delayed responses connect
using the undelayed address family.

Closes #21299
This commit is contained in:
Stefan Eissing 2026-04-13 16:11:37 +02:00 committed by Daniel Stenberg
parent bcd94e2750
commit 86f1e5b3f6
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
7 changed files with 277 additions and 50 deletions

View file

@ -190,3 +190,7 @@ and give a negative answer.
## `CURL_DBG_RESOLV_FAIL_IPV6`
Make libcurl fail a resolve for IPv6 only.
## `CURL_QUICK_EXIT`
Make `curl` use the quick exit option, even when built in debug mode.

View file

@ -464,6 +464,10 @@ CURLcode Curl_async_thrdd_multi_init(struct Curl_multi *multi,
void Curl_async_thrdd_multi_destroy(struct Curl_multi *multi, bool join)
{
if(multi->resolv_thrdq) {
#ifdef CURLVERBOSE
CURL_TRC_DNS(multi->admin, "destroy thread queue+pool, join=%d", join);
Curl_thrdq_trace(multi->resolv_thrdq, multi->admin);
#endif
Curl_thrdq_destroy(multi->resolv_thrdq, join);
multi->resolv_thrdq = NULL;
}

View file

@ -855,16 +855,17 @@ CURLcode config2setopts(struct OperationConfig *config,
if(result)
return result;
#ifndef DEBUGBUILD
/* On most modern OSes, exiting works thoroughly,
we clean everything up via exit(), so do not bother with slow
cleanups. Crappy ones might need to skip this.
Note: avoid having this setopt added to the --libcurl source
output. */
result = curl_easy_setopt(curl, CURLOPT_QUICK_EXIT, 1L);
if(result)
return result;
if(TRUE
#ifdef DEBUGBUILD
&& getenv("CURL_QUICK_EXIT")
#endif
) {
/* QUICK_EXIT allows for running threads to be detached and not
* joined. Preferably in non-debug runs. */
result = curl_easy_setopt(curl, CURLOPT_QUICK_EXIT, 1L);
if(result)
return result;
}
gen_trace_setopts(config, curl);

View file

@ -1917,9 +1917,15 @@ static CURLcode parallel_transfers(CURLSH *share)
if(!s->multi)
return CURLE_OUT_OF_MEMORY;
#ifndef DEBUGBUILD
(void)curl_multi_setopt(s->multi, CURLMOPT_QUICK_EXIT, 1L);
if(TRUE
#ifdef DEBUGBUILD
&& getenv("CURL_QUICK_EXIT")
#endif
) {
/* QUICK_EXIT allows for running threads to be detached and not
* joined. Preferably in non-debug runs. */
(void)curl_multi_setopt(s->multi, CURLMOPT_QUICK_EXIT, 1L);
}
(void)curl_multi_setopt(s->multi, CURLMOPT_NOTIFYFUNCTION, mnotify);
(void)curl_multi_setopt(s->multi, CURLMOPT_NOTIFYDATA, s);
(void)curl_multi_setopt(s->multi, CURLMOPT_MAX_HOST_CONNECTIONS, (long)

View file

@ -157,6 +157,38 @@ class TestResolve:
dnsd.set_answers(addr_aaaa=['[::1]'])
run_env = os.environ.copy()
run_env['CURL_DNS_SERVER'] = f'127.0.0.1:{dnsd.port}'
run_env['CURL_QUICK_EXIT'] = '1'
curl = CurlClient(env=env, run_env=run_env, force_resolv=False)
url = f'https://{env.authority_for(env.domain1, "http/1.1")}/data.json'
r = curl.http_download(urls=[url], with_stats=True)
r.check_exit_code(0)
r.check_stats(count=1, http_status=200, exitcode=0)
assert r.stats[0]['remote_ip'] == '::1'
# dnsd with one answer for A, delayed one for AAAA
@pytest.mark.skipif(condition=not Env.curl_override_dns(), reason="no DNS override")
def test_21_09_dnsd_a_delay(self, env: Env, httpd, dnsd):
dnsd.set_answers(addr_a=['127.0.0.1'], addr_aaaa=['[::1]'],
delay_aaaa_ms=env.test_timeout * 1000)
run_env = os.environ.copy()
run_env['CURL_DNS_SERVER'] = f'127.0.0.1:{dnsd.port}'
run_env['CURL_QUICK_EXIT'] = '1'
curl = CurlClient(env=env, run_env=run_env, force_resolv=False)
url = f'https://{env.authority_for(env.domain1, "http/1.1")}/data.json'
r = curl.http_download(urls=[url], with_stats=True)
r.check_exit_code(0)
r.check_stats(count=1, http_status=200, exitcode=0)
assert r.stats[0]['remote_ip'] == '127.0.0.1'
# dnsd with one answer for AAAA, delayed one for A
@pytest.mark.skipif(condition=not Env.curl_override_dns(), reason="no DNS override")
@pytest.mark.skipif(condition=not Env.curl_has_feature('IPv6'), reason="no IPv6")
def test_21_10_dnsd_aaaa_delay(self, env: Env, httpd, dnsd):
dnsd.set_answers(addr_a=['127.0.0.1'], addr_aaaa=['[::1]'],
delay_a_ms=env.test_timeout * 1000)
run_env = os.environ.copy()
run_env['CURL_DNS_SERVER'] = f'127.0.0.1:{dnsd.port}'
run_env['CURL_QUICK_EXIT'] = '1'
curl = CurlClient(env=env, run_env=run_env, force_resolv=False)
url = f'https://{env.authority_for(env.domain1, "http/1.1")}/data.json'
r = curl.http_download(urls=[url], with_stats=True)

View file

@ -146,12 +146,18 @@ class Dnsd:
os.makedirs(path)
def set_answers(self, addr_a: Optional[List[str]] = None,
addr_aaaa: Optional[List[str]] = None):
addr_aaaa: Optional[List[str]] = None,
delay_a_ms: int = 0,
delay_aaaa_ms: int = 0):
conf = []
if addr_a:
conf.extend([f'A: {addr}' for addr in addr_a])
if addr_aaaa:
conf.extend([f'AAAA: {addr}' for addr in addr_aaaa])
if delay_a_ms:
conf.append(f'Delay-A: {delay_a_ms}')
if delay_aaaa_ms:
conf.append(f'Delay-AAAA: {delay_aaaa_ms}')
conf.append('\n')
with open(self._conf_file, 'w') as fd:
fd.write("\n".join(conf))

View file

@ -26,6 +26,14 @@
static int dnsd_wrotepidfile = 0;
static int dnsd_wroteportfile = 0;
#ifdef HAVE_SYS_SELECT_H
#include <sys/select.h>
#endif
#ifdef __AMIGA__
#error building dnsd on AMIGA os is unsupported
#endif
static unsigned short get16bit(const unsigned char **pkt, size_t *size)
{
const unsigned char *p = *pkt;
@ -81,7 +89,7 @@ static const char *type2string(unsigned short qtype)
*
* Return query (qname + type + class), type and id.
*/
static int store_incoming(const unsigned char *data, size_t size,
static int store_incoming(int qid, const unsigned char *data, size_t size,
unsigned char *qbuf, size_t qbuflen, size_t *qlen,
unsigned short *qtype, unsigned short *idp)
{
@ -132,12 +140,12 @@ static int store_incoming(const unsigned char *data, size_t size,
data += 2; /* skip the next 16 bits */
size -= 2;
#if 0
fprintf(server, "QR: %x\n", (id & 0x8000) > 15);
fprintf(server, "OPCODE: %x\n", (id & 0x7800) >> 11);
fprintf(server, "TC: %x\n", (id & 0x200) >> 9);
fprintf(server, "RD: %x\n", (id & 0x100) >> 8);
fprintf(server, "Z: %x\n", (id & 0x70) >> 4);
fprintf(server, "RCODE: %x\n", (id & 0x0f));
fprintf(server, "QR: %x\n", (*idp & 0x8000) > 15);
fprintf(server, "OPCODE: %x\n", (*idp & 0x7800) >> 11);
fprintf(server, "TC: %x\n", (*idp & 0x200) >> 9);
fprintf(server, "RD: %x\n", (*idp & 0x100) >> 8);
fprintf(server, "Z: %x\n", (*idp & 0x70) >> 4);
fprintf(server, "RCODE: %x\n", (*idp & 0x0f));
#endif
(void)get16bit(&data, &size);
@ -152,7 +160,8 @@ static int store_incoming(const unsigned char *data, size_t size,
qd = get16bit(&data, &size);
fprintf(server, "QNAME %s QTYPE %s\n", name, type2string(qd));
*qtype = qd;
logmsg("Question for '%s' type %x / %s", name, qd, type2string(qd));
logmsg("[%d] Question for '%s' type %x / %s",
qid, name, qd, type2string(qd));
(void)get16bit(&data, &size);
@ -230,15 +239,94 @@ static int alpn_count;
static unsigned char ancount_a;
static unsigned char ancount_aaaa;
/* this is an answer to a question */
static int send_response(curl_socket_t sock,
const struct sockaddr *addr, curl_socklen_t addrlen,
const unsigned char *qbuf, size_t qlen,
unsigned short qtype, unsigned short id)
static timediff_t a_delay_ms;
static timediff_t aaaa_delay_ms;
static timediff_t https_delay_ms;
static int query_id = -1;
struct resp {
struct resp *next;
int qid;
struct curltime send_ts;
struct sockaddr addr;
curl_socklen_t addrlen;
char body[256];
size_t blen;
};
static struct resp *resp_queue;
static CURLcode send_resp(curl_socket_t sock, struct resp *resp)
{
ssize_t rc;
sending:
rc = sendto(sock, (const void *)resp->body, (SENDTO3)resp->blen, 0,
&resp->addr, resp->addrlen);
if((rc < 0) && (SOCKERRNO == SOCKEINTR))
goto sending;
if(rc != (ssize_t)resp->blen) {
logmsg("failed sending %d bytes, errno=%d\n", (int)resp->blen, SOCKERRNO);
return CURLE_SEND_ERROR;
}
logmsg("[%d] sent response", resp->qid);
return CURLE_OK;
}
static void queue_resp(struct resp *resp)
{
struct resp **panchor = &resp_queue;
while(*panchor) {
timediff_t ms = curlx_ptimediff_ms(&(*panchor)->send_ts, &resp->send_ts);
if(ms > 0) /* resp is to be sent before *panchor */
break;
panchor = &(*panchor)->next;
}
resp->next = *panchor;
*panchor = resp;
}
static timediff_t send_resp_queue(curl_socket_t sock)
{
struct resp **panchor = &resp_queue;
struct curltime now = curlx_now();
timediff_t timeout_ms = 0;
while(*panchor) {
struct resp *resp = *panchor;
timediff_t ms = curlx_ptimediff_ms(&resp->send_ts, &now);
if(ms > 0) {
timeout_ms = ms;
break;
}
*panchor = resp->next;
send_resp(sock, resp);
curlx_free(resp);
}
return timeout_ms;
}
static void clear_resp_queue(void)
{
while(resp_queue) {
struct resp *resp = resp_queue;
resp_queue = resp->next;
curlx_free(resp);
}
}
/* this is an answer to a question */
static struct resp *
create_resp(int qid, const struct sockaddr *addr, curl_socklen_t addrlen,
const unsigned char *qbuf, size_t qlen,
unsigned short qtype, unsigned short id)
{
struct resp *resp;
size_t i;
int a;
timediff_t delay_ms = 0;
char addrbuf[128]; /* IP address buffer */
unsigned char bytes[256] = {
0x80, 0xea, /* ID, overwrite */
@ -265,11 +353,29 @@ static int send_response(curl_socket_t sock,
0x0, 0x0 /* ARCOUNT */
};
resp = curlx_calloc(1, sizeof(*resp));
if(!resp)
return NULL;
resp->qid = qid;
/* on some platforms `curl_socklen_t` is an `int`. Casting might
* wrap this, but then it still has to fit our record size. */
if((size_t)addrlen > sizeof(resp->addr)) {
logmsg("unable to handle addrlen of %zu", (size_t)addrlen);
curlx_free(resp);
return NULL;
}
memcpy(&resp->addr, CURL_UNCONST(addr), addrlen);
resp->addrlen = addrlen;
bytes[0] = (unsigned char)(id >> 8);
bytes[1] = (unsigned char)(id & 0xff);
if(qlen > (sizeof(bytes) - 12))
return -1;
if(qlen > (sizeof(bytes) - 12)) {
logmsg("unable to handle query of length %zu", qlen);
curlx_free(resp);
return NULL;
}
/* append query, includes QTYPE and QCLASS */
memcpy(&bytes[12], qbuf, qlen);
@ -282,45 +388,55 @@ static int send_response(curl_socket_t sock,
for(a = 0; a < ancount_a; a++) {
const unsigned char *store = ipv4_pref;
add_answer(bytes, &i, store, sizeof(ipv4_pref), QTYPE_A);
logmsg("Sending back A (%x) '%s'", QTYPE_A,
logmsg("[%d] response A (%x) '%s'", qid, QTYPE_A,
curlx_inet_ntop(AF_INET, store, addrbuf, sizeof(addrbuf)));
}
if(!ancount_a)
logmsg("[%d] response A empty", qid);
delay_ms = a_delay_ms;
break;
case QTYPE_AAAA:
bytes[7] = ancount_aaaa;
for(a = 0; a < ancount_aaaa; a++) {
const unsigned char *store = ipv6_pref;
add_answer(bytes, &i, store, sizeof(ipv6_pref), QTYPE_AAAA);
logmsg("Sending back AAAA (%x) '%s'", QTYPE_AAAA,
logmsg("[%d] response AAAA (%x) '%s'", qid, QTYPE_AAAA,
curlx_inet_ntop(AF_INET6, store, addrbuf, sizeof(addrbuf)));
}
if(!ancount_aaaa)
logmsg("[%d] response AAAA empty", qid);
delay_ms = aaaa_delay_ms;
break;
case QTYPE_HTTPS:
bytes[7] = 1; /* one answer */
logmsg("[%d] response HTTPS (empty, so far)", qid);
bytes[7] = 0; /* no answer so far */
delay_ms = https_delay_ms;
break;
}
#ifdef __AMIGA__
/* Amiga breakage */
(void)rc;
(void)sock;
(void)addr;
(void)addrlen;
fprintf(stderr, "Not working\n");
return -1;
#else
rc = sendto(sock, (const void *)bytes, (SENDTO3)i, 0, addr, addrlen);
if(rc != (ssize_t)i) {
fprintf(stderr, "failed sending %d bytes\n", (int)i);
memcpy(&resp->body, bytes, i);
resp->blen = i;
resp->send_ts = curlx_now();
if(delay_ms > 0) {
int usec = (int)((delay_ms % 1000) * 1000);
resp->send_ts.tv_sec += (time_t)(delay_ms / 1000);
resp->send_ts.tv_usec += usec;
if(resp->send_ts.tv_usec >= 1000000) {
resp->send_ts.tv_sec++;
resp->send_ts.tv_usec -= 1000000;
}
}
#endif
return 0;
return resp;
}
static void read_instructions(void)
{
char file[256];
FILE *f;
/* reset defaults */
a_delay_ms = aaaa_delay_ms = https_delay_ms = 0;
snprintf(file, sizeof(file), "%s/" INSTRUCTIONS, logdir);
f = curlx_fopen(file, FOPEN_READTEXT);
if(f) {
@ -364,6 +480,33 @@ static void read_instructions(void)
break;
}
}
else if(!strncmp("Delay-A: ", buf, 9)) {
curl_off_t ms;
const char *pms = &buf[9];
rc = 0;
if(!curlx_str_number(&pms, &ms, 100000)) {
a_delay_ms = (timediff_t)ms;
rc = 1;
}
}
else if(!strncmp("Delay-AAAA: ", buf, 12)) {
curl_off_t ms;
const char *pms = &buf[12];
rc = 0;
if(!curlx_str_number(&pms, &ms, 100000)) {
aaaa_delay_ms = (timediff_t)ms;
rc = 1;
}
}
else if(!strncmp("Delay-HTTPS: ", buf, 13)) {
curl_off_t ms;
const char *pms = &buf[13];
rc = 0;
if(!curlx_str_number(&pms, &ms, 100000)) {
https_delay_ms = (timediff_t)ms;
rc = 1;
}
}
else {
/* accept empty line */
rc = buf[0] ? 0 : 1;
@ -391,6 +534,7 @@ static int test_dnsd(int argc, const char **argv)
int error;
char errbuf[STRERROR_LEN];
int result = 0;
struct resp *resp;
pidname = ".dnsd.pid";
serverlogfile = "log/dnsd.log";
@ -586,6 +730,7 @@ static int test_dnsd(int argc, const char **argv)
}
logmsg("Running %s version on port UDP/%d", ipv_inuse, (int)port);
curlx_nonblock(sock, TRUE);
for(;;) {
unsigned short id = 0;
@ -595,6 +740,7 @@ static int test_dnsd(int argc, const char **argv)
unsigned char qbuf[256]; /* query storage */
size_t qlen = 0; /* query size */
unsigned short qtype = 0;
timediff_t timeout_ms = 0;
fromlen = sizeof(from);
#ifdef USE_IPV6
if(!use_ipv6)
@ -604,6 +750,30 @@ static int test_dnsd(int argc, const char **argv)
else
fromlen = sizeof(from.sa6);
#endif
timeout_ms = send_resp_queue(sock);
{
fd_set readfds;
struct timeval tv;
int maxfd = (int)sock;
FD_ZERO(&readfds);
FD_SET(sock, &readfds);
if(!timeout_ms || (timeout_ms > 100))
timeout_ms = 100;
rc = select(maxfd + 1, &readfds, NULL, NULL,
curlx_mstotv(&tv, timeout_ms));
if(rc == -1) {
logmsg("error %d returned by select()", SOCKERRNO);
}
else if(!rc) { /* timeout */
logmsg("select timeout, run again");
continue;
}
}
n = (ssize_t)recvfrom(sock, (char *)inbuffer, sizeof(inbuffer), 0,
&from.sa, &fromlen);
if(got_exit_signal)
@ -618,12 +788,19 @@ static int test_dnsd(int argc, const char **argv)
per test case */
read_instructions();
store_incoming(inbuffer, n, qbuf, sizeof(qbuf), &qlen, &qtype, &id);
++query_id;
store_incoming(query_id, inbuffer, n,
qbuf, sizeof(qbuf), &qlen, &qtype, &id);
set_advisor_read_lock(loglockfile);
serverlogslocked = 1;
send_response(sock, &from.sa, fromlen, qbuf, qlen, qtype, id);
resp = create_resp(query_id, &from.sa, fromlen, qbuf,
qlen, qtype, id);
if(!resp)
logmsg("error creating response");
else
queue_resp(resp);
if(got_exit_signal)
break;
@ -632,10 +809,6 @@ static int test_dnsd(int argc, const char **argv)
serverlogslocked = 0;
clear_advisor_read_lock(loglockfile);
}
#if 0
logmsg("end of one transfer");
#endif
}
dnsd_cleanup:
@ -661,6 +834,7 @@ dnsd_cleanup:
clear_advisor_read_lock(loglockfile);
}
clear_resp_queue();
restore_signal_handlers(true);
if(got_exit_signal) {