From c3ae9ef8226ac60c5e5915e44a927d4b05daebd9 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 12 Jul 2026 03:58:18 +0100 Subject: [PATCH] hostip: only cache negative resolves for authoritative answers Closes #22302 --- docs/libcurl/libcurl-env-dbg.md | 8 ++- .../libcurl/opts/CURLOPT_DNS_CACHE_TIMEOUT.md | 4 +- lib/asyn-ares.c | 22 ++++-- lib/asyn-thrdd.c | 48 +++++++++++++ lib/asyn.h | 6 ++ lib/doh.c | 23 ++++++- lib/doh.h | 5 +- lib/fake_addrinfo.c | 6 +- lib/hostip.c | 39 +++++++++-- tests/http/test_21_resolve.py | 69 +++++++++++++++++++ tests/http/testenv/dnsd.py | 8 ++- tests/server/dnsd.c | 35 +++++++++- tests/unit/unit1650.c | 2 + 13 files changed, 253 insertions(+), 22 deletions(-) diff --git a/docs/libcurl/libcurl-env-dbg.md b/docs/libcurl/libcurl-env-dbg.md index 70d0f12c88..5004b8873d 100644 --- a/docs/libcurl/libcurl-env-dbg.md +++ b/docs/libcurl/libcurl-env-dbg.md @@ -185,7 +185,13 @@ Makes ever threaded resolve experience an initial delay in milliseconds. ## `CURL_DBG_RESOLV_FAIL_DELAY` With a threaded resolver, delay each lookup by the given milliseconds -and give a negative answer. +and fail it, as if a transient resolver failure happened. + +## `CURL_DBG_RESOLV_FAIL_NEGATIVE` + +With a threaded resolver, make a lookup failing via +`CURL_DBG_RESOLV_FAIL_DELAY` count as an authoritative negative answer, +eligible for negative DNS caching, instead of a transient failure. ## `CURL_DBG_RESOLV_FAIL_IPV6` diff --git a/docs/libcurl/opts/CURLOPT_DNS_CACHE_TIMEOUT.md b/docs/libcurl/opts/CURLOPT_DNS_CACHE_TIMEOUT.md index 5a9b7bbb9a..893852d3ea 100644 --- a/docs/libcurl/opts/CURLOPT_DNS_CACHE_TIMEOUT.md +++ b/docs/libcurl/opts/CURLOPT_DNS_CACHE_TIMEOUT.md @@ -53,7 +53,9 @@ libcurl prunes entries from the DNS cache if it exceeds 30,000 entries no matter which timeout value is used. (Added in version 8.1.0) Since curl 8.16.0, failed name resolves are stored in the DNS cache for half -the set timeout period. +the set timeout period. Since curl 8.22.0, this only happens when the +resolver answered that the name does not exist, not on transient or local +resolver failures. # DEFAULT diff --git a/lib/asyn-ares.c b/lib/asyn-ares.c index d17a6038e2..ef4bbcbcc6 100644 --- a/lib/asyn-ares.c +++ b/lib/asyn-ares.c @@ -308,6 +308,12 @@ CURLcode Curl_async_take_result(struct Curl_easy *data, * CURLE_COULDNT_RESOLVE_* code */ if(!result && !*pdns) { const char *msg = NULL; + /* only an authoritative "does not exist" answer from every query + may be cached as a negative entry, not transient failures like + timeouts or server troubles */ + async->negative_answer = !ares->transient_err && + ((ares->ares_status == ARES_ENOTFOUND) || + (ares->ares_status == ARES_ENODATA)); if(ares->ares_status != ARES_SUCCESS) msg = ares_strerror(ares->ares_status); result = Curl_async_failed(data, async, msg); @@ -569,8 +575,12 @@ static void async_ares_A_cb(void *user_data, int status, int timeouts, ares->res_A = async_ares_node2addr(ares_ai->nodes); ares_freeaddrinfo(ares_ai); } - else if(ares->ares_status != ARES_SUCCESS) /* do not overwrite success */ - ares->ares_status = status; + else { + if((status != ARES_ENOTFOUND) && (status != ARES_ENODATA)) + ares->transient_err = TRUE; + if(ares->ares_status != ARES_SUCCESS) /* do not overwrite success */ + ares->ares_status = status; + } } #ifdef CURLRES_IPV6 @@ -592,8 +602,12 @@ static void async_ares_AAAA_cb(void *user_data, int status, int timeouts, ares->res_AAAA = async_ares_node2addr(ares_ai->nodes); ares_freeaddrinfo(ares_ai); } - else if(ares->ares_status != ARES_SUCCESS) /* do not overwrite success */ - ares->ares_status = status; + else { + if((status != ARES_ENOTFOUND) && (status != ARES_ENODATA)) + ares->transient_err = TRUE; + if(ares->ares_status != ARES_SUCCESS) /* do not overwrite success */ + ares->ares_status = status; + } } #endif /* CURLRES_IPV6 */ diff --git a/lib/asyn-thrdd.c b/lib/asyn-thrdd.c index dc7c7b0418..1ee69f83bf 100644 --- a/lib/asyn-thrdd.c +++ b/lib/asyn-thrdd.c @@ -117,9 +117,11 @@ struct async_thrdd_item { uint16_t port; uint8_t transport; uint8_t dns_queries; + BIT(negative); /* resolver answered that the name does not exist */ #ifdef DEBUGBUILD uint32_t delay_ms; uint32_t delay_fail_ms; + BIT(dbg_negative); #endif char hostname[1]; }; @@ -182,6 +184,8 @@ static struct async_thrdd_item *async_thrdd_item_create( item->delay_fail_ms = (uint32_t)l + c; } } + if(getenv("CURL_DBG_RESOLV_FAIL_NEGATIVE")) + item->dbg_negative = TRUE; } #endif @@ -331,6 +335,26 @@ CURLcode Curl_async_await(struct Curl_easy *data, uint32_t resolv_id, #ifdef HAVE_GETADDRINFO +/* Was the getaddrinfo() failure an authoritative negative answer, + i.e. the resolver responded that the name (or its data) does not + exist? Transient failures like EAI_AGAIN and local troubles must + not count as negative answers. */ +static bool gai_negative(int rc) +{ + switch(rc) { +#ifdef EAI_NONAME + case EAI_NONAME: +#endif +#if defined(EAI_NODATA) && \ + (!defined(EAI_NONAME) || (EAI_NODATA != EAI_NONAME)) + case EAI_NODATA: +#endif + return TRUE; + default: + return FALSE; + } +} + /* Process the item, using Curl_getaddrinfo_ex() */ static void async_thrdd_item_process(void *arg) { @@ -346,6 +370,7 @@ static void async_thrdd_item_process(void *arg) } if(item->delay_fail_ms) { curlx_wait_ms(item->delay_fail_ms); + item->negative = item->dbg_negative; return; } #endif @@ -375,6 +400,7 @@ static void async_thrdd_item_process(void *arg) item->sockerr = SOCKERRNO ? SOCKERRNO : rc; if(item->sockerr == 0) item->sockerr = RESOLVER_ENOMEM; + item->negative = gai_negative(rc); } else { Curl_addrinfo_set_port(item->res, item->port); @@ -394,6 +420,7 @@ static void async_thrdd_item_process(void *arg) } if(item->delay_fail_ms) { curlx_wait_ms(item->delay_fail_ms); + item->negative = item->dbg_negative; return; } #endif @@ -402,6 +429,9 @@ static void async_thrdd_item_process(void *arg) item->sockerr = SOCKERRNO; if(item->sockerr == 0) item->sockerr = RESOLVER_ENOMEM; + /* this resolver cannot tell a transient failure from an + authoritative negative answer, treat it as before */ + item->negative = TRUE; } } @@ -714,6 +744,24 @@ CURLcode Curl_async_take_result(struct Curl_easy *data, return CURLE_AGAIN; Curl_expire_done(data, EXPIRE_ASYNC_NAME); + + /* A failure is an authoritative negative answer, eligible for + negative caching, only when every A/AAAA query performed came + back answering that the name does not exist. A query that + failed transiently or never returned is not an answer. */ + { + const uint8_t ip_queries = + async->dns_queries & (CURL_DNSQ_A | CURL_DNSQ_AAAA); + bool negative = (async->dns_responses & ip_queries) == ip_queries; + if(thrdd->res_A && (thrdd->res_A->res || !thrdd->res_A->negative)) + negative = FALSE; + if(thrdd->res_AAAA && (thrdd->res_AAAA->res || !thrdd->res_AAAA->negative)) + negative = FALSE; + if(!thrdd->res_A && !thrdd->res_AAAA) + negative = FALSE; + async->negative_answer = negative; + } + if(async->result) { result = async->result; goto out; diff --git a/lib/asyn.h b/lib/asyn.h index 7df9b681ba..e4bc05e0dd 100644 --- a/lib/asyn.h +++ b/lib/asyn.h @@ -122,6 +122,8 @@ struct async_ares_ctx { #ifdef USE_HTTPSRR struct Curl_https_rrinfo hinfo; #endif + BIT(transient_err); /* an A/AAAA query failed without the resolver + answering that the name does not exist */ }; void Curl_async_ares_shutdown(struct Curl_easy *data, @@ -251,6 +253,10 @@ struct Curl_resolv_async { BIT(for_proxy); BIT(done); BIT(shutdown); + BIT(negative_answer); /* resolver answered that the name does not + exist. Only such failures may be cached as + negative entries, not transient or local + resolver failures. */ char hostname[1]; }; diff --git a/lib/doh.c b/lib/doh.c index 8b643aa0b4..b1f138f32c 100644 --- a/lib/doh.c +++ b/lib/doh.c @@ -58,12 +58,13 @@ static const char * const errors[] = { "Unexpected CLASS", "No content", "Bad ID", - "Name too long" + "Name too long", + "No such name" }; static const char *doh_strerror(DOHcode code) { - if((code >= DOH_OK) && (code <= DOH_DNS_NAME_TOO_LONG)) + if((code >= DOH_OK) && (code <= DOH_DNS_NXDOMAIN)) return errors[code]; return "bad error code"; } @@ -751,6 +752,8 @@ UNITTEST DOHcode doh_resp_decode(const unsigned char *doh, if(!doh || doh[0] || doh[1]) return DOH_DNS_BAD_ID; /* bad ID */ rcode = doh[3] & 0x0f; + if(rcode == 3) + return DOH_DNS_NXDOMAIN; /* name does not exist */ if(rcode) return DOH_DNS_BAD_RCODE; /* bad rcode */ @@ -1248,6 +1251,7 @@ CURLcode Curl_doh_take_result(struct Curl_easy *data, } else if(!dohp->pending) { DOHcode rc[DOH_SLOT_COUNT]; + bool negative = TRUE; int slot; memset(rc, 0, sizeof(rc)); @@ -1262,6 +1266,11 @@ CURLcode Curl_doh_take_result(struct Curl_easy *data, rc[slot] = doh_resp_decode(curlx_dyn_uptr(&p->body), curlx_dyn_len(&p->body), p->dnstype, &de); + /* Failing without an NXDOMAIN answer - a SERVFAIL-class rcode or + an undecodable response - says nothing about the name. Such a + failure must not be cached as a negative entry. */ + if(rc[slot] && (rc[slot] != DOH_DNS_NXDOMAIN)) + negative = FALSE; if(rc[slot]) { CURL_TRC_DNS(data, "DoH: %s type %s for %s", doh_strerror(rc[slot]), doh_type2name(p->dnstype), dohp->host); @@ -1279,8 +1288,13 @@ CURLcode Curl_doh_take_result(struct Curl_easy *data, } result = doh2ai(&de, dohp->host, dohp->port, &ai); - if(result) + if(result) { + /* a decoded response without any usable address, e.g. only + CNAME records, is an authoritative "no data" answer */ + if((result == CURLE_COULDNT_RESOLVE_HOST) && negative) + async->negative_answer = TRUE; goto error; + } /* we got a response, create a dns entry. */ dns = Curl_dnscache_mk_entry(data, async->dns_queries, @@ -1314,6 +1328,9 @@ CURLcode Curl_doh_take_result(struct Curl_easy *data, *pdns = dns; } /* address processing done */ else { + /* every query failed. Only NXDOMAIN answers for all of them + make this a negative answer, eligible for caching. */ + async->negative_answer = negative; result = async->for_proxy ? CURLE_COULDNT_RESOLVE_PROXY : CURLE_COULDNT_RESOLVE_HOST; } diff --git a/lib/doh.h b/lib/doh.h index cd2aad9254..f4ecec0881 100644 --- a/lib/doh.h +++ b/lib/doh.h @@ -39,12 +39,13 @@ typedef enum { DOH_OUT_OF_MEM, /* 5 */ DOH_DNS_RDATA_LEN, /* 6 */ DOH_DNS_MALFORMAT, /* 7 */ - DOH_DNS_BAD_RCODE, /* 8 - no such name */ + DOH_DNS_BAD_RCODE, /* 8 - unsuccessful rcode, not NXDOMAIN */ DOH_DNS_UNEXPECTED_TYPE, /* 9 */ DOH_DNS_UNEXPECTED_CLASS, /* 10 */ DOH_NO_CONTENT, /* 11 */ DOH_DNS_BAD_ID, /* 12 */ - DOH_DNS_NAME_TOO_LONG /* 13 */ + DOH_DNS_NAME_TOO_LONG, /* 13 */ + DOH_DNS_NXDOMAIN /* 14 - no such name */ } DOHcode; typedef enum { diff --git a/lib/fake_addrinfo.c b/lib/fake_addrinfo.c index 6b04b5d2f5..030dc35283 100644 --- a/lib/fake_addrinfo.c +++ b/lib/fake_addrinfo.c @@ -42,6 +42,7 @@ void r_freeaddrinfo(struct addrinfo *cahead) struct context { struct ares_addrinfo *addr; + int status; }; static void async_addrinfo_cb(void *userp, int status, int timeouts, @@ -49,6 +50,7 @@ static void async_addrinfo_cb(void *userp, int status, int timeouts, { struct context *ctx = (struct context *)userp; (void)timeouts; + ctx->status = status; if(ARES_SUCCESS == status) { ctx->addr = addr; } @@ -190,8 +192,10 @@ int r_getaddrinfo(const char *node, /* free the old */ ares_freeaddrinfo(ctx.addr); } + else if((ctx.status == ARES_ENOTFOUND) || (ctx.status == ARES_ENODATA)) + rc = EAI_NONAME; /* no such name */ else - rc = EAI_NONAME; /* got nothing */ + rc = EAI_AGAIN; /* failed without an authoritative answer */ /* Cleanup */ ares_destroy(channel); diff --git a/lib/hostip.c b/lib/hostip.c index a18d9a62bb..6afe63ff45 100644 --- a/lib/hostip.c +++ b/lib/hostip.c @@ -452,9 +452,15 @@ static CURLcode hostip_resolv_take_result(struct Curl_easy *data, async->queries_ongoing, async->hostname, async->port); result = CURLE_OK; } - else if(result) { + else if(IS_RESOLV_FAIL(result)) { result = Curl_async_failed(data, async, NULL); } + else if(result) { + /* a local failure, not a resolve answer. Keep the error as it + is so it does not get treated as one. */ + CURL_TRC_DNS(data, "resolve error %d for %s:%u", + (int)result, async->hostname, async->port); + } else { CURL_TRC_DNS(data, "resolve complete for %s:%u", async->hostname, async->port); @@ -528,6 +534,10 @@ bool Curl_resolv_knows_https(struct Curl_easy *data, uint32_t resolv_id) #endif /* USE_CURL_ASYNC */ +/* Start resolving. `*pnegative` is only meaningful when this returns + a CURLE_COULDNT_RESOLVE_* failure: TRUE when the resolver answered + that the name does not exist, FALSE on transient or local failures + that must not be cached as negative entries. */ static CURLcode hostip_resolv_start(struct Curl_easy *data, uint8_t dns_queries, const char *hostname, @@ -537,7 +547,8 @@ static CURLcode hostip_resolv_start(struct Curl_easy *data, timediff_t timeout_ms, bool allowDOH, uint32_t *presolv_id, - struct Curl_dns_entry **pdns) + struct Curl_dns_entry **pdns, + bool *pnegative) { #ifdef USE_CURL_ASYNC struct Curl_resolv_async *async = NULL; @@ -546,6 +557,8 @@ static CURLcode hostip_resolv_start(struct Curl_easy *data, size_t hostname_len; CURLcode result = CURLE_OK; + *pnegative = FALSE; + (void)timeout_ms; /* not in all ifdefs */ *presolv_id = 0; *pdns = NULL; @@ -628,8 +641,12 @@ static CURLcode hostip_resolv_start(struct Curl_easy *data, if(result) goto out; addr = Curl_sync_getaddrinfo(data, dns_queries, hostname, port, transport); - if(!addr) + if(!addr) { result = RESOLV_FAIL(for_proxy); + /* the synchronous resolvers do not tell a transient failure from + an authoritative negative answer, treat it as before */ + *pnegative = TRUE; + } #endif out: @@ -657,6 +674,7 @@ out: data->state.async = async; } else { + *pnegative = !!async->negative_answer; Curl_async_destroy(data, async); } } @@ -678,6 +696,7 @@ static CURLcode hostip_resolv(struct Curl_easy *data, size_t hostname_len; CURLcode result = RESOLV_FAIL(for_proxy); bool cache_dns = FALSE; + bool negative = FALSE; (void)timeout_ms; /* not used in all ifdefs */ *presolv_id = 0; @@ -722,14 +741,14 @@ static CURLcode hostip_resolv(struct Curl_easy *data, cache_dns = TRUE; result = hostip_resolv_start(data, dns_queries, hostname, port, transport, for_proxy, timeout_ms, allowDOH, - presolv_id, pdns); + presolv_id, pdns, &negative); } out: if(result && (result != CURLE_AGAIN)) { Curl_dns_entry_unlink(data, pdns); if(IS_RESOLV_FAIL(result)) { - if(cache_dns) + if(cache_dns && negative) Curl_dnscache_add_negative(data, dns_queries, hostname, port); failf(data, "Could not resolve: %s:%u", hostname, port); } @@ -1067,8 +1086,14 @@ CURLcode Curl_resolv_take_result(struct Curl_easy *data, uint32_t resolv_id, Curl_dns_entry_unlink(data, pdns); } else if(IS_RESOLV_FAIL(result)) { - Curl_dnscache_add_negative(data, async->dns_queries, - async->hostname, async->port); + /* Only cache the failure when the resolver answered that the + name does not exist. Transient failures, e.g. an unreachable + or overloaded DNS server or local resource shortages, say + nothing about the name and would poison the cache for every + transfer using it. */ + if(async->negative_answer) + Curl_dnscache_add_negative(data, async->dns_queries, + async->hostname, async->port); failf(data, "Could not resolve: %s:%u", async->hostname, async->port); } else if(result) { diff --git a/tests/http/test_21_resolve.py b/tests/http/test_21_resolve.py index a50997409d..e3f0358d74 100644 --- a/tests/http/test_21_resolve.py +++ b/tests/http/test_21_resolve.py @@ -196,6 +196,75 @@ class TestResolve: r.check_stats(count=1, http_status=200, exitcode=0) assert r.stats[0]['remote_ip'] == '::1' + # transient resolve failures must not be cached as negative + # entries: a second lookup of the same name tries again + @pytest.mark.skipif(condition=not Env.curl_resolv_threaded(), reason="no threaded resolver") + def test_21_11_resolv_transient_uncached(self, env: Env, httpd, nghttpx): + count = 2 + delay_ms = 250 + run_env = os.environ.copy() + run_env['CURL_DBG_RESOLV_FAIL_DELAY'] = f'{delay_ms}' + curl = CurlClient(env=env, run_env=run_env, force_resolv=False) + urls = [f'https://test-again.http.curl.invalid/?id={i}' for i in range(count)] + r = curl.http_download(urls=urls, with_stats=True) + r.check_exit_code(6) + r.check_stats(count=count, http_status=0, exitcode=6) + # not cached as negative: the second transfer resolved again + if env.curl_is_verbose(): + assert not [t for t in r.trace_lines if 'Negative DNS entry' in t], f'{r}' + assert r.stats[1]['time_total'] > (delay_ms / 2) / 1000.0, f'{r.stats[1]}' + + # a negative resolve answer is cached: a second lookup of the same + # name fails right away from the cache + @pytest.mark.skipif(condition=not Env.curl_resolv_threaded(), reason="no threaded resolver") + def test_21_12_resolv_negative_cached(self, env: Env, httpd, nghttpx): + count = 2 + delay_ms = 250 + run_env = os.environ.copy() + run_env['CURL_DBG_RESOLV_FAIL_DELAY'] = f'{delay_ms}' + run_env['CURL_DBG_RESOLV_FAIL_NEGATIVE'] = '1' + curl = CurlClient(env=env, run_env=run_env, force_resolv=False) + urls = [f'https://test-nxdomain.http.curl.invalid/?id={i}' for i in range(count)] + r = curl.http_download(urls=urls, with_stats=True) + r.check_exit_code(6) + r.check_stats(count=count, http_status=0, exitcode=6) + # the second transfer failed right away from the cache entry + if env.curl_is_verbose(): + assert [t for t in r.trace_lines if 'Negative DNS entry' in t], f'{r}' + assert r.stats[1]['time_total'] < (delay_ms / 2) / 1000.0, f'{r.stats[1]}' + + # dnsd giving NXDOMAIN for all families: the negative answer is + # cached and a second lookup of the same name uses the cache + @pytest.mark.skipif(condition=not Env.curl_override_dns(), reason="no DNS override") + def test_21_13_dnsd_nxdomain_cached(self, env: Env, httpd, dnsd): + count = 2 + dnsd.set_answers(rcode_a=3, rcode_aaaa=3) + run_env = os.environ.copy() + run_env['CURL_DNS_SERVER'] = f'127.0.0.1:{dnsd.port}' + curl = CurlClient(env=env, run_env=run_env, force_resolv=False) + urls = [f'https://test-nx.http.curl.invalid/?id={i}' for i in range(count)] + r = curl.http_download(urls=urls, with_stats=True) + r.check_exit_code(6) + r.check_stats(count=count, http_status=0, exitcode=6) + if env.curl_is_verbose(): + assert [t for t in r.trace_lines if 'Negative DNS entry' in t], f'{r}' + + # dnsd failing one family with SERVFAIL: not an authoritative + # negative answer, a second lookup of the same name tries again + @pytest.mark.skipif(condition=not Env.curl_override_dns(), reason="no DNS override") + def test_21_14_dnsd_servfail_uncached(self, env: Env, httpd, dnsd): + count = 2 + dnsd.set_answers(rcode_a=2, rcode_aaaa=3) + run_env = os.environ.copy() + run_env['CURL_DNS_SERVER'] = f'127.0.0.1:{dnsd.port}' + curl = CurlClient(env=env, run_env=run_env, force_resolv=False) + urls = [f'https://test-sf.http.curl.invalid/?id={i}' for i in range(count)] + r = curl.http_download(urls=urls, with_stats=True) + r.check_exit_code(6) + r.check_stats(count=count, http_status=0, exitcode=6) + if env.curl_is_verbose(): + assert not [t for t in r.trace_lines if 'Negative DNS entry' in t], f'{r}' + def _clean_files(self, files): for file in files: if os.path.exists(file): diff --git a/tests/http/testenv/dnsd.py b/tests/http/testenv/dnsd.py index a7dc088557..8f6bb3edc8 100644 --- a/tests/http/testenv/dnsd.py +++ b/tests/http/testenv/dnsd.py @@ -156,7 +156,9 @@ class Dnsd: https: Optional[List[str]] = None, delay_a_ms: int = 0, delay_aaaa_ms: int = 0, - delay_https_ms: int = 0): + delay_https_ms: int = 0, + rcode_a: int = 0, + rcode_aaaa: int = 0): conf = [] if addr_a: conf.extend([f'A: {addr}' for addr in addr_a]) @@ -170,6 +172,10 @@ class Dnsd: conf.append(f'Delay-AAAA: {delay_aaaa_ms}') if delay_https_ms: conf.append(f'Delay-HTTPS: {delay_https_ms}') + if rcode_a: + conf.append(f'Rcode-A: {rcode_a}') + if rcode_aaaa: + conf.append(f'Rcode-AAAA: {rcode_aaaa}') conf.append('\n') with open(self._conf_file, 'w') as fd: fd.write("\n".join(conf)) diff --git a/tests/server/dnsd.c b/tests/server/dnsd.c index ca89c9c053..87f32384ee 100644 --- a/tests/server/dnsd.c +++ b/tests/server/dnsd.c @@ -327,6 +327,8 @@ static unsigned char ancount_aaaa; static timediff_t a_delay_ms; static timediff_t aaaa_delay_ms; static timediff_t https_delay_ms; +static unsigned char rcode_a; +static unsigned char rcode_aaaa; static int query_id = -1; @@ -444,15 +446,18 @@ create_resp(int qid, const struct sockaddr *addr, curl_socklen_t addrlen, 0x0, 0x0 /* ARCOUNT */ }; uint16_t ancount = 0; + unsigned char rcode = 0; switch(qtype) { case QTYPE_A: ancount = ancount_a; delay_ms = a_delay_ms; + rcode = rcode_a; break; case QTYPE_AAAA: ancount = ancount_aaaa; delay_ms = aaaa_delay_ms; + rcode = rcode_aaaa; break; case QTYPE_HTTPS: if(httpsrr.dlen) @@ -460,6 +465,8 @@ create_resp(int qid, const struct sockaddr *addr, curl_socklen_t addrlen, delay_ms = https_delay_ms; break; } + if(rcode) + ancount = 0; resp = curlx_calloc(1, sizeof(*resp)); if(!resp) @@ -478,6 +485,11 @@ create_resp(int qid, const struct sockaddr *addr, curl_socklen_t addrlen, header[0] = (uint8_t)(id >> 8); header[1] = (uint8_t)(id & 0xff); + if(rcode) { + header[3] = (uint8_t)((header[3] & 0xf0) | (rcode & 0x0f)); + logmsg("[%d] response rcode %u", qid, (unsigned int)rcode); + } + header[6] = (uint8_t)(ancount >> 8); header[7] = (uint8_t)(ancount & 0xff); @@ -491,7 +503,7 @@ create_resp(int qid, const struct sockaddr *addr, curl_socklen_t addrlen, switch(qtype) { case QTYPE_A: - for(a = 0; a < ancount_a; a++) { + for(a = 0; !rcode && (a < ancount_a); a++) { const unsigned char *store = ipv4_pref; const char *ip; if(add_answer(&resp->body, store, sizeof(ipv4_pref), QTYPE_A)) @@ -504,7 +516,7 @@ create_resp(int qid, const struct sockaddr *addr, curl_socklen_t addrlen, logmsg("[%d] response A empty", qid); break; case QTYPE_AAAA: - for(a = 0; a < ancount_aaaa; a++) { + for(a = 0; !rcode && (a < ancount_aaaa); a++) { const unsigned char *store = ipv6_pref; const char *ip; if(add_answer(&resp->body, store, sizeof(ipv6_pref), QTYPE_AAAA)) @@ -659,6 +671,7 @@ static void read_instructions(void) } /* reset defaults */ a_delay_ms = aaaa_delay_ms = https_delay_ms = 0; + rcode_a = rcode_aaaa = 0; blob_reset(&httpsrr); finfo_last = finfo; @@ -721,6 +734,24 @@ static void read_instructions(void) rc = 1; } } + else if(!strncmp("Rcode-A: ", buf, 9)) { + curl_off_t code; + const char *pc = &buf[9]; + rc = 0; + if(!curlx_str_number(&pc, &code, 15)) { + rcode_a = (unsigned char)code; + rc = 1; + } + } + else if(!strncmp("Rcode-AAAA: ", buf, 12)) { + curl_off_t code; + const char *pc = &buf[12]; + rc = 0; + if(!curlx_str_number(&pc, &code, 15)) { + rcode_aaaa = (unsigned char)code; + rc = 1; + } + } else { /* accept empty line */ rc = buf[0] ? 0 : 1; diff --git a/tests/unit/unit1650.c b/tests/unit/unit1650.c index cdc3375d3c..4e307474fd 100644 --- a/tests/unit/unit1650.c +++ b/tests/unit/unit1650.c @@ -86,6 +86,8 @@ static CURLcode test_unit1650(const char *arg) CURL_DNS_TYPE_A, DOH_DNS_BAD_ID, NULL }, {"\x00\x00\x00\x01\x00\x01\x00\x01\x00\x01\x00\x01", 12, CURL_DNS_TYPE_A, DOH_DNS_BAD_RCODE, NULL }, + {"\x00\x00\x00\x03\x00\x01\x00\x01\x00\x01\x00\x01", 12, + CURL_DNS_TYPE_A, DOH_DNS_NXDOMAIN, NULL }, {"\x00\x00\x01\x00\x00\x01\x00\x01\x00\x00\x00\x00\x03\x66\x6f\x6f", 16, CURL_DNS_TYPE_A, DOH_DNS_OUT_OF_RANGE, NULL }, {"\x00\x00\x01\x00\x00\x01\x00\x01\x00\x00\x00\x00\x03\x66\x6f\x6f\x00", 17,