hostip: only cache negative resolves for authoritative answers
No binary (build failed)
—
(log yok)
| @@ -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` | |
| @@ -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 | |
| @@ -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 */ | |
| @@ -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; | |
| @@ -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]; | |
| }; | |
| @@ -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; | |
| } | |
| @@ -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 { | |
| @@ -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); | |
| @@ -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) { | |
| @@ -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): | |
| @@ -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)) | |
| @@ -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; | |
| @@ -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, | |