diff --git a/CHANGES b/CHANGES index cf0b4945be..987862f2d8 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,8 @@ +5566. [func] Add "stale-answer-client-timeout" option, which + is the amount of time a recursive resolver waits before + attempting to answer the query using stale data from cache. + [GL #2247] + 5565. [func] The SONAMEs for BIND 9 libraries now include the current BIND 9 version number, in an effort to tightly couple internal libraries with a specific release. [GL #2387] 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/named.conf.rst b/bin/named/named.conf.rst index 9fbe08b0ba..40163878a5 100644 --- a/bin/named/named.conf.rst +++ b/bin/named/named.conf.rst @@ -403,6 +403,7 @@ OPTIONS sig-validity-interval integer [ integer ]; sortlist { address_match_element; ... }; stacksize ( default | unlimited | sizeval ); + stale-answer-client-timeout ( disabled | off | integer ); stale-answer-enable boolean; stale-answer-ttl duration; stale-cache-enable boolean; @@ -806,6 +807,7 @@ VIEW sig-signing-type integer; sig-validity-interval integer [ integer ]; sortlist { address_match_element; ... }; + stale-answer-client-timeout ( disabled | off | integer ); stale-answer-enable boolean; stale-answer-ttl duration; stale-cache-enable boolean; diff --git a/bin/named/server.c b/bin/named/server.c index 839438161c..19a0693fc5 100644 --- a/bin/named/server.c +++ b/bin/named/server.c @@ -4485,6 +4485,23 @@ 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); + if (cfg_obj_isstring(obj)) { + /* + * The only string values available for this option + * are "disabled" and "off". + * We use (uint32_t) -1 to represent disabled since + * a value of zero means that stale data can be used + * to promptly answer the query, while an attempt to + * refresh the RRset will still be made in background. + */ + view->staleanswerclienttimeout = (uint32_t)-1; + } else { + view->staleanswerclienttimeout = cfg_obj_asuint32(obj); + } + obj = NULL; result = named_config_get(maps, "stale-refresh-time", &obj); INSIST(result == ISC_R_SUCCESS); @@ -4774,6 +4791,27 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config, query_timeout = cfg_obj_asuint32(obj); dns_resolver_settimeout(view->resolver, query_timeout); + /* + * Adjust stale-answer-client-timeout upper bound + * to be resolver-query-timeout - 1s. + * This assignment is safe as dns_resolver_settimeout() + * ensures that resolver->querytimeout value will be in the + * [MINIMUM_QUERY_TIMEOUT, MAXIMUM_QUERY_TIMEOUT] range and + * MINIMUM_QUERY_TIMEOUT is > 1000 (in ms). + */ + if (view->staleanswerclienttimeout != (uint32_t)-1 && + view->staleanswerclienttimeout > + (dns_resolver_gettimeout(view->resolver) - 1000)) + { + view->staleanswerclienttimeout = + dns_resolver_gettimeout(view->resolver) - 1000; + isc_log_write( + named_g_lctx, NAMED_LOGCATEGORY_GENERAL, + NAMED_LOGMODULE_SERVER, ISC_LOG_WARNING, + "stale-answer-client-timeout adjusted to %" PRIu32, + view->staleanswerclienttimeout); + } + /* Specify whether to use 0-TTL for negative response for SOA query */ dns_resolver_setzeronosoattl(view->resolver, zero_no_soattl); diff --git a/bin/tests/system/serve-stale/clean.sh b/bin/tests/system/serve-stale/clean.sh index 5f3c90f540..a089574d37 100644 --- a/bin/tests/system/serve-stale/clean.sh +++ b/bin/tests/system/serve-stale/clean.sh @@ -15,3 +15,4 @@ rm -f */named.run */named.memstats rm -f ns*/managed-keys.bind* rm -f ns*/named_dump* rm -f ns*/named.stats* +rm -f ns*/named.run.prev diff --git a/bin/tests/system/serve-stale/ns1/named1.conf.in b/bin/tests/system/serve-stale/ns1/named1.conf.in index 41347871cd..a8410a0e44 100644 --- a/bin/tests/system/serve-stale/ns1/named1.conf.in +++ b/bin/tests/system/serve-stale/ns1/named1.conf.in @@ -30,6 +30,7 @@ options { max-stale-ttl 3600; stale-answer-ttl 4; stale-answer-enable yes; + stale-answer-client-timeout disabled; stale-cache-enable yes; stale-refresh-time 30; servfail-ttl 0; diff --git a/bin/tests/system/serve-stale/ns1/named2.conf.in b/bin/tests/system/serve-stale/ns1/named2.conf.in index 06fae5369c..c8a8daeee1 100644 --- a/bin/tests/system/serve-stale/ns1/named2.conf.in +++ b/bin/tests/system/serve-stale/ns1/named2.conf.in @@ -30,6 +30,7 @@ options { max-stale-ttl 20; stale-answer-ttl 3; stale-answer-enable yes; + stale-answer-client-timeout disabled; stale-cache-enable yes; servfail-ttl 0; }; diff --git a/bin/tests/system/serve-stale/ns1/named3.conf.in b/bin/tests/system/serve-stale/ns1/named3.conf.in index f97dea958d..6fed86ae40 100644 --- a/bin/tests/system/serve-stale/ns1/named3.conf.in +++ b/bin/tests/system/serve-stale/ns1/named3.conf.in @@ -30,6 +30,7 @@ options { max-stale-ttl 20; stale-answer-ttl 3; stale-answer-enable yes; + stale-answer-client-timeout disabled; stale-cache-enable yes; stale-refresh-time 0; servfail-ttl 0; diff --git a/bin/tests/system/serve-stale/ns3/named2.conf.in b/bin/tests/system/serve-stale/ns3/named2.conf.in new file mode 100644 index 0000000000..e91f67b044 --- /dev/null +++ b/bin/tests/system/serve-stale/ns3/named2.conf.in @@ -0,0 +1,48 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + + +/* + * Test default stale-answer-client-timeout value + */ + +key rndc_key { + secret "1234abcd8765"; + algorithm hmac-sha256; +}; + +controls { + inet 10.53.0.3 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +options { + query-source address 10.53.0.3; + notify-source 10.53.0.3; + transfer-source 10.53.0.3; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.3; }; + listen-on-v6 { none; }; + dnssec-validation no; + recursion yes; + stale-answer-enable yes; + stale-cache-enable yes; + stale-answer-ttl 3; + stale-refresh-time 0; + max-stale-ttl 3600; + resolver-query-timeout 10; +}; + +zone "." { + type secondary; + primaries { 10.53.0.1; }; + file "root.bk"; +}; diff --git a/bin/tests/system/serve-stale/ns3/named3.conf.in b/bin/tests/system/serve-stale/ns3/named3.conf.in new file mode 100644 index 0000000000..0520e514c1 --- /dev/null +++ b/bin/tests/system/serve-stale/ns3/named3.conf.in @@ -0,0 +1,48 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + + +/* + * Test disable of stale-answer-client-timeout. + */ + +key rndc_key { + secret "1234abcd8765"; + algorithm hmac-sha256; +}; + +controls { + inet 10.53.0.3 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +options { + query-source address 10.53.0.3; + notify-source 10.53.0.3; + transfer-source 10.53.0.3; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.3; }; + listen-on-v6 { none; }; + dnssec-validation no; + recursion yes; + stale-answer-enable yes; + stale-cache-enable yes; + stale-answer-ttl 3; + stale-answer-client-timeout off; + stale-refresh-time 0; + max-stale-ttl 3600; + resolver-query-timeout 10; +}; + +zone "." { + type hint; + file "root.db"; +}; diff --git a/bin/tests/system/serve-stale/ns3/named4.conf.in b/bin/tests/system/serve-stale/ns3/named4.conf.in new file mode 100644 index 0000000000..5fd5ef82f4 --- /dev/null +++ b/bin/tests/system/serve-stale/ns3/named4.conf.in @@ -0,0 +1,48 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + + +/* + * Test stale-answer-client-timeout 0. + */ + +key rndc_key { + secret "1234abcd8765"; + algorithm hmac-sha256; +}; + +controls { + inet 10.53.0.3 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +options { + query-source address 10.53.0.3; + notify-source 10.53.0.3; + transfer-source 10.53.0.3; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.3; }; + listen-on-v6 { none; }; + dnssec-validation no; + recursion yes; + stale-answer-enable yes; + stale-cache-enable yes; + stale-answer-ttl 3; + stale-answer-client-timeout 0; + stale-refresh-time 0; + resolver-query-timeout 10; + max-stale-ttl 3600; +}; + +zone "." { + type hint; + file "root.db"; +}; diff --git a/bin/tests/system/serve-stale/ns3/named5.conf.in b/bin/tests/system/serve-stale/ns3/named5.conf.in new file mode 100644 index 0000000000..92b2cdf108 --- /dev/null +++ b/bin/tests/system/serve-stale/ns3/named5.conf.in @@ -0,0 +1,48 @@ +/* + * Copyright (C) Internet Systems Consortium, Inc. ("ISC") + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + + +/* + * Test stale-answer-client-timeout 0. + */ + +key rndc_key { + secret "1234abcd8765"; + algorithm hmac-sha256; +}; + +controls { + inet 10.53.0.3 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +options { + query-source address 10.53.0.3; + notify-source 10.53.0.3; + transfer-source 10.53.0.3; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.3; }; + listen-on-v6 { none; }; + dnssec-validation no; + recursion yes; + stale-answer-enable yes; + stale-cache-enable yes; + stale-answer-ttl 3; + stale-answer-client-timeout 0; + stale-refresh-time 4; + resolver-query-timeout 10; + max-stale-ttl 3600; +}; + +zone "." { + type hint; + file "root.db"; +}; diff --git a/bin/tests/system/serve-stale/ns3/root.db b/bin/tests/system/serve-stale/ns3/root.db new file mode 100644 index 0000000000..8f779b0878 --- /dev/null +++ b/bin/tests/system/serve-stale/ns3/root.db @@ -0,0 +1,11 @@ +; Copyright (C) Internet Systems Consortium, Inc. ("ISC") +; +; This Source Code Form is subject to the terms of the Mozilla Public +; License, v. 2.0. If a copy of the MPL was not distributed with this +; file, You can obtain one at http://mozilla.org/MPL/2.0/. +; +; See the COPYRIGHT file distributed with this work for additional +; information regarding copyright ownership. + +. 300 NS ns.nil. +ns.nil. 300 A 10.53.0.1 diff --git a/bin/tests/system/serve-stale/tests.sh b/bin/tests/system/serve-stale/tests.sh index 5ede9c51e1..92263d86df 100755 --- a/bin/tests/system/serve-stale/tests.sh +++ b/bin/tests/system/serve-stale/tests.sh @@ -19,7 +19,6 @@ stale_answer_ttl=$(sed -ne 's,^[[:space:]]*stale-answer-ttl \([[:digit:]]*\).*,\ status=0 n=0 - # # First test server with serve-stale options set. # @@ -188,7 +187,7 @@ if [ $ret != 0 ]; then echo_i "failed"; fi # 2. Disable responses from authoritative server. # 3. Sleep for TTL duration so rrset TTL expires (2 sec) # 4. Query data.example -# 5. Check if response come from stale rrset (3 sec TTL) +# 5. Check if response come from stale rrset (4 sec TTL) # 6. Enable responses from authoritative server. # 7. Query data.example # 8. Check if response come from stale rrset, since the query @@ -548,8 +547,8 @@ grep "ANSWER: 0," dig.out.test$n > /dev/null || ret=1 if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status+ret)) -# keep track of time so we can access these rrset later, -# when we expect them to become ancient. +# Keep track of time so we can access these RRset later, when we expect them +# to become ancient. t1=`$PERL -e 'print time()'` n=$((n+1)) @@ -644,16 +643,16 @@ grep "1 #NXDOMAIN" ns1/named.stats.$n.cachedb > /dev/null || ret=1 status=$((status+ret)) if [ $ret != 0 ]; then echo_i "failed"; fi -# retrieve max-stale-ttl value, +# Retrieve max-stale-ttl value. interval_to_ancient=`grep 'max-stale-ttl' ns1/named2.conf.in | awk '{ print $2 }' | tr -d ';'` -# we add 2 seconds to it since this is the ttl value of the records being tested. +# We add 2 seconds to it since this is the ttl value of the records being +# tested. interval_to_ancient=$((interval_to_ancient + 2)) t2=`$PERL -e 'print time()'` elapsed=$((t2 - t1)) -# if elapsed time so far is less than max-stale-ttl + 2 seconds, -# then we sleep enough to ensure that we'll ask for ancient rrsets -# in the next queries. +# If elapsed time so far is less than max-stale-ttl + 2 seconds, then we sleep +# enough to ensure that we'll ask for ancient RRsets in the next queries. if [ $elapsed -lt $interval_to_ancient ]; then sleep $((interval_to_ancient - elapsed)) fi @@ -885,7 +884,7 @@ $DIG -p ${PORT} @10.53.0.1 data.example TXT > dig.out.test$((n+1)) # Step 8. n=$((n+1)) -echo_i "check stale data.example comes from authoritative (stale-refresh-time disabled) ($n)" +echo_i "check data.example comes from authoritative (stale-refresh-time disabled) ($n)" ret=0 grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 @@ -1040,8 +1039,8 @@ $RNDCCMD 10.53.0.3 stats > /dev/null 2>&1 [ -f ns3/named.stats ] || ret=1 cp ns3/named.stats ns3/named.stats.$n # Check first 10 lines of Cache DB statistics. After last queries, we expect -# one active TXT RRset, one stale TXT, one stale nxrrset TXT, and one -# stale NXDOMAIN. +# one active TXT RRset, one stale TXT, one stale nxrrset TXT, and one stale +# NXDOMAIN. grep -A 10 "++ Cache DB RRsets ++" ns3/named.stats.$n > ns3/named.stats.$n.cachedb || ret=1 grep "1 TXT" ns3/named.stats.$n.cachedb > /dev/null || ret=1 grep "1 #TXT" ns3/named.stats.$n.cachedb > /dev/null || ret=1 @@ -1260,8 +1259,8 @@ $RNDCCMD 10.53.0.4 stats > /dev/null 2>&1 [ -f ns4/named.stats ] || ret=1 cp ns4/named.stats ns4/named.stats.$n # Check first 10 lines of Cache DB statistics. After last queries, we expect -# one active TXT RRset, one stale TXT, one stale nxrrset TXT, and one -# stale NXDOMAIN. +# one active TXT RRset, one stale TXT, one stale nxrrset TXT, and one stale +# NXDOMAIN. grep -A 10 "++ Cache DB RRsets ++" ns4/named.stats.$n > ns4/named.stats.$n.cachedb || ret=1 grep "1 TXT" ns4/named.stats.$n.cachedb > /dev/null || ret=1 grep "1 #TXT" ns4/named.stats.$n.cachedb > /dev/null || ret=1 @@ -1282,9 +1281,9 @@ status=$((status+ret)) echo_i "stop ns4" $PERL ../stop.pl --use-rndc --port ${CONTROLPORT} serve-stale ns4 -# Load the cache as if it was five minutes (RBTDB_VIRTUAL) older. -# Since max-stale-ttl defaults to a week, we need to adjust the date by -# one week and five minutes. +# Load the cache as if it was five minutes (RBTDB_VIRTUAL) older. Since +# max-stale-ttl defaults to a week, we need to adjust the date by one week and +# five minutes. LASTWEEK=`TZ=UTC perl -e 'my $now = time(); my $oneWeekAgo = $now - 604800; my $fiveMinutesAgo = $oneWeekAgo - 300; @@ -1553,5 +1552,463 @@ grep -F "#NXDOMAIN" ns5/named.stats.$n.cachedb > /dev/null && ret=1 status=$((status+ret)) if [ $ret != 0 ]; then echo_i "failed"; fi +######################################################## +# Test for stale-answer-client-timeout (default 1.8s). # +######################################################## +echo_i "test stale-answer-client-timeout (default 1.8)" + +n=$((n+1)) +echo_i "updating ns3/named.conf ($n)" +ret=0 +copy_setports ns3/named2.conf.in ns3/named.conf +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +echo_i "restart ns3" +$PERL ../stop.pl --use-rndc --port ${CONTROLPORT} serve-stale ns3 +start_server --noclean --restart --port ${PORT} serve-stale ns3 + +n=$((n+1)) +echo_i "check 'rndc serve-stale status' ($n)" +ret=0 +$RNDCCMD 10.53.0.3 serve-stale status > rndc.out.test$n 2>&1 || ret=1 +grep '_default: on (stale-answer-ttl=3 max-stale-ttl=3600 stale-refresh-time=0)' rndc.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +n=$((n+1)) +echo_i "enable responses from authoritative server ($n)" +ret=0 +$DIG -p ${PORT} @10.53.0.2 txt enable > dig.out.test$n +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +grep "TXT.\"1\"" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +n=$((n+1)) +echo_i "prime cache data.example (stale-answer-client-timeout)" +ret=0 +$DIG -p ${PORT} @10.53.0.3 data.example TXT > dig.out.test$n +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +n=$((n+1)) +echo_i "prime cache nodata.example (stale-answer-client-timeout)" +ret=0 +$DIG -p ${PORT} @10.53.0.3 nodata.example TXT > dig.out.test$n +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "ANSWER: 0," dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +n=$((n+1)) +echo_i "disable responses from authoritative server ($n)" +ret=0 +$DIG -p ${PORT} @10.53.0.2 txt disable > dig.out.test$n +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +grep "TXT.\"0\"" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +# Allow RRset to become stale. +sleep 2 + +# We configured a long value of 30 seconds for resolver-query-timeout. +# That should give us enough time to receive an stale answer from cache +# after stale-answer-client-timeout timer of 1.8 sec triggers. +n=$((n+1)) +echo_i "check stale data.example comes from cache (default stale-answer-client-timeout) ($n)" +nextpart ns3/named.run > /dev/null +t1=`$PERL -e 'print time()'` +$DIG -p ${PORT} +tries=1 +timeout=10 @10.53.0.3 data.example TXT > dig.out.test$n +t2=`$PERL -e 'print time()'` +wait_for_log 5 "data.example client timeout, stale answer used" ns3/named.run || ret=1 +ret=0 +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +grep "data\.example\..*3.*IN.*TXT.*A text record with a 2 second ttl" dig.out.test$n > /dev/null || ret=1 +# Default stale-answer-client-timeout is 1.8s, we allow some extra time +# just in case other tests are taking too much cpu. +[ $((t2 - t1)) -le 10 ] || { echo_i "query took $((t2 - t1))s to resolve."; ret=1; } +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +echo_i "sending queries for tests $((n+1))-$((n+2))..." +$DIG -p ${PORT} +tries=1 +timeout=3 @10.53.0.3 nodata.example TXT > dig.out.test$((n+1)) & +$DIG -p ${PORT} +tries=1 +timeout=30 @10.53.0.3 nodata.example TXT > dig.out.test$((n+2)) +wait + +# Since nodata.example is cached as NXRRSET and marked as stale at this point, +# BIND must not return this RRset when stale-answer-client-timeout triggers, +# instead, it must attempt to refresh the RRset. Since the authoritative +# server is disabled and we are using resolver-query-timeout value of 10 +# seconds, we expect this query with a timeout of 3 seconds to time out. +n=$((n+1)) +echo_i "check query for nodata.example times out (default stale-answer-client-timeout) ($n)" +grep "connection timed out" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +# For this query we expect BIND to return stale NXRRSET data for +# nodata.example after resolver-query-timeout expires. +n=$((n+1)) +echo_i "check stale nodata.example comes from cache after resolver-query-timeout expires (default stale-answer-client-timeout) ($n)" +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "ANSWER: 0," dig.out.test$n > /dev/null || ret=1 +grep "example\..*3.*IN.*SOA" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +############################################# +# Test for stale-answer-client-timeout off. # +############################################# +echo_i "test stale-answer-client-timeout (off)" + +n=$((n+1)) +echo_i "updating ns3/named.conf ($n)" +ret=0 +copy_setports ns3/named3.conf.in ns3/named.conf +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +n=$((n+1)) +echo_i "running 'rndc reload' ($n)" +ret=0 +rndc_reload ns3 10.53.0.3 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +# Send a query, auth server is disabled, we will enable it after a while in +# order to receive an answer before resolver-query-timeout expires. Since +# stale-answer-client-timeout is disabled we must receive an answer from +# authoritative server. +echo_i "sending query for test $((n+2))" +$DIG -p ${PORT} @10.53.0.3 data.example TXT > dig.out.test$((n+2)) & +sleep 3 + +n=$((n+1)) +echo_i "enable responses from authoritative server ($n)" +ret=0 +$DIG -p ${PORT} @10.53.0.2 txt enable > dig.out.test$n +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +grep "TXT.\"1\"" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +# Wait until dig is done. +wait + +n=$((n+1)) +echo_i "check data.example comes from authoritative server (stale-answer-client-timeout off) ($n)" +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +grep "data\.example\..*[12].*IN.*TXT.*A text record with a 2 second ttl" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +############################################# +# Test for stale-answer-client-timeout 0. # +############################################# +echo_i "test stale-answer-client-timeout (0)" + +n=$((n+1)) +echo_i "updating ns3/named.conf ($n)" +ret=0 +copy_setports ns3/named4.conf.in ns3/named.conf +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +echo_i "restart ns3" +$PERL ../stop.pl --use-rndc --port ${CONTROLPORT} serve-stale ns3 +start_server --noclean --restart --port ${PORT} serve-stale ns3 + +n=$((n+1)) +echo_i "prime cache data.example (stale-answer-client-timeout 0)" +ret=0 +$DIG -p ${PORT} @10.53.0.3 data.example TXT > dig.out.test$n +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +n=$((n+1)) +echo_i "prime cache nodata.example (stale-answer-client-timeout 0)" +ret=0 +$DIG -p ${PORT} @10.53.0.3 nodata.example TXT > dig.out.test$n +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "ANSWER: 0," dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +n=$((n+1)) +echo_i "disable responses from authoritative server ($n)" +ret=0 +$DIG -p ${PORT} @10.53.0.2 txt disable > dig.out.test$n +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +grep "TXT.\"0\"" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +# Allow RRset to become stale. +sleep 2 + +n=$((n+1)) +ret=0 +echo_i "check stale data.example comes from cache (stale-answer-client-timeout 0) ($n)" +nextpart ns3/named.run > /dev/null +$DIG -p ${PORT} @10.53.0.3 data.example TXT > dig.out.test$n +wait_for_log 5 "data.example stale answer used, an attempt to refresh the RRset" ns3/named.run || ret=1 +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +grep "data\.example\..*3.*IN.*TXT.*A text record with a 2 second ttl" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +n=$((n+1)) +echo_i "enable responses from authoritative server ($n)" +ret=0 +$DIG -p ${PORT} @10.53.0.2 txt enable > dig.out.test$n +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +grep "TXT.\"1\"" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +wait_for_rrset_refresh() { + nextpart ns3/named.run | grep 'data.example.*2.*TXT.*"A text record with a 2 second ttl"' > /dev/null && return 0 + return 1 +} + +# This test ensures that after we get stale data due to +# stale-answer-client-timeout 0, enabling the authoritative server will allow +# the RRset to be updated. +n=$((n+1)) +ret=0 +echo_i "check stale data.example was refreshed (stale-answer-client-timeout 0) ($n)" +retry_quiet 10 wait_for_rrset_refresh || ret=1 +$DIG -p ${PORT} @10.53.0.3 data.example TXT > dig.out.test$n +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +grep "data\.example\..*[12].*IN.*TXT.*A text record with a 2 second ttl" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +n=$((n+1)) +echo_i "disable responses from authoritative server ($n)" +ret=0 +$DIG -p ${PORT} @10.53.0.2 txt disable > dig.out.test$n +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +grep "TXT.\"0\"" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +echo_i "sending queries for tests $((n+1))-$((n+2))..." +$DIG -p ${PORT} +tries=1 +timeout=3 @10.53.0.3 nodata.example TXT > dig.out.test$((n+1)) & +$DIG -p ${PORT} +tries=1 +timeout=30 @10.53.0.3 nodata.example TXT > dig.out.test$((n+2)) +wait + +# Since nodata.example is cached as NXRRSET and marked as stale at this point, +# BIND must not prompty return this RRset due to +# stale-answer-client-timeout == 0, instead, it must attempt to refresh the +# RRset. Since the authoritative server is disabled and we are using +# resolver-query-timeout value of 10 seconds, we expect this query with a +# timeout of 3 seconds to time out. +n=$((n+1)) +echo_i "check query for nodata.example times out (stale-answer-client-timeout 0) ($n)" +grep "connection timed out" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +# For this query we expect BIND to return stale NXRRSET data for +# nodata.example after resolver-query-timeout expires. +n=$((n+1)) +echo_i "check stale nodata.example comes from cache after resolver-query-timeout expires (stale-answer-client-timeout 0) ($n)" +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "ANSWER: 0," dig.out.test$n > /dev/null || ret=1 +grep "example\..*3.*IN.*SOA" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +#################################################################### +# Test for stale-answer-client-timeout 0 and stale-refresh-time 4. # +#################################################################### +echo_i "test stale-answer-client-timeout (0) and stale-refresh-time (4)" + +n=$((n+1)) +echo_i "updating ns3/named.conf ($n)" +ret=0 +copy_setports ns3/named5.conf.in ns3/named.conf +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +n=$((n+1)) +echo_i "running 'rndc reload' ($n)" +ret=0 +rndc_reload ns3 10.53.0.3 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +n=$((n+1)) +echo_i "flush cache, enable responses from authoritative server ($n)" +ret=0 +$RNDCCMD 10.53.0.3 flushtree example > rndc.out.test$n.1 2>&1 || ret=1 +$DIG -p ${PORT} @10.53.0.2 txt enable > dig.out.test$n +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +grep "TXT.\"1\"" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +n=$((n+1)) +echo_i "prime cache data.example (stale-answer-client-timeout 0, stale-refresh-time 4) ($n)" +ret=0 +$DIG -p ${PORT} @10.53.0.3 data.example TXT > dig.out.test$n +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +grep "data\.example\..*2.*IN.*TXT.*A text record with a 2 second ttl" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +# Allow RRset to become stale. +sleep 2 + +n=$((n+1)) +echo_i "disable responses from authoritative server ($n)" +ret=0 +$DIG -p ${PORT} @10.53.0.2 txt disable > dig.out.test$n +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +grep "TXT.\"0\"" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +n=$((n+1)) +ret=0 +echo_i "check stale data.example comes from cache (stale-answer-client-timeout 0 stale-refresh-time 4) ($n)" +nextpart ns3/named.run > /dev/null +$DIG -p ${PORT} @10.53.0.3 data.example TXT > dig.out.test$n +wait_for_log 5 "data.example stale answer used, an attempt to refresh the RRset" ns3/named.run || ret=1 +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +grep "data\.example\..*3.*IN.*TXT.*A text record with a 2 second ttl" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +n=$((n+1)) +echo_i "enable responses from authoritative server ($n)" +ret=0 +$DIG -p ${PORT} @10.53.0.2 txt enable > dig.out.test$n +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +grep "TXT.\"1\"" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +# This test ensures that after we get stale data due to +# stale-answer-client-timeout 0, enabling the authoritative server will allow +# the RRset to be updated. +n=$((n+1)) +ret=0 +echo_i "check stale data.example was refreshed (stale-answer-client-timeout 0 stale-refresh-time 4) ($n)" +retry_quiet 10 wait_for_rrset_refresh || ret=1 +$DIG -p ${PORT} @10.53.0.3 data.example TXT > dig.out.test$n +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +grep "data\.example\..*[12].*IN.*TXT.*A text record with a 2 second ttl" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +# Allow RRset to become stale. +sleep 2 + +n=$((n+1)) +echo_i "disable responses from authoritative server ($n)" +ret=0 +$DIG -p ${PORT} @10.53.0.2 txt disable > dig.out.test$n +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +grep "TXT.\"0\"" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +n=$((n+1)) +ret=0 +echo_i "check stale data.example comes from cache (stale-answer-client-timeout 0 stale-refresh-time 4) ($n)" +nextpart ns3/named.run > /dev/null +$DIG -p ${PORT} @10.53.0.3 data.example TXT > dig.out.test$n +wait_for_log 5 "data.example stale answer used, an attempt to refresh the RRset" ns3/named.run || ret=1 +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +grep "data\.example\..*3.*IN.*TXT.*A text record with a 2 second ttl" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +# Allow stale-refresh-time to be activated. +n=$((n+1)) +ret=0 +echo_i "wait until resolver query times out, activating stale-refresh-time" +wait_for_log 15 "data.example resolver failure, stale answer used" ns3/named.run || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +n=$((n+1)) +ret=0 +echo_i "check stale data.example comes from cache within stale-refresh-time (stale-answer-client-timeout 0 stale-refresh-time 4) ($n)" +nextpart ns3/named.run > /dev/null +$DIG -p ${PORT} @10.53.0.3 data.example TXT > dig.out.test$n +wait_for_log 5 "data.example query within stale refresh time" ns3/named.run || ret=1 +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +grep "data\.example\..*3.*IN.*TXT.*A text record with a 2 second ttl" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +n=$((n+1)) +echo_i "enable responses from authoritative server ($n)" +ret=0 +$DIG -p ${PORT} @10.53.0.2 txt enable > dig.out.test$n +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +grep "TXT.\"1\"" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +# We give BIND some time to ensure that after we enable authoritative server, +# this RRset is still not refreshed because it was hit during +# stale-refresh-time window. +sleep 1 + +n=$((n+1)) +ret=0 +echo_i "check stale data.example was not refreshed (stale-answer-client-timeout 0 stale-refresh-time 4) ($n)" +nextpart ns3/named.run > /dev/null +$DIG -p ${PORT} @10.53.0.3 data.example TXT > dig.out.test$n +wait_for_log 5 "data.example query within stale refresh time" ns3/named.run || ret=1 +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +grep "data\.example\..*3.*IN.*TXT.*A text record with a 2 second ttl" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +# After the refresh-time-window, the RRset will be refreshed. +sleep 4 + +n=$((n+1)) +ret=0 +echo_i "check stale data.example comes from cache (stale-answer-client-timeout 0 stale-refresh-time 4) ($n)" +$DIG -p ${PORT} @10.53.0.3 data.example TXT > dig.out.test$n +wait_for_log 5 "data.example stale answer used, an attempt to refresh the RRset" ns3/named.run || ret=1 +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +grep "data\.example\..*3.*IN.*TXT.*A text record with a 2 second ttl" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + +n=$((n+1)) +ret=0 +echo_i "check stale data.example was refreshed (stale-answer-client-timeout 0 stale-refresh-time 4) ($n)" +$DIG -p ${PORT} @10.53.0.3 data.example TXT > dig.out.test$n +grep "status: NOERROR" dig.out.test$n > /dev/null || ret=1 +grep "ANSWER: 1," dig.out.test$n > /dev/null || ret=1 +grep "data\.example\..*[12].*IN.*TXT.*A text record with a 2 second ttl" dig.out.test$n > /dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status+ret)) + echo_i "exit status: $status" [ $status -eq 0 ] || exit 1 diff --git a/doc/arm/reference.rst b/doc/arm/reference.rst index 96416d320e..eb9140b3dc 100644 --- a/doc/arm/reference.rst +++ b/doc/arm/reference.rst @@ -1832,6 +1832,20 @@ Boolean Options Information about stale answers is logged under the ``serve-stale`` log category. +``stale-answer-client-timeout`` + This option defines the amount of time ``named`` waits before attempting to + answer the query with a stale RRset from cache. If a stale answer is found, + ``named`` continues the ongoing fetches, attempting to refresh the RRset in + cache until the ``resolver-query-timeout`` interval is reached. + + The default value is ``1800`` (in milliseconds) and the maximum value is + bounded to ``resolver-query-timeout`` minus one second. A value of ``0`` + immediately returns a cached RRset if available, and still attempts a refresh + of the data in cache. + + The option can be disabled by setting the value to ``off`` or ``disabled``. + It also has no effect if ``stale-answer-enable`` is disabled. + ``stale-cache-enable`` If ``yes``, enable the retaining of "stale" cached answers. Default ``no``. diff --git a/doc/man/named.conf.5in b/doc/man/named.conf.5in index 9798295f4a..2180bdbd6c 100644 --- a/doc/man/named.conf.5in +++ b/doc/man/named.conf.5in @@ -466,6 +466,7 @@ options { sig\-validity\-interval integer [ integer ]; sortlist { address_match_element; ... }; stacksize ( default | unlimited | sizeval ); + stale\-answer\-client\-timeout ( disabled | off | integer ); stale\-answer\-enable boolean; stale\-answer\-ttl duration; stale\-cache\-enable boolean; @@ -901,6 +902,7 @@ view string [ class ] { sig\-signing\-type integer; sig\-validity\-interval integer [ integer ]; sortlist { address_match_element; ... }; + stale\-answer\-client\-timeout ( disabled | off | integer ); stale\-answer\-enable boolean; stale\-answer\-ttl duration; stale\-cache\-enable boolean; diff --git a/doc/misc/options b/doc/misc/options index 33874c2d0c..3444f6eaf8 100644 --- a/doc/misc/options +++ b/doc/misc/options @@ -333,6 +333,7 @@ options { sig-validity-interval [ ]; sortlist { ; ... }; stacksize ( default | unlimited | ); + stale-answer-client-timeout ( disabled | off | ); stale-answer-enable ; stale-answer-ttl ; stale-cache-enable ; @@ -696,6 +697,7 @@ view [ ] { sig-signing-type ; sig-validity-interval [ ]; sortlist { ; ... }; + stale-answer-client-timeout ( disabled | off | ); stale-answer-enable ; stale-answer-ttl ; stale-cache-enable ; diff --git a/doc/misc/options.active b/doc/misc/options.active index 6d158267bf..deeda67131 100644 --- a/doc/misc/options.active +++ b/doc/misc/options.active @@ -331,6 +331,7 @@ options { sig-validity-interval [ ]; sortlist { ; ... }; stacksize ( default | unlimited | ); + stale-answer-client-timeout ( disabled | off | ); stale-answer-enable ; stale-answer-ttl ; stale-cache-enable ; @@ -692,6 +693,7 @@ view [ ] { sig-signing-type ; sig-validity-interval [ ]; sortlist { ; ... }; + stale-answer-client-timeout ( disabled | off | ); stale-answer-enable ; stale-answer-ttl ; stale-cache-enable ; diff --git a/doc/misc/options.grammar.rst b/doc/misc/options.grammar.rst index 867256d25a..4fdc098aae 100644 --- a/doc/misc/options.grammar.rst +++ b/doc/misc/options.grammar.rst @@ -259,6 +259,7 @@ sig-validity-interval [ ]; sortlist { ; ... }; stacksize ( default | unlimited | ); + stale-answer-client-timeout ( disabled | off | ); stale-answer-enable ; stale-answer-ttl ; stale-cache-enable ; diff --git a/doc/notes/notes-current.rst b/doc/notes/notes-current.rst index 15067ac56d..9044417d92 100644 --- a/doc/notes/notes-current.rst +++ b/doc/notes/notes-current.rst @@ -26,6 +26,23 @@ New Features - None. +- A new option, ```stale-answer-client-timeout``, has been added to + improve ``named``'s behavior with respect to serving stale data. The option + defines the amount of time ``named`` waits before attempting + to answer the query with a stale RRset from cache. If a stale answer + is found, ``named`` continues the ongoing fetches, attempting to + refresh the RRset in cache until the ``resolver-query-timeout`` interval is + reached. + + The default value is ``1800`` (in milliseconds) and the maximum value is + bounded to ``resolver-query-timeout`` minus one second. A value of + ``0`` immediately returns a cached RRset if available, and still + attempts a refresh of the data in cache. + + The option can be disabled by setting the value to ``off`` or + ``disabled``. It also has no effect if ``stale-answer-enable`` is + disabled. + Removed Features ~~~~~~~~~~~~~~~~ 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/rdataset.h b/lib/dns/include/dns/rdataset.h index 72d1445649..9f493e191c 100644 --- a/lib/dns/include/dns/rdataset.h +++ b/lib/dns/include/dns/rdataset.h @@ -189,6 +189,7 @@ struct dns_rdataset { #define DNS_RDATASETATTR_CYCLIC 0x00800000 /*%< Cyclic ordering. */ #define DNS_RDATASETATTR_STALE 0x01000000 #define DNS_RDATASETATTR_ANCIENT 0x02000000 +#define DNS_RDATASETATTR_STALE_WINDOW 0x04000000 /*% * _OMITDNSSEC: 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 ff72c8b758..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; @@ -1343,6 +1344,15 @@ dns_view_setviewrevert(dns_view_t *view); *\li 'view' to be valid. */ +bool +dns_view_staleanswerenabled(dns_view_t *view); +/*%< + * Check if stale answers are enabled for this view. + * + * Requires: + *\li 'view' to be valid. + */ + ISC_LANG_ENDDECLS #endif /* DNS_VIEW_H */ diff --git a/lib/dns/rbtdb.c b/lib/dns/rbtdb.c index b9a46cea40..13be34268f 100644 --- a/lib/dns/rbtdb.c +++ b/lib/dns/rbtdb.c @@ -273,7 +273,8 @@ typedef ISC_LIST(dns_rbtnode_t) rbtnodelist_t; #define RDATASET_ATTR_ZEROTTL 0x0800 #define RDATASET_ATTR_CASEFULLYLOWER 0x1000 /*%< Ancient - awaiting cleanup. */ -#define RDATASET_ATTR_ANCIENT 0x2000 +#define RDATASET_ATTR_ANCIENT 0x2000 +#define RDATASET_ATTR_STALE_WINDOW 0x4000 /* * XXX @@ -303,6 +304,9 @@ typedef ISC_LIST(dns_rbtnode_t) rbtnodelist_t; #define STALE(header) \ ((atomic_load_acquire(&(header)->attributes) & RDATASET_ATTR_STALE) != \ 0) +#define STALE_WINDOW(header) \ + ((atomic_load_acquire(&(header)->attributes) & \ + RDATASET_ATTR_STALE_WINDOW) != 0) #define RESIGN(header) \ ((atomic_load_acquire(&(header)->attributes) & \ RDATASET_ATTR_RESIGN) != 0) @@ -3148,6 +3152,9 @@ bind_rdataset(dns_rbtdb_t *rbtdb, dns_rbtnode_t *node, rdatasetheader_t *header, rdataset->attributes |= DNS_RDATASETATTR_PREFETCH; } if (STALE(header)) { + if (STALE_WINDOW(header)) { + rdataset->attributes |= DNS_RDATASETATTR_STALE_WINDOW; + } rdataset->attributes |= DNS_RDATASETATTR_STALE; rdataset->stale_ttl = (rbtdb->serve_stale_ttl + header->rdh_ttl) - now; @@ -4551,6 +4558,8 @@ check_stale_header(dns_rbtnode_t *node, rdatasetheader_t *header, * skip this record. We skip the records with ZEROTTL * (these records should not be cached anyway). */ + + RDATASET_ATTR_CLR(header, RDATASET_ATTR_STALE_WINDOW); if (!ZEROTTL(header) && KEEPSTALE(search->rbtdb) && stale > search->now) { mark_header_stale(search->rbtdb, header); @@ -4574,6 +4583,15 @@ check_stale_header(dns_rbtnode_t *node, rdatasetheader_t *header, * then don't skip this stale entry but use it * instead. */ + RDATASET_ATTR_SET(header, + RDATASET_ATTR_STALE_WINDOW); + return (false); + } else if ((search->options & DNS_DBFIND_STALEONLY) != + 0) { + /* + * We want stale RRset only, so we don't skip + * it. + */ return (false); } return ((search->options & DNS_DBFIND_STALEOK) == 0); diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c index 447d85062b..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]; @@ -4850,9 +4982,11 @@ fctx_create(dns_resolver_t *res, const dns_name_t *name, dns_rdatatype_t type, isc_result_t iresult; isc_interval_t interval; unsigned int findoptions = 0; - char buf[DNS_NAME_FORMATSIZE + DNS_RDATATYPE_FORMATSIZE]; + char buf[DNS_NAME_FORMATSIZE + DNS_RDATATYPE_FORMATSIZE + 1]; 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'. @@ -4879,8 +5013,8 @@ fctx_create(dns_resolver_t *res, const dns_name_t *name, dns_rdatatype_t type, */ dns_name_format(name, buf, sizeof(buf)); dns_rdatatype_format(type, typebuf, sizeof(typebuf)); - strlcat(buf, "/", sizeof(buf)); - strlcat(buf, typebuf, sizeof(buf)); + p = strlcat(buf, "/", sizeof(buf)); + strlcat(buf + p, typebuf, sizeof(buf)); fctx->info = isc_mem_strdup(mctx, buf); FCTXTRACE("create"); @@ -5077,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 @@ -5085,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); @@ -5099,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. */ @@ -5143,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); @@ -5356,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; @@ -6125,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); @@ -10814,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/dns/view.c b/lib/dns/view.c index 42c17dc6da..3572cec04a 100644 --- a/lib/dns/view.c +++ b/lib/dns/view.c @@ -2518,3 +2518,25 @@ dns_view_setviewrevert(dns_view_t *view) { dns_zt_setviewrevert(zonetable); } } + +bool +dns_view_staleanswerenabled(dns_view_t *view) { + uint32_t stale_ttl = 0; + bool result = false; + + REQUIRE(DNS_VIEW_VALID(view)); + + if (dns_db_getservestalettl(view->cachedb, &stale_ttl) != ISC_R_SUCCESS) + { + return (false); + } + if (stale_ttl > 0) { + if (view->staleanswersok == dns_stale_answer_yes) { + result = true; + } else if (view->staleanswersok == dns_stale_answer_conf) { + result = view->staleanswersenable; + } + } + + return (result); +} diff --git a/lib/dns/win32/libdns.def.in b/lib/dns/win32/libdns.def.in index c9b4e1fe80..917484d1ec 100644 --- a/lib/dns/win32/libdns.def.in +++ b/lib/dns/win32/libdns.def.in @@ -1147,6 +1147,7 @@ dns_view_setrootdelonly dns_view_setviewcommit dns_view_setviewrevert dns_view_simplefind +dns_view_staleanswerenabled dns_view_thaw dns_view_untrust dns_view_weakattach diff --git a/lib/isccfg/namedconf.c b/lib/isccfg/namedconf.c index b7ed28d24e..1ca22dd8d2 100644 --- a/lib/isccfg/namedconf.c +++ b/lib/isccfg/namedconf.c @@ -1899,6 +1899,28 @@ static cfg_type_t cfg_type_dns64 = { "dns64", cfg_parse_netprefix_map, cfg_print_map, cfg_doc_map, &cfg_rep_map, dns64_clausesets }; +static const char *staleanswerclienttimeout_enums[] = { "disabled", "off", + NULL }; +static isc_result_t +parse_staleanswerclienttimeout(cfg_parser_t *pctx, const cfg_type_t *type, + cfg_obj_t **ret) { + return (cfg_parse_enum_or_other(pctx, type, &cfg_type_uint32, ret)); +} + +static void +doc_staleanswerclienttimeout(cfg_printer_t *pctx, const cfg_type_t *type) { + cfg_doc_enum_or_other(pctx, type, &cfg_type_uint32); +} + +static cfg_type_t cfg_type_staleanswerclienttimeout = { + "staleanswerclienttimeout", + parse_staleanswerclienttimeout, + cfg_print_ustring, + doc_staleanswerclienttimeout, + &cfg_rep_string, + staleanswerclienttimeout_enums +}; + /*% * Clauses that can be found within the 'view' statement, * with defaults in the 'options' statement. @@ -2027,6 +2049,8 @@ 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_staleanswerclienttimeout, + 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..edd59197f2 100644 --- a/lib/ns/client.c +++ b/lib/ns/client.c @@ -421,6 +421,10 @@ ns_client_send(ns_client_t *client) { REQUIRE(NS_CLIENT_VALID(client)); + if ((client->query.attributes & NS_QUERYATTR_ANSWERED) != 0) { + return; + } + /* * XXXWPK TODO * Delay the response according to the -T delay option @@ -670,6 +674,8 @@ renderend: ns_statscounter_truncatedresp); } + client->query.attributes |= NS_QUERYATTR_ANSWERED; + return; cleanup: @@ -2326,6 +2332,7 @@ ns__client_setup(ns_client_t *client, ns_clientmgr_t *mgr, bool new) { .query = query }; } + client->query.attributes &= ~NS_QUERYATTR_ANSWERED; client->state = NS_CLIENTSTATE_INACTIVE; client->udpsize = 512; client->ednsversion = -1; diff --git a/lib/ns/include/ns/query.h b/lib/ns/include/ns/query.h index 011a8b4c13..74a95c9b94 100644 --- a/lib/ns/include/ns/query.h +++ b/lib/ns/include/ns/query.h @@ -117,6 +117,8 @@ struct ns_query { #define NS_QUERYATTR_DNS64EXCLUDE 0x08000 #define NS_QUERYATTR_RRL_CHECKED 0x10000 #define NS_QUERYATTR_REDIRECT 0x20000 +#define NS_QUERYATTR_ANSWERED 0x40000 +#define NS_QUERYATTR_STALEOK 0x80000 typedef struct query_ctx query_ctx_t; diff --git a/lib/ns/query.c b/lib/ns/query.c index ae0a7bef4b..f413ddee19 100644 --- a/lib/ns/query.c +++ b/lib/ns/query.c @@ -86,11 +86,11 @@ */ #define MAX_RESTARTS 16 -#define QUERY_ERROR(qctx, r) \ - do { \ - qctx->result = r; \ - qctx->want_restart = false; \ - qctx->line = __LINE__; \ +#define QUERY_ERROR(qctx, r) \ + do { \ + (qctx)->result = r; \ + (qctx)->want_restart = false; \ + (qctx)->line = __LINE__; \ } while (0) /*% Partial answer? */ @@ -133,12 +133,21 @@ #define REDIRECT(c) (((c)->query.attributes & NS_QUERYATTR_REDIRECT) != 0) +/*% Was the query already answered due to stale-answer-client-timeout? */ +#define QUERY_ANSWERED(q) (((q)->attributes & NS_QUERYATTR_ANSWERED) != 0) + +/*% Does the query only wants to check for stale RRset? */ +#define QUERY_STALEONLY(q) (((q)->dboptions & DNS_DBFIND_STALEONLY) != 0) + /*% Does the rdataset 'r' have an attached 'No QNAME Proof'? */ #define NOQNAME(r) (((r)->attributes & DNS_RDATASETATTR_NOQNAME) != 0) /*% Does the rdataset 'r' contain a stale answer? */ #define STALE(r) (((r)->attributes & DNS_RDATASETATTR_STALE) != 0) +/*% Does the rdataset 'r' is stale and within stale-refresh-time? */ +#define STALE_WINDOW(r) (((r)->attributes & DNS_RDATASETATTR_STALE_WINDOW) != 0) + #ifdef WANT_QUERYTRACE static inline void client_trace(ns_client_t *client, int level, const char *message) { @@ -173,10 +182,11 @@ client_trace(ns_client_t *client, int level, const char *message) { #define CCTRACE(l, m) ((void)m) #endif /* WANT_QUERYTRACE */ -#define DNS_GETDB_NOEXACT 0x01U -#define DNS_GETDB_NOLOG 0x02U -#define DNS_GETDB_PARTIAL 0x04U -#define DNS_GETDB_IGNOREACL 0x08U +#define DNS_GETDB_NOEXACT 0x01U +#define DNS_GETDB_NOLOG 0x02U +#define DNS_GETDB_PARTIAL 0x04U +#define DNS_GETDB_IGNOREACL 0x08U +#define DNS_GETDB_STALEFIRST 0X0CU #define PENDINGOK(x) (((x)&DNS_DBFIND_PENDINGOK) != 0) @@ -558,7 +568,10 @@ query_send(ns_client_t *client) { inc_stats(client, counter); ns_client_send(client); - isc_nmhandle_detach(&client->reqhandle); + + if (!QUERY_STALEONLY(&client->query)) { + isc_nmhandle_detach(&client->reqhandle); + } } static void @@ -585,7 +598,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->query)) { + isc_nmhandle_detach(&client->reqhandle); + } } static void @@ -598,7 +614,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->query)) { + isc_nmhandle_detach(&client->reqhandle); + } } static inline void @@ -5106,6 +5125,26 @@ qctx_init(ns_client_t *client, dns_fetchevent_t **eventp, dns_rdatatype_t qtype, CALL_HOOK_NORETURN(NS_QUERY_QCTX_INITIALIZED, qctx); } +/* + * Make 'dst' and exact copy of 'src', with exception of the + * option field, which is reset to zero. + * This function also attaches dst's view and db to the src's + * view and cachedb. + */ +static void +qctx_copy(const query_ctx_t *qctx, query_ctx_t *dst) { + REQUIRE(qctx != NULL); + REQUIRE(dst != NULL); + + memmove(dst, qctx, sizeof(*dst)); + dst->view = NULL; + dst->db = NULL; + dst->options = 0; + dns_view_attach(qctx->view, &dst->view); + dns_db_attach(qctx->view->cachedb, &dst->db); + CCTRACE(ISC_LOG_DEBUG(3), "qctx_copy"); +} + /*% * Clean up and disassociate the rdataset and node pointers in qctx. */ @@ -5158,7 +5197,7 @@ qctx_freedata(query_ctx_t *qctx) { dns_db_detach(&qctx->zdb); } - if (qctx->event != NULL) { + if (qctx->event != NULL && !QUERY_STALEONLY(&qctx->client->query)) { free_devent(qctx->client, ISC_EVENT_PTR(&qctx->event), &qctx->event); } @@ -5562,12 +5601,138 @@ ns__query_start(query_ctx_t *qctx) { } } - return (query_lookup(qctx)); + if (!qctx->is_zone && (qctx->view->staleanswerclienttimeout == 0) && + dns_view_staleanswerenabled(qctx->view)) + { + /* + * If stale answers are enabled and + * stale-answer-client-timeout is zero, then we can promptly + * answer with a stale RRset if one is available in cache. + */ + qctx->options |= DNS_GETDB_STALEFIRST; + } + + result = query_lookup(qctx); + + /* + * Clear "look-also-for-stale-data" flag. + * If a fetch is created to resolve this query, then, + * when it completes, this option is not expected to be set. + */ + qctx->options &= ~DNS_GETDB_STALEFIRST; cleanup: return (result); } +/* + * Allocate buffers in 'qctx' used to store query results. + * + * 'buffer' must be a pointer to an object whose lifetime + * doesn't expire while 'qctx' is in use. + */ +static isc_result_t +qctx_prepare_buffers(query_ctx_t *qctx, isc_buffer_t *buffer) { + REQUIRE(qctx != NULL); + REQUIRE(qctx->client != NULL); + REQUIRE(buffer != NULL); + + qctx->dbuf = ns_client_getnamebuf(qctx->client); + if (ISC_UNLIKELY(qctx->dbuf == NULL)) { + CCTRACE(ISC_LOG_ERROR, + "qctx_prepare_buffers: ns_client_getnamebuf " + "failed"); + return (ISC_R_NOMEMORY); + } + + qctx->fname = ns_client_newname(qctx->client, qctx->dbuf, buffer); + if (ISC_UNLIKELY(qctx->fname == NULL)) { + CCTRACE(ISC_LOG_ERROR, + "qctx_prepare_buffers: ns_client_newname failed"); + + return (ISC_R_NOMEMORY); + } + + qctx->rdataset = ns_client_newrdataset(qctx->client); + if (ISC_UNLIKELY(qctx->rdataset == NULL)) { + CCTRACE(ISC_LOG_ERROR, + "qctx_prepare_buffers: ns_client_newrdataset failed"); + goto error; + } + + if ((WANTDNSSEC(qctx->client) || qctx->findcoveringnsec) && + (!qctx->is_zone || dns_db_issecure(qctx->db))) + { + qctx->sigrdataset = ns_client_newrdataset(qctx->client); + if (qctx->sigrdataset == NULL) { + CCTRACE(ISC_LOG_ERROR, + "qctx_prepare_buffers: " + "ns_client_newrdataset failed (2)"); + goto error; + } + } + + return (ISC_R_SUCCESS); + +error: + if (qctx->fname != NULL) { + ns_client_releasename(qctx->client, &qctx->fname); + } + if (qctx->rdataset != NULL) { + ns_client_putrdataset(qctx->client, &qctx->rdataset); + } + + return (ISC_R_NOMEMORY); +} + +/* + * Setup a new query context for resolving a query. + * + * This function is only called if both these conditions are met: + * 1. BIND is configured with stale-answer-client-timeout 0. + * 2. A stale RRset is found in cache during initial query + * database lookup. + * + * We continue with this function for refreshing/resolving an RRset + * after answering a client with stale data. + */ +static void +query_refresh_rrset(query_ctx_t *orig_qctx) { + isc_buffer_t buffer; + query_ctx_t qctx; + + REQUIRE(orig_qctx != NULL); + REQUIRE(orig_qctx->client != NULL); + + qctx_copy(orig_qctx, &qctx); + qctx.client->query.dboptions &= ~(DNS_DBFIND_STALEONLY | + DNS_DBFIND_STALEOK | + DNS_DBFIND_STALEENABLED); + + /* + * We'll need some resources... + */ + if (qctx_prepare_buffers(&qctx, &buffer) != ISC_R_SUCCESS) { + dns_db_detach(&qctx.db); + qctx_destroy(&qctx); + return; + } + + /* + * Pretend we didn't find anything in cache. + */ + (void)query_gotanswer(&qctx, ISC_R_NOTFOUND); + + if (qctx.fname != NULL) { + ns_client_releasename(qctx.client, &qctx.fname); + } + if (qctx.rdataset != NULL) { + ns_client_putrdataset(qctx.client, &qctx.rdataset); + } + + qctx_destroy(&qctx); +} + /*% * Perform a local database lookup, in either an authoritative or * cache database. If unable to answer, call ns_query_done(); otherwise @@ -5575,15 +5740,18 @@ cleanup: */ static isc_result_t query_lookup(query_ctx_t *qctx) { - isc_buffer_t b; + isc_buffer_t buffer; isc_result_t result = ISC_R_UNSET; dns_clientinfomethods_t cm; dns_clientinfo_t ci; dns_name_t *rpzqname = NULL; unsigned int dboptions; - dns_ttl_t stale_ttl = 0; dns_ttl_t stale_refresh = 0; bool dbfind_stale = false; + bool stale_only = false; + bool stale_found = false; + bool refresh_rrset = false; + bool stale_refresh_window = false; CCTRACE(ISC_LOG_DEBUG(3), "query_lookup"); @@ -5595,37 +5763,12 @@ query_lookup(query_ctx_t *qctx) { /* * We'll need some resources... */ - qctx->dbuf = ns_client_getnamebuf(qctx->client); - if (ISC_UNLIKELY(qctx->dbuf == NULL)) { - CCTRACE(ISC_LOG_ERROR, "query_lookup: ns_client_getnamebuf " - "failed (2)"); - QUERY_ERROR(qctx, ISC_R_NOMEMORY); + result = qctx_prepare_buffers(qctx, &buffer); + if (result != ISC_R_SUCCESS) { + QUERY_ERROR(qctx, result); return (ns_query_done(qctx)); } - qctx->fname = ns_client_newname(qctx->client, qctx->dbuf, &b); - qctx->rdataset = ns_client_newrdataset(qctx->client); - - if (ISC_UNLIKELY(qctx->fname == NULL || qctx->rdataset == NULL)) { - CCTRACE(ISC_LOG_ERROR, "query_lookup: ns_client_newname failed " - "(2)"); - QUERY_ERROR(qctx, ISC_R_NOMEMORY); - return (ns_query_done(qctx)); - } - - if ((WANTDNSSEC(qctx->client) || qctx->findcoveringnsec) && - (!qctx->is_zone || dns_db_issecure(qctx->db))) - { - qctx->sigrdataset = ns_client_newrdataset(qctx->client); - if (qctx->sigrdataset == NULL) { - CCTRACE(ISC_LOG_ERROR, "query_lookup: " - "ns_client_newrdataset failed " - "(2)"); - QUERY_ERROR(qctx, ISC_R_NOMEMORY); - return (ns_query_done(qctx)); - } - } - /* * Now look for an answer in the database. */ @@ -5635,6 +5778,16 @@ query_lookup(query_ctx_t *qctx) { rpzqname = qctx->client->query.qname; } + if ((qctx->options & DNS_GETDB_STALEFIRST) != 0) { + /* + * If DNS_GETDB_STALEFIRST is set, it means that a stale + * RRset may be returned as part of this lookup. An attempt + * to refresh the RRset will still take place if an + * active RRset is not available. + */ + qctx->client->query.dboptions |= DNS_DBFIND_STALEONLY; + } + dboptions = qctx->client->query.dboptions; if (!qctx->is_zone && qctx->findcoveringnsec && (qctx->type != dns_rdatatype_null || !dns_name_istat(rpzqname))) @@ -5644,18 +5797,9 @@ query_lookup(query_ctx_t *qctx) { (void)dns_db_getservestalerefresh(qctx->client->view->cachedb, &stale_refresh); - (void)dns_db_getservestalettl(qctx->client->view->cachedb, &stale_ttl); - if (stale_refresh > 0) { - if (qctx->client->view->staleanswersok == dns_stale_answer_yes) - { - dboptions |= DNS_DBFIND_STALEENABLED; - } else if (qctx->client->view->staleanswersok == - dns_stale_answer_conf) { - if (qctx->client->view->staleanswersenable && - stale_ttl > 0) { - dboptions |= DNS_DBFIND_STALEENABLED; - } - } + if (stale_refresh > 0 && + dns_view_staleanswerenabled(qctx->client->view)) { + dboptions |= DNS_DBFIND_STALEENABLED; } result = dns_db_findext(qctx->db, rpzqname, qctx->version, qctx->type, @@ -5681,69 +5825,214 @@ query_lookup(query_ctx_t *qctx) { /* * If DNS_DBFIND_STALEOK is set this means we are dealing with a * lookup following a failed lookup and it is okay to serve a stale - * answer. This will start a time window in rbtdb, tracking the last - * time the RRset lookup failed. - * - * A stale answer may also be served if this is a normal lookup, - * the view has enabled serve-stale (DNS_DBFIND_STALE_ENABLED is set), - * and the request is within the stale-refresh-time window. If this - * is the case we have to make sure that the lookup found a stale - * answer, otherwise "fresh" answers are also treated as stale. + * answer. This will (re)start the 'stale-refresh-time' window in + * rbtdb, tracking the last time the RRset lookup failed. */ dbfind_stale = ((dboptions & DNS_DBFIND_STALEOK) != 0); - if (dbfind_stale != 0 || - (((dboptions & DNS_DBFIND_STALEENABLED) != 0) && - STALE(qctx->rdataset))) + + /* + * If DNS_DBFIND_STALEENABLED is set, this may be a normal lookup, but + * we are allowed to immediately respond with a stale answer if the + * request is within the 'stale-refresh-time' window. + */ + stale_refresh_window = (STALE_WINDOW(qctx->rdataset) && + (dboptions & DNS_DBFIND_STALEENABLED) != 0); + + /* + * If DNS_DBFIND_STALEONLY is set, a stale positive answer is requested. + * This can happen if 'stale-answer-client-timeout' is enabled. + * + * If 'stale-answer-client-timeout' is set to 0, and a stale positive + * answer is found, send it to the client, and try to refresh the + * RRset. If a stale negative answer is found, continue with recursion + * (perhaps the query will be resolved eventually and the answer from + * the authoritative is returned to the client, or the query will + * timeout, in that case DNS_DBFIND_STALEOK may be set, and a stale + * negative answer is returned (or SERVFAIL). + * + * If 'stale-answer-client-timeout' is non-zero, and a stale positive + * answer is found, send it to the client. Don't try to refresh the + * RRset because a fetch is already in progress. If a stale negative + * answer is found, then abort the lookup and the client has to wait + * until recursion is finished. + */ + stale_only = ((dboptions & DNS_DBFIND_STALEONLY) != 0); + + if (dbfind_stale || + (STALE(qctx->rdataset) && (stale_only || stale_refresh_window))) { char namebuf[DNS_NAME_FORMATSIZE]; - bool success; inc_stats(qctx->client, ns_statscounter_trystale); - qctx->client->query.dboptions &= ~DNS_DBFIND_STALEOK; if (dns_rdataset_isassociated(qctx->rdataset) && dns_rdataset_count(qctx->rdataset) > 0 && STALE(qctx->rdataset)) { qctx->rdataset->ttl = qctx->view->staleanswerttl; - success = true; + stale_found = true; } else { - success = false; + stale_found = false; } dns_name_format(qctx->client->query.qname, namebuf, sizeof(namebuf)); + if (dbfind_stale) { isc_log_write(ns_lctx, NS_LOGCATEGORY_SERVE_STALE, NS_LOGMODULE_QUERY, ISC_LOG_INFO, "%s resolver failure, stale answer %s", namebuf, - success ? "used" : "unavailable"); - } else { + stale_found ? "used" : "unavailable"); + if (!stale_found) { + /* + * Resolver failure, no stale data, nothing + * more we can do, return SERVFAIL. + */ + QUERY_ERROR(qctx, DNS_R_SERVFAIL); + return (ns_query_done(qctx)); + } + + stale_only = false; + } else if (stale_refresh_window) { + /* + * A recent lookup failed, so during this time window + * we are allowed to return stale data immediately. + */ isc_log_write(ns_lctx, NS_LOGCATEGORY_SERVE_STALE, NS_LOGMODULE_QUERY, ISC_LOG_INFO, "%s query within stale refresh time, " "stale answer %s", namebuf, - success ? "used" : "unavailable"); - } + stale_found ? "used" : "unavailable"); - if (!success) { - QUERY_ERROR(qctx, DNS_R_SERVFAIL); - return (ns_query_done(qctx)); + if (!stale_found) { + /* + * During the stale refresh window explicitly + * do not try to refresh the data, because a + * recent lookup failed. + */ + QUERY_ERROR(qctx, DNS_R_SERVFAIL); + return (ns_query_done(qctx)); + } + + stale_only = false; + } else if ((dboptions & DNS_DBFIND_STALEONLY) != 0) { + if ((qctx->options & DNS_GETDB_STALEFIRST) != 0) { + if (stale_found && result == ISC_R_SUCCESS) { + /* + * Immediately return the stale answer, + * start a resolver fetch to refresh + * the data in cache. + */ + isc_log_write( + ns_lctx, + NS_LOGCATEGORY_SERVE_STALE, + NS_LOGMODULE_QUERY, + ISC_LOG_INFO, + "%s stale answer used, an " + "attempt to refresh the RRset " + "will still be made", + namebuf); + refresh_rrset = true; + } else { + /* + * We have nothing useful in cache to + * return immediately. + */ + qctx_clean(qctx); + qctx_freedata(qctx); + dns_db_attach( + qctx->client->view->cachedb, + &qctx->db); + qctx->client->query.dboptions &= + ~DNS_DBFIND_STALEONLY; + qctx->options &= ~DNS_GETDB_STALEFIRST; + if (qctx->client->query.fetch != NULL) { + dns_resolver_destroyfetch( + &qctx->client->query + .fetch); + } + return query_lookup(qctx); + } + } else { + /* + * The 'stale-answer-client-timeout' triggered, + * return the stale answer if available, + * otherwise wait until the resolver finishes. + */ + isc_log_write( + ns_lctx, NS_LOGCATEGORY_SERVE_STALE, + NS_LOGMODULE_QUERY, ISC_LOG_INFO, + "%s client timeout, stale answer %s", + namebuf, + stale_found ? "used" : "unavailable"); + if (!stale_found || result != ISC_R_SUCCESS) { + return (result); + } + } } + } else { + stale_only = false; + } + + if (!stale_only) { + /* + * The DNS_DBFIND_STALEONLY option may be set, but if we + * didn't have stale data in cache, or we responded with + * a stale answer because of 'stale-refresh-time', this is + * actually not a 'stale-only' lookup. Clear the flag to + * allow the client to be detach the handle. + */ + qctx->client->query.dboptions &= ~DNS_DBFIND_STALEONLY; + } + + result = query_gotanswer(qctx, result); + + if (refresh_rrset) { + /* + * If we reached this point then it means that we have found a + * stale RRset entry in cache and BIND is configured to allow + * queries to be answered with stale data if no active RRset + * is available, i.e. "stale-anwer-client-timeout 0". But, we + * still need to refresh the RRset. + */ + query_refresh_rrset(qctx); } - 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) { + query_ctx_t qctx; + + qctx_init(client, NULL, client->query.qtype, &qctx); + dns_db_attach(client->view->cachedb, &qctx.db); + client->query.dboptions |= DNS_DBFIND_STALEONLY; + (void)query_lookup(&qctx); + if (qctx.node != NULL) { + dns_db_detachnode(qctx.db, &qctx.node); + } + qctx_freedata(&qctx); + client->query.dboptions &= ~DNS_DBFIND_STALEONLY; + qctx_destroy(&qctx); +} + +/* + * 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) { @@ -5758,7 +6047,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); @@ -5766,6 +6056,14 @@ 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); + isc_event_free(ISC_EVENT_PTR(&event)); + return; + } else { + client->query.dboptions &= ~DNS_DBFIND_STALEONLY; + } + LOCK(&client->query.fetchlock); if (client->query.fetch != NULL) { /* @@ -6086,6 +6384,13 @@ ns_query_recurse(ns_client_t *client, dns_rdatatype_t qtype, dns_name_t *qname, peeraddr = &client->peeraddr; } + if (client->view->staleanswerclienttimeout > 0 && + client->view->staleanswerclienttimeout != (uint32_t)-1 && + 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, @@ -7136,45 +7441,20 @@ root_key_sentinel_return_servfail(query_ctx_t *qctx, isc_result_t result) { */ static bool query_usestale(query_ctx_t *qctx) { - bool staleanswersok = false; - dns_ttl_t stale_ttl = 0; - isc_result_t result; - qctx_clean(qctx); qctx_freedata(qctx); - /* - * Stale answers only make sense if stale_ttl > 0 but we want rndc to - * be able to control returning stale answers if they are configured. - */ - dns_db_attach(qctx->client->view->cachedb, &qctx->db); - result = dns_db_getservestalettl(qctx->db, &stale_ttl); - if (result == ISC_R_SUCCESS && stale_ttl > 0) { - switch (qctx->client->view->staleanswersok) { - case dns_stale_answer_yes: - staleanswersok = true; - break; - case dns_stale_answer_conf: - staleanswersok = qctx->client->view->staleanswersenable; - break; - case dns_stale_answer_no: - staleanswersok = false; - break; - } - } else { - staleanswersok = false; - } - - if (staleanswersok) { + if (dns_view_staleanswerenabled(qctx->client->view)) { + dns_db_attach(qctx->client->view->cachedb, &qctx->db); qctx->client->query.dboptions |= DNS_DBFIND_STALEOK; if (qctx->client->query.fetch != NULL) { dns_resolver_destroyfetch(&qctx->client->query.fetch); } - } else { - dns_db_detach(&qctx->db); + + return (true); } - return (staleanswersok); + return (false); } /*% @@ -7688,7 +7968,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)) + { query_prefetch(qctx->client, qctx->fname, qctx->rdataset); } @@ -7796,7 +8078,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)); query_addauth(qctx); @@ -11213,6 +11495,7 @@ isc_result_t ns_query_done(query_ctx_t *qctx) { isc_result_t result = ISC_R_UNSET; const dns_namelist_t *secs = qctx->client->message->sections; + bool query_stale_only; CCTRACE(ISC_LOG_DEBUG(3), "ns_query_done"); @@ -11281,7 +11564,10 @@ 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->query) || + ((qctx->options & DNS_GETDB_STALEFIRST) != 0))) + { return (qctx->result); } @@ -11292,8 +11578,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) @@ -11315,9 +11603,16 @@ ns_query_done(query_ctx_t *qctx) { CALL_HOOK(NS_QUERY_DONE_SEND, qctx); + /* + * Client may have been detached after query_send(), so + * we test and store the flag state here, for safety. + */ + query_stale_only = QUERY_STALEONLY(&qctx->client->query); query_send(qctx->client); - qctx->detach_client = true; + if (!query_stale_only) { + qctx->detach_client = true; + } return (qctx->result); cleanup: