mirror of
https://github.com/curl/curl.git
synced 2026-07-22 13:27:15 +03:00
hostip: only cache negative resolves for authoritative answers
Closes #22302
This commit is contained in:
parent
353d05ef0c
commit
c3ae9ef822
13 changed files with 253 additions and 22 deletions
|
|
@ -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];
|
||||
};
|
||||
|
||||
|
|
|
|||
23
lib/doh.c
23
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
39
lib/hostip.c
39
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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue