diff --git a/bin/named/config.c b/bin/named/config.c index 77c0abaaa5..2850ff9960 100644 --- a/bin/named/config.c +++ b/bin/named/config.c @@ -194,9 +194,10 @@ options {\n\ servfail-ttl 1;\n\ # sortlist \n\ stale-answer-enable false;\n\ - stale-refresh-time 30; /* 30 seconds */\n\ + stale-answer-client-timeout 1800; /* in milliseconds */\n\ stale-answer-ttl 30; /* 30 seconds */\n\ stale-cache-enable false;\n\ + stale-refresh-time 30; /* 30 seconds */\n\ synth-from-dnssec no;\n\ # topology \n\ transfer-format many-answers;\n\ diff --git a/bin/named/server.c b/bin/named/server.c index 839438161c..6888333087 100644 --- a/bin/named/server.c +++ b/bin/named/server.c @@ -4485,6 +4485,11 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config, view->staleanswersok = dns_stale_answer_conf; } + obj = NULL; + result = named_config_get(maps, "stale-answer-client-timeout", &obj); + INSIST(result == ISC_R_SUCCESS); + view->staleanswerclienttimeout = cfg_obj_asuint32(obj); + obj = NULL; result = named_config_get(maps, "stale-refresh-time", &obj); INSIST(result == ISC_R_SUCCESS); diff --git a/lib/dns/include/dns/db.h b/lib/dns/include/dns/db.h index b79dcae0fa..1e01c43dfd 100644 --- a/lib/dns/include/dns/db.h +++ b/lib/dns/include/dns/db.h @@ -239,8 +239,35 @@ struct dns_dbonupdatelistener { #define DNS_DBFIND_FORCENSEC3 0x0080 #define DNS_DBFIND_ADDITIONALOK 0x0100 #define DNS_DBFIND_NOZONECUT 0x0200 -#define DNS_DBFIND_STALEOK 0x0400 + +/* + * DNS_DBFIND_STALEOK: This flag is set when BIND fails to refresh a + * RRset due to timeout (resolver-query-timeout), its intent is to + * try to look for stale data in cache as a fallback, but only if + * stale answers are enabled in configuration. + * + * This flag is also used to activate stale-refresh-time window, since it + * is the only way the database knows that a resolution has failed. + */ +#define DNS_DBFIND_STALEOK 0x0400 + +/* + * DNS_DBFIND_STALEENABLED: This flag is used as a hint to the database + * that it may use stale data. It is always set during query lookup if + * stale answers are enabled, but only effectively used during + * stale-refresh-time window. Also during this window, the resolver will + * not try to resolve the query, in other words no attempt to refresh the + * data in cache is made when the stale-refresh-time window is active. + */ #define DNS_DBFIND_STALEENABLED 0x0800 + +/* + * DNS_DBFIND_STALEONLY: This new introduced flag is used when we want + * stale data from the database, but not due to a failure in resolution, + * it also doesn't require stale-refresh-time window timer to be active. + * As long as there is a stale RRset available, it should be returned. + */ +#define DNS_DBFIND_STALEONLY 0x1000 /*@}*/ /*@{*/ diff --git a/lib/dns/include/dns/events.h b/lib/dns/include/dns/events.h index 0e066310d1..50a8d01ac6 100644 --- a/lib/dns/include/dns/events.h +++ b/lib/dns/include/dns/events.h @@ -78,6 +78,7 @@ #define DNS_EVENT_CATZDELZONE (ISC_EVENTCLASS_DNS + 56) #define DNS_EVENT_RPZUPDATED (ISC_EVENTCLASS_DNS + 57) #define DNS_EVENT_STARTUPDATE (ISC_EVENTCLASS_DNS + 58) +#define DNS_EVENT_TRYSTALE (ISC_EVENTCLASS_DNS + 59) #define DNS_EVENT_FIRSTEVENT (ISC_EVENTCLASS_DNS + 0) #define DNS_EVENT_LASTEVENT (ISC_EVENTCLASS_DNS + 65535) diff --git a/lib/dns/include/dns/resolver.h b/lib/dns/include/dns/resolver.h index b9b20e1577..07b94fdcd1 100644 --- a/lib/dns/include/dns/resolver.h +++ b/lib/dns/include/dns/resolver.h @@ -129,9 +129,10 @@ typedef enum { dns_quotatype_zone = 0, dns_quotatype_server } dns_quotatype_t; * if possible. */ /* Reserved in use by adb.c 0x00400000 */ -#define DNS_FETCHOPT_EDNSVERSIONSET 0x00800000 -#define DNS_FETCHOPT_EDNSVERSIONMASK 0xff000000 -#define DNS_FETCHOPT_EDNSVERSIONSHIFT 24 +#define DNS_FETCHOPT_EDNSVERSIONSET 0x00800000 +#define DNS_FETCHOPT_EDNSVERSIONMASK 0xff000000 +#define DNS_FETCHOPT_EDNSVERSIONSHIFT 24 +#define DNS_FETCHOPT_TRYSTALE_ONTIMEOUT 0x01000000 /* * Upper bounds of class of query RTT (ms). Corresponds to diff --git a/lib/dns/include/dns/view.h b/lib/dns/include/dns/view.h index 1da372a89a..c0a4eb5d53 100644 --- a/lib/dns/include/dns/view.h +++ b/lib/dns/include/dns/view.h @@ -173,6 +173,7 @@ struct dns_view { dns_stale_answer_t staleanswersok; /* rndc setting */ bool staleanswersenable; /* named.conf setting * */ + uint32_t staleanswerclienttimeout; uint16_t nocookieudp; uint16_t padding; dns_acl_t * pad_acl; diff --git a/lib/dns/rbtdb.c b/lib/dns/rbtdb.c index b9a46cea40..24442bd3ef 100644 --- a/lib/dns/rbtdb.c +++ b/lib/dns/rbtdb.c @@ -4562,6 +4562,13 @@ check_stale_header(dns_rbtnode_t *node, rdatasetheader_t *header, */ if ((search->options & DNS_DBFIND_STALEOK) != 0) { header->last_refresh_fail_ts = search->now; + } else if ((search->options & DNS_DBFIND_STALEONLY) != + 0) { + /* + * We want stale RRset only, so we don't skip + * it. + */ + return (false); } else if ((search->options & DNS_DBFIND_STALEENABLED) != 0 && search->now < diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c index b0a0ec7820..066db581ea 100644 --- a/lib/dns/resolver.c +++ b/lib/dns/resolver.c @@ -307,7 +307,9 @@ struct fetchctx { dns_rdataset_t nameservers; atomic_uint_fast32_t attributes; isc_timer_t *timer; + isc_timer_t *timer_try_stale; isc_time_t expires; + isc_time_t expires_try_stale; isc_interval_t interval; dns_message_t *qmessage; ISC_LIST(resquery_t) queries; @@ -1135,6 +1137,16 @@ fctx_starttimer(fetchctx_t *fctx) { NULL, true)); } +static inline isc_result_t +fctx_starttimer_trystale(fetchctx_t *fctx) { + /* + * Start the stale-answer-client-timeout timer for fctx. + */ + + return (isc_timer_reset(fctx->timer_try_stale, isc_timertype_once, + &fctx->expires_try_stale, NULL, true)); +} + static inline void fctx_stoptimer(fetchctx_t *fctx) { isc_result_t result; @@ -1153,6 +1165,22 @@ fctx_stoptimer(fetchctx_t *fctx) { } } +static inline void +fctx_stoptimer_trystale(fetchctx_t *fctx) { + isc_result_t result; + + if (fctx->timer_try_stale != NULL) { + result = isc_timer_reset(fctx->timer_try_stale, + isc_timertype_inactive, NULL, NULL, + true); + if (result != ISC_R_SUCCESS) { + UNEXPECTED_ERROR(__FILE__, __LINE__, + "isc_timer_reset(): %s", + isc_result_totext(result)); + } + } +} + static inline isc_result_t fctx_startidletimer(fetchctx_t *fctx, isc_interval_t *interval) { /* @@ -1537,6 +1565,7 @@ fctx_stopqueries(fetchctx_t *fctx, bool no_response, bool age_untried) { FCTXTRACE("stopqueries"); fctx_cancelqueries(fctx, no_response, age_untried); fctx_stoptimer(fctx); + fctx_stoptimer_trystale(fctx); } static inline void @@ -1697,6 +1726,16 @@ fctx_sendevents(fetchctx_t *fctx, isc_result_t result, int line) { event = next_event) { next_event = ISC_LIST_NEXT(event, ev_link); ISC_LIST_UNLINK(fctx->events, event, ev_link); + if (event->ev_type == DNS_EVENT_TRYSTALE) { + /* + * Not applicable to TRY STALE events, this function is + * called when the fetch has either completed or timed + * out due to resolver-query-timeout being reached. + */ + isc_task_detach((isc_task_t **)&event->ev_sender); + isc_event_free((isc_event_t **)&event); + continue; + } task = event->ev_sender; event->ev_sender = fctx; event->vresult = fctx->vresult; @@ -4182,7 +4221,13 @@ fctx_try(fetchctx_t *fctx, bool retrying, bool badcache) { */ if (fctx->minimized && !fctx->forwarding) { unsigned int options = fctx->options; - options &= ~DNS_FETCHOPT_QMINIMIZE; + /* + * Also clear DNS_FETCHOPT_TRYSTALE_ONTIMEOUT here, otherwise + * every query minimization step will activate the try-stale + * timer again. + */ + options &= ~(DNS_FETCHOPT_QMINIMIZE | + DNS_FETCHOPT_TRYSTALE_ONTIMEOUT); /* * Is another QNAME minimization fetch still running? @@ -4222,6 +4267,7 @@ fctx_try(fetchctx_t *fctx, bool retrying, bool badcache) { fctx_increference(fctx); task = res->buckets[bucketnum].task; fctx_stoptimer(fctx); + fctx_stoptimer_trystale(fctx); result = dns_resolver_createfetch( fctx->res, &fctx->qminname, fctx->qmintype, &fctx->domain, &fctx->nameservers, NULL, NULL, 0, @@ -4247,6 +4293,7 @@ fctx_try(fetchctx_t *fctx, bool retrying, bool badcache) { } fctx_increference(fctx); + result = fctx_query(fctx, addrinfo, fctx->options); if (result != ISC_R_SUCCESS) { fctx_done(fctx, result, __LINE__); @@ -4504,6 +4551,9 @@ fctx_destroy(fetchctx_t *fctx) { isc_counter_detach(&fctx->qc); fcount_decr(fctx); isc_timer_detach(&fctx->timer); + if (fctx->timer_try_stale != NULL) { + isc_timer_detach(&fctx->timer_try_stale); + } dns_message_detach(&fctx->qmessage); if (dns_name_countlabels(&fctx->domain) > 0) { dns_name_free(&fctx->domain, fctx->mctx); @@ -4579,6 +4629,57 @@ fctx_timeout(isc_task_t *task, isc_event_t *event) { isc_event_free(&event); } +/* + * Fetch event handlers called if stale answers are enabled + * (stale-answer-enabled) and the fetch took more than + * stale-answer-client-timeout to complete. + */ +static void +fctx_timeout_try_stale(isc_task_t *task, isc_event_t *event) { + fetchctx_t *fctx = event->ev_arg; + dns_fetchevent_t *dns_event, *next_event; + isc_task_t *sender_task; + unsigned int count = 0; + + REQUIRE(VALID_FCTX(fctx)); + + UNUSED(task); + + FCTXTRACE("timeout_try_stale"); + + if (event->ev_type != ISC_TIMEREVENT_LIFE) { + return; + } + + LOCK(&fctx->res->buckets[fctx->bucketnum].lock); + + /* + * Trigger events of type DNS_EVENT_TRYSTALE. + */ + for (dns_event = ISC_LIST_HEAD(fctx->events); dns_event != NULL; + dns_event = next_event) + { + next_event = ISC_LIST_NEXT(dns_event, ev_link); + + if (dns_event->ev_type != DNS_EVENT_TRYSTALE) { + continue; + } + + ISC_LIST_UNLINK(fctx->events, dns_event, ev_link); + sender_task = dns_event->ev_sender; + dns_event->ev_sender = fctx; + dns_event->vresult = ISC_R_TIMEDOUT; + dns_event->result = ISC_R_TIMEDOUT; + + isc_task_sendanddetach(&sender_task, ISC_EVENT_PTR(&dns_event)); + count++; + } + + UNLOCK(&fctx->res->buckets[fctx->bucketnum].lock); + + isc_event_free(&event); +} + static void fctx_shutdown(fetchctx_t *fctx) { isc_event_t *cevent; @@ -4760,6 +4861,9 @@ fctx_start(isc_task_t *task, isc_event_t *event) { * All is well. Start working on the fetch. */ result = fctx_starttimer(fctx); + if (result == ISC_R_SUCCESS && fctx->timer_try_stale != NULL) { + result = fctx_starttimer_trystale(fctx); + } if (result != ISC_R_SUCCESS) { fctx_done(fctx, result, __LINE__); } else { @@ -4826,6 +4930,34 @@ fctx_join(fetchctx_t *fctx, isc_task_t *task, const isc_sockaddr_t *client, return (ISC_R_SUCCESS); } +static inline void +fctx_add_event(fetchctx_t *fctx, isc_task_t *task, const isc_sockaddr_t *client, + dns_messageid_t id, isc_taskaction_t action, void *arg, + dns_fetch_t *fetch, isc_eventtype_t event_type) { + isc_task_t *tclone; + dns_fetchevent_t *event; + /* + * We store the task we're going to send this event to in the + * sender field. We'll make the fetch the sender when we actually + * send the event. + */ + tclone = NULL; + isc_task_attach(task, &tclone); + event = (dns_fetchevent_t *)isc_event_allocate(fctx->res->mctx, tclone, + event_type, action, arg, + sizeof(*event)); + event->result = DNS_R_SERVFAIL; + event->qtype = fctx->type; + event->db = NULL; + event->node = NULL; + event->rdataset = NULL; + event->sigrdataset = NULL; + event->fetch = fetch; + event->client = client; + event->id = id; + ISC_LIST_APPEND(fctx->events, event, ev_link); +} + static inline void log_ns_ttl(fetchctx_t *fctx, const char *where) { char namebuf[DNS_NAME_FORMATSIZE]; @@ -4854,6 +4986,7 @@ fctx_create(dns_resolver_t *res, const dns_name_t *name, dns_rdatatype_t type, char typebuf[DNS_RDATATYPE_FORMATSIZE]; isc_mem_t *mctx; size_t p; + bool try_stale; /* * Caller must be holding the lock for bucket number 'bucketnum'. @@ -5078,6 +5211,29 @@ fctx_create(dns_resolver_t *res, const dns_name_t *name, dns_rdatatype_t type, goto cleanup_qmessage; } + try_stale = ((options & DNS_FETCHOPT_TRYSTALE_ONTIMEOUT) != 0); + if (try_stale) { + INSIST(res->view->staleanswerclienttimeout <= + (res->query_timeout - 1000)); + /* + * Compute an expiration time after which stale data will + * attempted to be served, if stale answers are enabled and + * target RRset is available in cache. + */ + isc_interval_set( + &interval, res->view->staleanswerclienttimeout / 1000, + res->view->staleanswerclienttimeout % 1000 * 1000000); + iresult = isc_time_nowplusinterval(&fctx->expires_try_stale, + &interval); + if (iresult != ISC_R_SUCCESS) { + UNEXPECTED_ERROR(__FILE__, __LINE__, + "isc_time_nowplusinterval: %s", + isc_result_totext(iresult)); + result = ISC_R_UNEXPECTED; + goto cleanup_qmessage; + } + } + /* * Default retry interval initialization. We set the interval now * mostly so it won't be uninitialized. It will be set to the @@ -5086,10 +5242,11 @@ fctx_create(dns_resolver_t *res, const dns_name_t *name, dns_rdatatype_t type, isc_interval_set(&fctx->interval, 2, 0); /* - * Create an inactive timer. It will be made active when the fetch - * is actually started. + * Create an inactive timer for resolver-query-timeout. It + * will be made active when the fetch is actually started. */ fctx->timer = NULL; + iresult = isc_timer_create(res->timermgr, isc_timertype_inactive, NULL, NULL, res->buckets[bucketnum].task, fctx_timeout, fctx, &fctx->timer); @@ -5100,6 +5257,26 @@ fctx_create(dns_resolver_t *res, const dns_name_t *name, dns_rdatatype_t type, goto cleanup_qmessage; } + /* + * If stale answers are enabled, then create an inactive timer + * for stale-answer-client-timeout. It will be made active when + * the fetch is actually started. + */ + fctx->timer_try_stale = NULL; + if (try_stale) { + iresult = isc_timer_create( + res->timermgr, isc_timertype_inactive, NULL, NULL, + res->buckets[bucketnum].task, fctx_timeout_try_stale, + fctx, &fctx->timer_try_stale); + if (iresult != ISC_R_SUCCESS) { + UNEXPECTED_ERROR(__FILE__, __LINE__, + "isc_timer_create: %s", + isc_result_totext(iresult)); + result = ISC_R_UNEXPECTED; + goto cleanup_qmessage; + } + } + /* * Attach to the view's cache and adb. */ @@ -5144,6 +5321,7 @@ cleanup_mctx: dns_adb_detach(&fctx->adb); dns_db_detach(&fctx->cache); isc_timer_detach(&fctx->timer); + isc_timer_detach(&fctx->timer_try_stale); cleanup_qmessage: dns_message_detach(&fctx->qmessage); @@ -5357,6 +5535,23 @@ clone_results(fetchctx_t *fctx) { for (event = ISC_LIST_NEXT(hevent, ev_link); event != NULL; event = ISC_LIST_NEXT(event, ev_link)) { + if (event->ev_type == DNS_EVENT_TRYSTALE) { + /* + * We don't need to clone resulting data to this + * type of event, as its associated callback is only + * called when stale-answer-client-timeout triggers, + * and the logic in there doesn't expect any result + * as input, as it will itself lookup for stale data + * in cache to use as result, if any is available. + * + * Also, if we reached this point, then the whole fetch + * context is done, it will cancel timers, process + * associated callbacks of type DNS_EVENT_FETCHDONE, and + * silently remove/free events of type + * DNS_EVENT_TRYSTALE. + */ + continue; + } name = dns_fixedname_name(&event->foundname); dns_name_copynf(hname, name); event->result = hevent->result; @@ -6126,6 +6321,7 @@ cache_name(fetchctx_t *fctx, dns_name_t *name, dns_message_t *message, (!need_validation)) { have_answer = true; event = ISC_LIST_HEAD(fctx->events); + if (event != NULL) { adbp = &event->db; aname = dns_fixedname_name(&event->foundname); @@ -10815,6 +11011,14 @@ dns_resolver_createfetch(dns_resolver_t *res, const dns_name_t *name, result = fctx_join(fctx, task, client, id, action, arg, rdataset, sigrdataset, fetch); + + if (result == ISC_R_SUCCESS && + ((options & DNS_FETCHOPT_TRYSTALE_ONTIMEOUT) != 0)) + { + fctx_add_event(fctx, task, client, id, action, arg, fetch, + DNS_EVENT_TRYSTALE); + } + if (new_fctx) { if (result == ISC_R_SUCCESS) { /* diff --git a/lib/isccfg/namedconf.c b/lib/isccfg/namedconf.c index b7ed28d24e..6b7c6fbabe 100644 --- a/lib/isccfg/namedconf.c +++ b/lib/isccfg/namedconf.c @@ -2027,6 +2027,7 @@ static cfg_clausedef_t view_clauses[] = { { "servfail-ttl", &cfg_type_duration, 0 }, { "sortlist", &cfg_type_bracketed_aml, 0 }, { "stale-answer-enable", &cfg_type_boolean, 0 }, + { "stale-answer-client-timeout", &cfg_type_uint32, 0 }, { "stale-answer-ttl", &cfg_type_duration, 0 }, { "stale-cache-enable", &cfg_type_boolean, 0 }, { "stale-refresh-time", &cfg_type_duration, 0 }, diff --git a/lib/ns/client.c b/lib/ns/client.c index 0889688d49..7cc96efc99 100644 --- a/lib/ns/client.c +++ b/lib/ns/client.c @@ -879,7 +879,9 @@ ns_client_error(ns_client_t *client, isc_result_t result) { } } - ns_client_send(client); + if ((client->query.attributes & NS_QUERYATTR_ANSWERED) == 0) { + ns_client_send(client); + } } isc_result_t diff --git a/lib/ns/include/ns/query.h b/lib/ns/include/ns/query.h index 011a8b4c13..55060a3a8c 100644 --- a/lib/ns/include/ns/query.h +++ b/lib/ns/include/ns/query.h @@ -117,6 +117,7 @@ struct ns_query { #define NS_QUERYATTR_DNS64EXCLUDE 0x08000 #define NS_QUERYATTR_RRL_CHECKED 0x10000 #define NS_QUERYATTR_REDIRECT 0x20000 +#define NS_QUERYATTR_ANSWERED 0x40000 typedef struct query_ctx query_ctx_t; diff --git a/lib/ns/query.c b/lib/ns/query.c index a8712f3a99..9dcbc7ab7c 100644 --- a/lib/ns/query.c +++ b/lib/ns/query.c @@ -133,6 +133,12 @@ #define REDIRECT(c) (((c)->query.attributes & NS_QUERYATTR_REDIRECT) != 0) +/*% Was the query already answered due to stale-answer-client-timeout? */ +#define QUERY_ANSWERED(c) (((c)->query.attributes & NS_QUERYATTR_ANSWERED) != 0) + +/*% Does the query only wants to check for stale RRset? */ +#define QUERY_STALEONLY(c) (((c)->query.dboptions & DNS_DBFIND_STALEONLY) != 0) + /*% Does the rdataset 'r' have an attached 'No QNAME Proof'? */ #define NOQNAME(r) (((r)->attributes & DNS_RDATASETATTR_NOQNAME) != 0) @@ -557,8 +563,13 @@ query_send(ns_client_t *client) { } inc_stats(client, counter); - ns_client_send(client); - isc_nmhandle_detach(&client->reqhandle); + if (!QUERY_ANSWERED(client)) { + ns_client_send(client); + } + + if (!QUERY_STALEONLY(client)) { + isc_nmhandle_detach(&client->reqhandle); + } } static void @@ -585,7 +596,10 @@ query_error(ns_client_t *client, isc_result_t result, int line) { log_queryerror(client, result, line, loglevel); ns_client_error(client, result); - isc_nmhandle_detach(&client->reqhandle); + + if (!QUERY_STALEONLY(client)) { + isc_nmhandle_detach(&client->reqhandle); + } } static void @@ -598,7 +612,10 @@ query_next(ns_client_t *client, isc_result_t result) { inc_stats(client, ns_statscounter_failure); } ns_client_drop(client, result); - isc_nmhandle_detach(&client->reqhandle); + + if (!QUERY_STALEONLY(client)) { + isc_nmhandle_detach(&client->reqhandle); + } } static inline void @@ -5158,7 +5175,7 @@ qctx_freedata(query_ctx_t *qctx) { dns_db_detach(&qctx->zdb); } - if (qctx->event != NULL) { + if (qctx->event != NULL && !QUERY_STALEONLY(qctx->client)) { free_devent(qctx->client, ISC_EVENT_PTR(&qctx->event), &qctx->event); } @@ -5583,6 +5600,7 @@ query_lookup(query_ctx_t *qctx) { unsigned int dboptions; dns_ttl_t stale_refresh = 0; bool dbfind_stale = false; + bool stale_ok; CCTRACE(ISC_LOG_DEBUG(3), "query_lookup"); @@ -5681,10 +5699,10 @@ query_lookup(query_ctx_t *qctx) { * answer, otherwise "fresh" answers are also treated as stale. */ dbfind_stale = ((dboptions & DNS_DBFIND_STALEOK) != 0); - if (dbfind_stale != 0 || - (((dboptions & DNS_DBFIND_STALEENABLED) != 0) && - STALE(qctx->rdataset))) - { + stale_ok = ((dboptions & + (DNS_DBFIND_STALEENABLED | DNS_DBFIND_STALEONLY)) != 0); + + if (dbfind_stale != 0 || (stale_ok && STALE(qctx->rdataset))) { char namebuf[DNS_NAME_FORMATSIZE]; bool success; @@ -5709,6 +5727,12 @@ query_lookup(query_ctx_t *qctx) { "%s resolver failure, stale answer %s", namebuf, success ? "used" : "unavailable"); + } else if ((dboptions & DNS_DBFIND_STALEONLY) != 0) { + isc_log_write(ns_lctx, NS_LOGCATEGORY_SERVE_STALE, + NS_LOGMODULE_QUERY, ISC_LOG_INFO, + "%s client timeout, stale answer %s", + namebuf, + success ? "used" : "unavailable"); } else { isc_log_write(ns_lctx, NS_LOGCATEGORY_SERVE_STALE, NS_LOGMODULE_QUERY, ISC_LOG_INFO, @@ -5719,21 +5743,75 @@ query_lookup(query_ctx_t *qctx) { } if (!success) { - QUERY_ERROR(qctx, DNS_R_SERVFAIL); - return (ns_query_done(qctx)); + /* + * If DNS_DBFIND_STALEONLY is set then it means + * stale-answer-client-timeout was triggered, in + * that case we only want to check if a stale RRset is + * available, if that's the case we promptly answer the + * client with the stale data found. If a stale RRset is + * not available then we must wait for the original + * query to be resumed in order to build a proper + * answer. + */ + if ((dboptions & DNS_DBFIND_STALEONLY) == 0) { + QUERY_ERROR(qctx, DNS_R_SERVFAIL); + return (ns_query_done(qctx)); + } + + return (result); } } - return (query_gotanswer(qctx, result)); + + /* + * If DNS_DBFIND_STALEONLY is disabled then we proceed as normal, + * otherwise we only proceed with query_gotanswer if we + * successfully found a stale RRset in cache. + */ + if (((dboptions & DNS_DBFIND_STALEONLY) == 0) || + result == ISC_R_SUCCESS || result == DNS_R_GLUE || + result == DNS_R_ZONECUT) + { + return (query_gotanswer(qctx, result)); + } cleanup: return (result); } /* - * Event handler to resume processing a query after recursion. - * If the query has timed out or been canceled or the system - * is shutting down, clean up and exit; otherwise, call - * query_resume() to continue the ongoing work. + * Create a new query context with the sole intent + * of looking up for a stale RRset in cache. + * If an entry is found, we mark the original query as + * answered, in order to avoid answering the query twice, + * when the original fetch finishes. + */ +static inline void +query_lookup_staleonly(ns_client_t *client, dns_fetchevent_t *devent) { + query_ctx_t qctx; + isc_result_t result; + + qctx_init(client, &devent, client->query.qtype, &qctx); + dns_db_attach(client->view->cachedb, &qctx.db); + client->query.dboptions |= DNS_DBFIND_STALEONLY; + result = query_lookup(&qctx); + if (result == ISC_R_SUCCESS) { + client->query.attributes |= NS_QUERYATTR_ANSWERED; + } + if (qctx.node != NULL) { + dns_db_detachnode(qctx.db, &qctx.node); + } + qctx_freedata(&qctx); + client->query.dboptions &= ~DNS_DBFIND_STALEONLY; + qctx_destroy(&qctx); + isc_event_free(ISC_EVENT_PTR(&qctx.event)); +} + +/* + * Event handler to resume processing a query after recursion, or when a + * client timeout is triggered. If the query has timed out or been cancelled + * or the system is shutting down, clean up and exit. If a client timeout is + * triggered, see if we can respond with a stale answer from cache. Otherwise, + * call query_resume() to continue the ongoing work. */ static void fetch_callback(isc_task_t *task, isc_event_t *event) { @@ -5748,7 +5826,8 @@ fetch_callback(isc_task_t *task, isc_event_t *event) { UNUSED(task); - REQUIRE(event->ev_type == DNS_EVENT_FETCHDONE); + REQUIRE(event->ev_type == DNS_EVENT_FETCHDONE || + event->ev_type == DNS_EVENT_TRYSTALE); client = devent->ev_arg; REQUIRE(NS_CLIENT_VALID(client)); REQUIRE(task == client->task); @@ -5756,6 +5835,11 @@ fetch_callback(isc_task_t *task, isc_event_t *event) { CTRACE(ISC_LOG_DEBUG(3), "fetch_callback"); + if (event->ev_type == DNS_EVENT_TRYSTALE) { + query_lookup_staleonly(client, devent); + return; + } + LOCK(&client->query.fetchlock); if (client->query.fetch != NULL) { /* @@ -6076,6 +6160,10 @@ ns_query_recurse(ns_client_t *client, dns_rdatatype_t qtype, dns_name_t *qname, peeraddr = &client->peeraddr; } + if (dns_view_staleanswerenabled(client->view)) { + client->query.fetchoptions |= DNS_FETCHOPT_TRYSTALE_ONTIMEOUT; + } + isc_nmhandle_attach(client->handle, &client->fetchhandle); result = dns_resolver_createfetch( client->view->resolver, qname, qtype, qdomain, nameservers, @@ -7678,7 +7766,9 @@ query_addanswer(query_ctx_t *qctx) { query_filter64(qctx); ns_client_putrdataset(qctx->client, &qctx->rdataset); } else { - if (!qctx->is_zone && RECURSIONOK(qctx->client)) { + if (!qctx->is_zone && RECURSIONOK(qctx->client) && + !QUERY_STALEONLY(qctx->client)) + { query_prefetch(qctx->client, qctx->fname, qctx->rdataset); } @@ -7786,7 +7876,7 @@ query_respond(query_ctx_t *qctx) { * We shouldn't ever fail to add 'rdataset' * because it's already in the answer. */ - INSIST(qctx->rdataset == NULL); + INSIST(qctx->rdataset == NULL || QUERY_ANSWERED(qctx->client)); query_addauth(qctx); @@ -11271,7 +11361,7 @@ ns_query_done(query_ctx_t *qctx) { * If we're recursing then just return; the query will * resume when recursion ends. */ - if (RECURSING(qctx->client)) { + if (RECURSING(qctx->client) && !QUERY_STALEONLY(qctx->client)) { return (qctx->result); } @@ -11282,8 +11372,10 @@ ns_query_done(query_ctx_t *qctx) { * to the AA bit if the auth-nxdomain config option * says so, then render and send the response. */ - query_setup_sortlist(qctx); - query_glueanswer(qctx); + if (!QUERY_ANSWERED(qctx->client)) { + query_setup_sortlist(qctx); + query_glueanswer(qctx); + } if (qctx->client->message->rcode == dns_rcode_nxdomain && qctx->view->auth_nxdomain)