From 854458469185074f261c9ca2b89f8ceaefb1d770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Men=C5=A1=C3=ADk?= Date: Wed, 6 Nov 2019 21:46:21 +0100 Subject: [PATCH] Add serve-stale feature Backported from 9.12 version, adds support for stale-answer-enable option, as well stale-answer-ttl and max-stale-ttl. --- bind-9.11-serve-stale.patch | 3858 +++++++++++++++++++++++++++++++++++ bind.spec | 7 +- 2 files changed, 3864 insertions(+), 1 deletion(-) create mode 100644 bind-9.11-serve-stale.patch diff --git a/bind-9.11-serve-stale.patch b/bind-9.11-serve-stale.patch new file mode 100644 index 0000000..ed03636 --- /dev/null +++ b/bind-9.11-serve-stale.patch @@ -0,0 +1,3858 @@ +From 1196b07e79e1d8d23afd1003a7a242ac06a2f2a2 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Petr=20Men=C5=A1=C3=ADk?= +Date: Thu, 7 Nov 2019 14:31:03 +0100 +Subject: [PATCH] Implement serve-stale in 9.11 +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Squashed commit of the following: + +commit 32f47f36e545223b2a4757588d7bd4af8c5f5760 +Author: Petr Menšík +Date: Tue Sep 3 18:45:54 2019 +0200 + + convert serve_stale to db_test + + Manual checkout from commit e8f61dd315c5d1c88915bb79361182241e42e47a. + Use test modified for cmocka, including serve-stale tests. + +commit 071eb1fb0786f6d614955813d99c3caabff33383 +Author: Michał Kępień +Date: Fri Apr 27 09:13:26 2018 +0200 + + Detect recursion loops during query processing + + Interrupt query processing when query_recurse() attempts to ask the same + name servers for the same QNAME/QTYPE tuple for two times in a row as + this indicates that query processing may be stuck for an indeterminate + period of time, e.g. due to interactions between features able to + restart query_lookup(). + + (cherry picked from commit 46bb4dd124ed031d4c219d1e37a3c6322092e30c) + +commit c12090bc361c7fa4522ace73899e778e44e9b295 +Author: Petr Menšík +Date: Mon Sep 2 11:12:32 2019 +0200 + + Fix test name used in whole test-suite + + Correct name is serve-stale + +commit ff4d826f295d268a248ca06941d65c903e1b405c +Author: Petr Menšík +Date: Fri Aug 30 17:43:28 2019 +0200 + + Clean files in more generic rules + +commit 8d81ed15eda9a2a11e1433d1fdddacfc772708b6 +Author: Petr Menšík +Date: Thu Aug 29 21:27:57 2019 +0200 + + [rt46602] Pass port numbers to tests via environment variables + + Manually applied commit f5d8f079008b648d2e343543e66dd728054c6101 + +commit 94fafa477891576286def8c4041ad127734af2d1 +Author: Tony Finch +Date: Tue Apr 10 16:17:57 2018 +0100 + + Move serve-stale logging to its own category, so that its verbosity can be curtailed. + + (cherry picked from commit 4b442c309dfb2c8880b19af4133047655bb734df) + +commit e0c884bee98c3d2533dfaa667f58c6a80d8a3a00 +Author: Michał Kępień +Date: Fri Apr 27 09:13:26 2018 +0200 + + Prevent check_stale_header() from leaking rdataset headers + + check_stale_header() fails to update the pointer to the previous header + while processing rdataset headers eligible for serve-stale, thus + enabling rdataset headers to be leaked (i.e. disassociated from a node + and left on the relevant TTL heap) while iterating through a node. This + can lead to several different assertion failures. Add the missing + pointer update. + + (cherry picked from commit 391fac1fc8d2e470287b5cc4344b3adb90c6f54a) + +commit d724cc1d80ee8d46113eaf82549d49636739b67c +Author: Matthijs Mekking +Date: Thu Jan 24 10:24:44 2019 +0100 + + Print in dump-file stale ttl + + This change makes rndc dumpdb correctly print the "; stale" line. + It also provides extra information on how long this data may still + be served to clients (in other words how long the stale RRset may + still be used). + + (cherry picked from commit 924ebc605db798e2a383ee5eaaebad739e7c789c) + +commit 625da4bd4590ac6108bb30eddd23ceffb245ae49 +Author: Michał Kępień +Date: Mon Oct 22 15:26:45 2018 +0200 + + Check serve-stale behavior with a cold cache + + Ensure that serve-stale works as expected when returning stale answers + is enabled, the authoritative server does not respond, and there is no + cached answer available. + + (cherry picked from commit 27cfe83a388147edfa0451b28c06c746912ea684) + +commit d67ae10461c409fdafdbbe64f857db2552b71059 +Author: Michał Kępień +Date: Mon Oct 22 15:26:45 2018 +0200 + + Check TTL of stale answers + + Make sure that stale answers returned when the serve-stale feature is + enabled have a TTL matching the value of the stale-answer-ttl setting. + + (cherry picked from commit 893ab37ce78c658215bd3a019f25afe795b37d5a) + +commit 50459107805e68e4a63a8e497bf58ef3ce013ddb +Author: Michał Kępień +Date: Mon Jul 9 14:35:12 2018 +0200 + + Do not use Net::DNS::Nameserver in the "serve-stale" system test + + Net::DNS versions older than 0.67 respond to queries sent to a + Net::DNS::Nameserver even if its ReplyHandler returns undef. This makes + the "serve-stale" system test fail as it takes advantage of the newer + behavior. Since the latest Net::DNS version available with stock + RHEL/CentOS 6 packages is 0.65 and we officially support that operating + system, bin/tests/system/serve-stale/ans2/ans.pl should behave + consistently for various Net::DNS versions. Ensure that by reworking it + so that it does not use Net::DNS::Nameserver. + + (cherry picked from commit c4209418a50c09142375f7edadca731c526f3d3a) + +commit 4b5befc714bb386bd245b1c14ce3bce5ae6fb5fa +Author: Petr Menšík +Date: Tue Jun 5 21:38:29 2018 +0200 + + Fix server-stale requirement, skip without Time::HiRes + + (cherry picked from commit 7a0c7bf9c8e6a724e52635eed213ad25b9504e66) + +commit 5ce51a3a7e5ef3087c4d022e3fca42fb2fd0c996 +Author: Ondřej Surý +Date: Wed Oct 18 13:01:14 2017 +0200 + + [rt46602] Update server-stale test to run on port passed from run.sh script + + (cherry picked from commit f83ebd34b9555a5a834c58146035173bcbd01dda) + +commit 3954a9bf3437f6fab050294a7f2f954a23d161ec +Author: Ondřej Surý +Date: Wed Oct 18 14:18:59 2017 +0200 + + [rt46602] Add serve-stale working files to .gitignore + + (cherry picked from commit cba162e70e7fac43435a606106841a69ce468526) + +commit 112aa21f5fa875494820e4d1eb70e41e10e1aae7 +Author: Mark Andrews +Date: Thu Oct 12 15:33:47 2017 +1100 + + test for Net::DNS::Nameserver + + (cherry picked from commit 5b60d0608ac2852753180b762d1917163f9dc315) + +commit 9d610e46af8a636f44914cee4cf8b2016054db1e +Author: Mark Andrews +Date: Thu Oct 12 15:19:45 2017 +1100 + + add Net::DNS prerequiste test + + (cherry picked from commit fa644181f51559da3e3913acd72dbc3f6d916e71) + +commit e4ea7ba88d9a9a0c79579400c68a5dabe03e8572 +Author: Mark Andrews +Date: Wed Sep 6 19:26:10 2017 +1000 + + add quotes arount $send_response + + (cherry picked from commit 023ab19634b287543169e9b7b5259f3126cd60ff) + +commit 0af0c5d33c2de34da164571288b650282c6be10a +Author: Mark Andrews +Date: Thu Nov 23 16:11:49 2017 +1100 + + initalise serve_stale_ttl + + (cherry picked from commit 2f4e0e5a81278f59037bf06ae99ff52245cd57e9) + +commit fbadd90ee81863d617c4c319d5f0079b877fe102 +Author: Evan Hunt +Date: Thu Sep 14 11:48:21 2017 -0700 + + [master] add thanks to APNIC and add missing note for serve-stale + +commit deb8adaa59955970b9d2f2fe58060a3cbf08312b +Author: Mark Andrews +Date: Wed Sep 6 12:16:10 2017 +1000 + + silence 'staleanswersok' may be used uninitialized in this function warning. [RT #14147 + +commit 0e2d03823768dc545015e6ce309777210f4a9f85 +Author: Petr Menšík +Date: Thu Aug 29 19:57:58 2019 +0200 + + More fixes to merge + +commit 360e25ffe7623ea0a2eec49395001f4940967776 +Author: Mark Andrews +Date: Wed Sep 6 09:58:29 2017 +1000 + + 4700. [func] Serving of stale answers is now supported. This + allows named to provide stale cached answers when + the authoritative server is under attack. + See max-stale-ttl, stale-answer-enable, + stale-answer-ttl. [RT #44790] + +Signed-off-by: Petr Menšík +--- + bin/named/config.c | 9 +- + bin/named/control.c | 2 + + bin/named/include/named/control.h | 1 + + bin/named/include/named/log.h | 1 + + bin/named/include/named/query.h | 15 + + bin/named/include/named/server.h | 13 +- + bin/named/log.c | 1 + + bin/named/query.c | 164 +++++- + bin/named/server.c | 177 +++++- + bin/named/statschannel.c | 6 + + bin/rndc/rndc.c | 2 + + bin/rndc/rndc.docbook | 19 + + bin/tests/system/chain/prereq.sh | 7 + + bin/tests/system/conf.sh.in | 2 +- + bin/tests/system/dyndb/driver/db.c | 2 + + bin/tests/system/serve-stale/.gitignore | 11 + + bin/tests/system/serve-stale/ans2/ans.pl.in | 178 ++++++ + bin/tests/system/serve-stale/clean.sh | 15 + + .../system/serve-stale/ns1/named1.conf.in | 35 ++ + .../system/serve-stale/ns1/named2.conf.in | 35 ++ + bin/tests/system/serve-stale/ns1/root.db | 5 + + .../system/serve-stale/ns3/named.conf.in | 35 ++ + bin/tests/system/serve-stale/prereq.sh | 38 ++ + bin/tests/system/serve-stale/setup.sh | 13 + + bin/tests/system/serve-stale/tests.sh | 536 ++++++++++++++++++ + doc/arm/Bv9ARM-book.xml | 69 ++- + doc/arm/logging-categories.xml | 11 + + doc/arm/notes-rh-changes.xml | 14 +- + doc/misc/options | 10 + + lib/bind9/check.c | 78 ++- + lib/dns/cache.c | 38 +- + lib/dns/db.c | 22 + + lib/dns/ecdb.c | 4 +- + lib/dns/include/dns/cache.h | 21 + + lib/dns/include/dns/db.h | 35 ++ + lib/dns/include/dns/rdataset.h | 11 + + lib/dns/include/dns/resolver.h | 43 +- + lib/dns/include/dns/types.h | 6 + + lib/dns/include/dns/view.h | 3 + + lib/dns/master.c | 14 +- + lib/dns/masterdump.c | 23 + + lib/dns/rbtdb.c | 207 ++++++- + lib/dns/resolver.c | 78 ++- + lib/dns/sdb.c | 4 +- + lib/dns/sdlz.c | 4 +- + lib/dns/tests/db_test.c | 198 ++++++- + lib/dns/view.c | 3 + + lib/isccfg/namedconf.c | 5 + + 48 files changed, 2121 insertions(+), 102 deletions(-) + create mode 100644 bin/tests/system/serve-stale/.gitignore + create mode 100644 bin/tests/system/serve-stale/ans2/ans.pl.in + create mode 100644 bin/tests/system/serve-stale/clean.sh + create mode 100644 bin/tests/system/serve-stale/ns1/named1.conf.in + create mode 100644 bin/tests/system/serve-stale/ns1/named2.conf.in + create mode 100644 bin/tests/system/serve-stale/ns1/root.db + create mode 100644 bin/tests/system/serve-stale/ns3/named.conf.in + create mode 100644 bin/tests/system/serve-stale/prereq.sh + create mode 100644 bin/tests/system/serve-stale/setup.sh + create mode 100755 bin/tests/system/serve-stale/tests.sh + +diff --git a/bin/named/config.c b/bin/named/config.c +index 63da4b03f6..b598f9bfe3 100644 +--- a/bin/named/config.c ++++ b/bin/named/config.c +@@ -182,13 +182,14 @@ options {\n\ + #ifdef HAVE_LMDB + " lmdb-mapsize 32M;\n" + #endif +-" max-acache-size 16M;\n\ +- max-cache-size 90%;\n\ ++" max-cache-size 90%;\n\ ++ max-acache-size 16M;\n\ + max-cache-ttl 604800; /* 1 week */\n\ + max-clients-per-query 100;\n\ + max-ncache-ttl 10800; /* 3 hours */\n\ + max-recursion-depth 7;\n\ + max-recursion-queries 75;\n\ ++ max-stale-ttl 604800; /* 1 week */\n\ + message-compression yes;\n\ + # min-roots ;\n\ + minimal-any false;\n\ +@@ -203,10 +204,14 @@ options {\n\ + request-expire true;\n\ + request-ixfr true;\n\ + require-server-cookie no;\n\ ++ resolver-nonbackoff-tries 3;\n\ ++ resolver-retry-interval 800; /* in milliseconds */\n\ + # rfc2308-type1 ;\n\ + root-key-sentinel yes;\n\ + servfail-ttl 1;\n\ + # sortlist \n\ ++ stale-answer-enable false;\n\ ++ stale-answer-ttl 1; /* 1 second */\n\ + # topology \n\ + transfer-format many-answers;\n\ + v6-bias 50;\n\ +diff --git a/bin/named/control.c b/bin/named/control.c +index df23c26507..8b79850b3d 100644 +--- a/bin/named/control.c ++++ b/bin/named/control.c +@@ -282,6 +282,8 @@ ns_control_docommand(isccc_sexpr_t *message, bool readonly, + result = ns_server_validation(ns_g_server, lex, text); + } else if (command_compare(command, NS_COMMAND_ZONESTATUS)) { + result = ns_server_zonestatus(ns_g_server, lex, text); ++ } else if (command_compare(command, NS_COMMAND_SERVESTALE)) { ++ result = ns_server_servestale(ns_g_server, lex, text); + } else { + isc_log_write(ns_g_lctx, NS_LOGCATEGORY_GENERAL, + NS_LOGMODULE_CONTROL, ISC_LOG_WARNING, +diff --git a/bin/named/include/named/control.h b/bin/named/include/named/control.h +index 8705fdd68a..1634154c1c 100644 +--- a/bin/named/include/named/control.h ++++ b/bin/named/include/named/control.h +@@ -69,6 +69,7 @@ + #define NS_COMMAND_MKEYS "managed-keys" + #define NS_COMMAND_DNSTAPREOPEN "dnstap-reopen" + #define NS_COMMAND_DNSTAP "dnstap" ++#define NS_COMMAND_SERVESTALE "serve-stale" + + isc_result_t + ns_controls_create(ns_server_t *server, ns_controls_t **ctrlsp); +diff --git a/bin/named/include/named/log.h b/bin/named/include/named/log.h +index 56bfcd4668..cd8db60ed4 100644 +--- a/bin/named/include/named/log.h ++++ b/bin/named/include/named/log.h +@@ -32,6 +32,7 @@ + #define NS_LOGCATEGORY_UPDATE_SECURITY (&ns_g_categories[6]) + #define NS_LOGCATEGORY_QUERY_ERRORS (&ns_g_categories[7]) + #define NS_LOGCATEGORY_TAT (&ns_g_categories[8]) ++#define NS_LOGCATEGORY_SERVE_STALE (&ns_g_categories[9]) + + /* + * Backwards compatibility. +diff --git a/bin/named/include/named/query.h b/bin/named/include/named/query.h +index 9661f56b72..445b578c08 100644 +--- a/bin/named/include/named/query.h ++++ b/bin/named/include/named/query.h +@@ -35,6 +35,18 @@ typedef struct ns_dbversion { + ISC_LINK(struct ns_dbversion) link; + } ns_dbversion_t; + ++/*% ++ * nameserver recursion parameters, to uniquely identify a recursion ++ * query; this is used to detect a recursion loop ++ */ ++typedef struct ns_query_recparam { ++ dns_rdatatype_t qtype; ++ dns_name_t * qname; ++ dns_fixedname_t fqname; ++ dns_name_t * qdomain; ++ dns_fixedname_t fqdomain; ++} ns_query_recparam_t; ++ + /*% nameserver query structure */ + struct ns_query { + unsigned int attributes; +@@ -63,6 +75,7 @@ struct ns_query { + unsigned int dns64_aaaaoklen; + unsigned int dns64_options; + unsigned int dns64_ttl; ++ + struct { + dns_db_t * db; + dns_zone_t * zone; +@@ -76,6 +89,8 @@ struct ns_query { + bool authoritative; + bool is_zone; + } redirect; ++ ++ ns_query_recparam_t recparam; + dns_keytag_t root_key_sentinel_keyid; + bool root_key_sentinel_is_ta; + bool root_key_sentinel_not_ta; +diff --git a/bin/named/include/named/server.h b/bin/named/include/named/server.h +index b2c1d05497..86f0b2bfb7 100644 +--- a/bin/named/include/named/server.h ++++ b/bin/named/include/named/server.h +@@ -222,7 +222,10 @@ enum { + + dns_nsstatscounter_keytagopt = 56, + +- dns_nsstatscounter_max = 57 ++ dns_nsstatscounter_trystale = 57, ++ dns_nsstatscounter_usedstale = 58, ++ ++ dns_nsstatscounter_max = 59 + }; + + /*% +@@ -761,4 +764,12 @@ ns_server_mkeys(ns_server_t *server, isc_lex_t *lex, isc_buffer_t **text); + isc_result_t + ns_server_dnstap(ns_server_t *server, isc_lex_t *lex, isc_buffer_t **text); + ++ ++/*% ++ * Control whether stale answers are served or not when configured in ++ * named.conf. ++ */ ++isc_result_t ++ns_server_servestale(ns_server_t *server, isc_lex_t *lex, ++ isc_buffer_t **text); + #endif /* NAMED_SERVER_H */ +diff --git a/bin/named/log.c b/bin/named/log.c +index 3aa25e9a95..12f178b342 100644 +--- a/bin/named/log.c ++++ b/bin/named/log.c +@@ -38,6 +38,7 @@ static isc_logcategory_t categories[] = { + { "update-security", 0 }, + { "query-errors", 0 }, + { "trust-anchor-telemetry", 0 }, ++ { "serve-stale", 0 }, + { NULL, 0 } + }; + +diff --git a/bin/named/query.c b/bin/named/query.c +index 55b7b7cbde..f872dfc842 100644 +--- a/bin/named/query.c ++++ b/bin/named/query.c +@@ -125,10 +125,14 @@ + #define REDIRECT(c) (((c)->query.attributes & \ + NS_QUERYATTR_REDIRECT) != 0) + +-/*% No QNAME Proof? */ ++/*% 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) ++ + #ifdef WANT_QUERYTRACE + static inline void + client_trace(ns_client_t *client, int level, const char *message) { +@@ -217,6 +221,10 @@ static bool + rpz_ck_dnssec(ns_client_t *client, isc_result_t qresult, + dns_rdataset_t *rdataset, dns_rdataset_t *sigrdataset); + ++static void ++recparam_update(ns_query_recparam_t *param, dns_rdatatype_t qtype, ++ const dns_name_t *qname, const dns_name_t *qdomain); ++ + /*% + * Increment query statistics counters. + */ +@@ -470,6 +478,7 @@ query_reset(ns_client_t *client, bool everything) { + client->query.isreferral = false; + client->query.dns64_options = 0; + client->query.dns64_ttl = UINT32_MAX; ++ recparam_update(&client->query.recparam, 0, NULL, NULL); + client->query.root_key_sentinel_keyid = 0; + client->query.root_key_sentinel_is_ta = false; + client->query.root_key_sentinel_not_ta = false; +@@ -4254,6 +4263,54 @@ query_prefetch(ns_client_t *client, dns_name_t *qname, + dns_rdataset_clearprefetch(rdataset); + } + ++/*% ++ * Check whether the recursion parameters in 'param' match the current query's ++ * recursion parameters provided in 'qtype', 'qname', and 'qdomain'. ++ */ ++static bool ++recparam_match(const ns_query_recparam_t *param, dns_rdatatype_t qtype, ++ const dns_name_t *qname, const dns_name_t *qdomain) ++{ ++ REQUIRE(param != NULL); ++ ++ return (param->qtype == qtype && ++ param->qname != NULL && qname != NULL && ++ param->qdomain != NULL && qdomain != NULL && ++ dns_name_equal(param->qname, qname) && ++ dns_name_equal(param->qdomain, qdomain)); ++} ++ ++/*% ++ * Update 'param' with current query's recursion parameters provided in ++ * 'qtype', 'qname', and 'qdomain'. ++ */ ++static void ++recparam_update(ns_query_recparam_t *param, dns_rdatatype_t qtype, ++ const dns_name_t *qname, const dns_name_t *qdomain) ++{ ++ isc_result_t result; ++ ++ REQUIRE(param != NULL); ++ ++ param->qtype = qtype; ++ ++ if (qname == NULL) { ++ param->qname = NULL; ++ } else { ++ param->qname = dns_fixedname_initname(¶m->fqname); ++ result = dns_name_copy(qname, param->qname, NULL); ++ RUNTIME_CHECK(result == ISC_R_SUCCESS); ++ } ++ ++ if (qdomain == NULL) { ++ param->qdomain = NULL; ++ } else { ++ param->qdomain = dns_fixedname_initname(¶m->fqdomain); ++ result = dns_name_copy(qdomain, param->qdomain, NULL); ++ RUNTIME_CHECK(result == ISC_R_SUCCESS); ++ } ++} ++ + static isc_result_t + query_recurse(ns_client_t *client, dns_rdatatype_t qtype, dns_name_t *qname, + dns_name_t *qdomain, dns_rdataset_t *nameservers, +@@ -4263,6 +4320,19 @@ query_recurse(ns_client_t *client, dns_rdatatype_t qtype, dns_name_t *qname, + dns_rdataset_t *rdataset, *sigrdataset; + isc_sockaddr_t *peeraddr; + ++ /* ++ * Check recursion parameters from the previous query to see if they ++ * match. If not, update recursion parameters and proceed. ++ */ ++ if (recparam_match(&client->query.recparam, qtype, qname, qdomain)) { ++ ns_client_log(client, NS_LOGCATEGORY_CLIENT, ++ NS_LOGMODULE_QUERY, ISC_LOG_INFO, ++ "recursion loop detected"); ++ return (ISC_R_FAILURE); ++ } ++ ++ recparam_update(&client->query.recparam, qtype, qname, qdomain); ++ + if (!resuming) + inc_stats(client, dns_nsstatscounter_recursion); + +@@ -6780,6 +6850,7 @@ query_find(ns_client_t *client, dns_fetchevent_t *event, dns_rdatatype_t qtype) + int line = -1; + bool dns64_exclude, dns64, rpz; + bool nxrewrite = false; ++ bool want_stale = false; + bool redirected = false; + dns_clientinfomethods_t cm; + dns_clientinfo_t ci; +@@ -7089,6 +7160,7 @@ query_find(ns_client_t *client, dns_fetchevent_t *event, dns_rdatatype_t qtype) + type = qtype; + + restart: ++ // query_start + CTRACE(ISC_LOG_DEBUG(3), "query_find: restart"); + want_restart = false; + authoritative = false; +@@ -7233,6 +7305,7 @@ query_find(ns_client_t *client, dns_fetchevent_t *event, dns_rdatatype_t qtype) + } + + db_find: ++ // query_lookup + CTRACE(ISC_LOG_DEBUG(3), "query_find: db_find"); + /* + * We'll need some resources... +@@ -7290,6 +7363,35 @@ query_find(ns_client_t *client, dns_fetchevent_t *event, dns_rdatatype_t qtype) + if (!is_zone) + dns_cache_updatestats(client->view->cache, result); + ++ if (want_stale) { ++ char namebuf[DNS_NAME_FORMATSIZE]; ++ bool success; ++ ++ client->query.dboptions &= ~DNS_DBFIND_STALEOK; ++ want_stale = false; ++ ++ if (dns_rdataset_isassociated(rdataset) && ++ dns_rdataset_count(rdataset) > 0 && ++ STALE(rdataset)) { ++ rdataset->ttl = client->view->staleanswerttl; ++ success = true; ++ } else { ++ success = false; ++ } ++ ++ dns_name_format(client->query.qname, ++ namebuf, sizeof(namebuf)); ++ isc_log_write(ns_g_lctx, NS_LOGCATEGORY_SERVE_STALE, ++ NS_LOGMODULE_QUERY, ISC_LOG_INFO, ++ "%s resolver failure, stale answer %s", ++ namebuf, success ? "used" : "unavailable"); ++ ++ if (!success) { ++ QUERY_ERROR(DNS_R_SERVFAIL); ++ goto cleanup; ++ } ++ } ++ + resume: + CTRACE(ISC_LOG_DEBUG(3), "query_find: resume"); + +@@ -7635,6 +7737,7 @@ query_find(ns_client_t *client, dns_fetchevent_t *event, dns_rdatatype_t qtype) + * The cache doesn't even have the root NS. Get them from + * the hints DB. + */ ++ // query_notfound + INSIST(!is_zone); + if (db != NULL) + dns_db_detach(&db); +@@ -7697,12 +7800,14 @@ query_find(ns_client_t *client, dns_fetchevent_t *event, dns_rdatatype_t qtype) + */ + /* FALLTHROUGH */ + case DNS_R_DELEGATION: ++ // query_delegation + authoritative = false; + if (is_zone) { + /* + * Look to see if we are authoritative for the + * child zone if the query type is DS. + */ ++ // query_zone_delegation + if (!RECURSIONOK(client) && + (options & DNS_GETDB_NOEXACT) != 0 && + qtype == dns_rdatatype_ds) { +@@ -8089,6 +8194,7 @@ query_find(ns_client_t *client, dns_fetchevent_t *event, dns_rdatatype_t qtype) + false, true); + } + } ++ // query_nxdomain + if (dns_rdataset_isassociated(rdataset)) { + /* + * If we've got a NSEC record, we need to save the +@@ -8409,7 +8515,8 @@ query_find(ns_client_t *client, dns_fetchevent_t *event, dns_rdatatype_t qtype) + /* + * If we have a zero ttl from the cache refetch it. + */ +- if (!is_zone && !resuming && rdataset->ttl == 0 && ++ // query_cname ++ if (!is_zone && !resuming && !STALE(rdataset) && rdataset->ttl == 0 && + RECURSIONOK(client)) + { + if (dns_rdataset_isassociated(rdataset)) +@@ -8627,7 +8734,11 @@ query_find(ns_client_t *client, dns_fetchevent_t *event, dns_rdatatype_t qtype) + "query_find: unexpected error after resuming: %s", + isc_result_totext(result)); + CTRACE(ISC_LOG_ERROR, errmsg); +- QUERY_ERROR(DNS_R_SERVFAIL); ++ if (resuming) { ++ want_stale = true; ++ } else { ++ QUERY_ERROR(DNS_R_SERVFAIL); ++ } + goto cleanup; + } + +@@ -8883,7 +8994,7 @@ query_find(ns_client_t *client, dns_fetchevent_t *event, dns_rdatatype_t qtype) + /* + * If we have a zero ttl from the cache refetch it. + */ +- if (!is_zone && !resuming && rdataset->ttl == 0 && ++ if (!is_zone && !resuming && !STALE(rdataset) && rdataset->ttl == 0 && + RECURSIONOK(client)) + { + if (dns_rdataset_isassociated(rdataset)) +@@ -8894,6 +9005,7 @@ query_find(ns_client_t *client, dns_fetchevent_t *event, dns_rdatatype_t qtype) + if (node != NULL) + dns_db_detachnode(db, &node); + ++ // query_respond + INSIST(!REDIRECT(client)); + result = query_recurse(client, qtype, + client->query.qname, +@@ -9174,6 +9286,7 @@ query_find(ns_client_t *client, dns_fetchevent_t *event, dns_rdatatype_t qtype) + dns_fixedname_name(&wildcardname), + true, false); + cleanup: ++ // query_done + CTRACE(ISC_LOG_DEBUG(3), "query_find: cleanup"); + /* + * General cleanup. +@@ -9230,6 +9343,49 @@ query_find(ns_client_t *client, dns_fetchevent_t *event, dns_rdatatype_t qtype) + goto restart; + } + ++ if (want_stale) { ++ dns_ttl_t stale_ttl = 0; ++ isc_result_t result; ++ bool staleanswersok = false; ++ ++ /* ++ * 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(client->view->cachedb, &db); ++ result = dns_db_getservestalettl(db, &stale_ttl); ++ if (result == ISC_R_SUCCESS && stale_ttl > 0) { ++ switch (client->view->staleanswersok) { ++ case dns_stale_answer_yes: ++ staleanswersok = true; ++ break; ++ case dns_stale_answer_conf: ++ staleanswersok = ++ client->view->staleanswersenable; ++ break; ++ case dns_stale_answer_no: ++ staleanswersok = false; ++ break; ++ } ++ } else { ++ staleanswersok = false; ++ } ++ ++ if (staleanswersok) { ++ client->query.dboptions |= DNS_DBFIND_STALEOK; ++ inc_stats(client, dns_nsstatscounter_trystale); ++ if (client->query.fetch != NULL) ++ dns_resolver_destroyfetch( ++ &client->query.fetch); ++ goto db_find; ++ } ++ dns_db_detach(&db); ++ want_stale = false; ++ QUERY_ERROR(DNS_R_SERVFAIL); ++ goto cleanup; ++ } ++ + if (eresult != ISC_R_SUCCESS && + (!PARTIALANSWER(client) || WANTRECURSION(client) + || eresult == DNS_R_DROP)) { +diff --git a/bin/named/server.c b/bin/named/server.c +index 109f0dd09f..9580d9e095 100644 +--- a/bin/named/server.c ++++ b/bin/named/server.c +@@ -1722,7 +1722,8 @@ static bool + cache_sharable(dns_view_t *originview, dns_view_t *view, + bool new_zero_no_soattl, + unsigned int new_cleaning_interval, +- uint64_t new_max_cache_size) ++ uint64_t new_max_cache_size, ++ uint32_t new_stale_ttl) + { + /* + * If the cache cannot even reused for the same view, it cannot be +@@ -1737,6 +1738,7 @@ cache_sharable(dns_view_t *originview, dns_view_t *view, + */ + if (dns_cache_getcleaninginterval(originview->cache) != + new_cleaning_interval || ++ dns_cache_getservestalettl(originview->cache) != new_stale_ttl || + dns_cache_getcachesize(originview->cache) != new_max_cache_size) { + return (false); + } +@@ -3289,6 +3291,7 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, + size_t max_acache_size; + size_t max_adb_size; + uint32_t lame_ttl, fail_ttl; ++ uint32_t max_stale_ttl; + dns_tsig_keyring_t *ring = NULL; + dns_view_t *pview = NULL; /* Production view */ + isc_mem_t *cmctx = NULL, *hmctx = NULL; +@@ -3317,6 +3320,7 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, + bool old_rpz_ok = false; + isc_dscp_t dscp4 = -1, dscp6 = -1; + dns_dyndbctx_t *dctx = NULL; ++ unsigned int resolver_param; + + REQUIRE(DNS_VIEW_VALID(view)); + +@@ -3731,6 +3735,24 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, + if (view->maxncachettl > 7 * 24 * 3600) + view->maxncachettl = 7 * 24 * 3600; + ++ obj = NULL; ++ result = ns_config_get(maps, "max-stale-ttl", &obj); ++ INSIST(result == ISC_R_SUCCESS); ++ max_stale_ttl = cfg_obj_asuint32(obj); ++ ++ obj = NULL; ++ result = ns_config_get(maps, "stale-answer-enable", &obj); ++ INSIST(result == ISC_R_SUCCESS); ++ view->staleanswersenable = cfg_obj_asboolean(obj); ++ ++ result = dns_viewlist_find(&ns_g_server->viewlist, view->name, ++ view->rdclass, &pview); ++ if (result == ISC_R_SUCCESS) { ++ view->staleanswersok = pview->staleanswersok; ++ dns_view_detach(&pview); ++ } else ++ view->staleanswersok = dns_stale_answer_conf; ++ + /* + * Configure the view's cache. + * +@@ -3764,7 +3786,8 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, + nsc = cachelist_find(cachelist, cachename, view->rdclass); + if (nsc != NULL) { + if (!cache_sharable(nsc->primaryview, view, zero_no_soattl, +- cleaning_interval, max_cache_size)) { ++ cleaning_interval, max_cache_size, ++ max_stale_ttl)) { + isc_log_write(ns_g_lctx, NS_LOGCATEGORY_GENERAL, + NS_LOGMODULE_SERVER, ISC_LOG_ERROR, + "views %s and %s can't share the cache " +@@ -3863,9 +3886,15 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, + + dns_cache_setcleaninginterval(cache, cleaning_interval); + dns_cache_setcachesize(cache, max_cache_size); ++ dns_cache_setservestalettl(cache, max_stale_ttl); + + dns_cache_detach(&cache); + ++ obj = NULL; ++ result = ns_config_get(maps, "stale-answer-ttl", &obj); ++ INSIST(result == ISC_R_SUCCESS); ++ view->staleanswerttl = ISC_MAX(cfg_obj_asuint32(obj), 1); ++ + /* + * Resolver. + * +@@ -4054,6 +4083,21 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, + maxbits = 4096; + view->maxbits = maxbits; + ++ /* ++ * Set resolver retry parameters. ++ */ ++ obj = NULL; ++ CHECK(ns_config_get(maps, "resolver-retry-interval", &obj)); ++ resolver_param = cfg_obj_asuint32(obj); ++ if (resolver_param > 0) ++ dns_resolver_setretryinterval(view->resolver, resolver_param); ++ ++ obj = NULL; ++ CHECK(ns_config_get(maps, "resolver-nonbackoff-tries", &obj)); ++ resolver_param = cfg_obj_asuint32(obj); ++ if (resolver_param > 0) ++ dns_resolver_setnonbackofftries(view->resolver, resolver_param); ++ + /* + * Set supported DNSSEC algorithms. + */ +@@ -14414,3 +14458,132 @@ ns_server_dnstap(ns_server_t *server, isc_lex_t *lex, isc_buffer_t **text) { + return (ISC_R_NOTIMPLEMENTED); + #endif + } ++ ++isc_result_t ++ns_server_servestale(ns_server_t *server, isc_lex_t *lex, ++ isc_buffer_t **text) ++{ ++ char *ptr, *classtxt, *viewtxt = NULL; ++ char msg[128]; ++ dns_rdataclass_t rdclass = dns_rdataclass_in; ++ dns_view_t *view; ++ bool found = false; ++ dns_stale_answer_t staleanswersok = dns_stale_answer_conf; ++ bool wantstatus = false; ++ isc_result_t result = ISC_R_SUCCESS; ++ ++ /* Skip the command name. */ ++ ptr = next_token(lex, text); ++ if (ptr == NULL) ++ return (ISC_R_UNEXPECTEDEND); ++ ++ ptr = next_token(lex, NULL); ++ if (ptr == NULL) ++ return (ISC_R_UNEXPECTEDEND); ++ ++ if (strcasecmp(ptr, "on") == 0 || strcasecmp(ptr, "yes") == 0) { ++ staleanswersok = dns_stale_answer_yes; ++ } else if (strcasecmp(ptr, "off") == 0 || strcasecmp(ptr, "no") == 0) { ++ staleanswersok = dns_stale_answer_no; ++ } else if (strcasecmp(ptr, "reset") == 0) { ++ staleanswersok = dns_stale_answer_conf; ++ } else if (strcasecmp(ptr, "status") == 0) { ++ wantstatus = true; ++ } else ++ return (DNS_R_SYNTAX); ++ ++ /* Look for the optional class name. */ ++ classtxt = next_token(lex, text); ++ if (classtxt != NULL) { ++ /* Look for the optional view name. */ ++ viewtxt = next_token(lex, text); ++ } ++ ++ if (classtxt != NULL) { ++ isc_textregion_t r; ++ ++ r.base = classtxt; ++ r.length = strlen(classtxt); ++ result = dns_rdataclass_fromtext(&rdclass, &r); ++ if (result != ISC_R_SUCCESS) { ++ if (viewtxt == NULL) { ++ viewtxt = classtxt; ++ classtxt = NULL; ++ result = ISC_R_SUCCESS; ++ } else { ++ snprintf(msg, sizeof(msg), ++ "unknown class '%s'", classtxt); ++ (void) putstr(text, msg); ++ goto cleanup; ++ } ++ } ++ } ++ ++ result = isc_task_beginexclusive(server->task); ++ RUNTIME_CHECK(result == ISC_R_SUCCESS); ++ ++ for (view = ISC_LIST_HEAD(server->viewlist); ++ view != NULL; ++ view = ISC_LIST_NEXT(view, link)) ++ { ++ dns_ttl_t stale_ttl = 0; ++ dns_db_t *db = NULL; ++ ++ if (classtxt != NULL && rdclass != view->rdclass) ++ continue; ++ ++ if (viewtxt != NULL && strcmp(view->name, viewtxt) != 0) ++ continue; ++ ++ if (!wantstatus) { ++ view->staleanswersok = staleanswersok; ++ found = true; ++ continue; ++ } ++ ++ db = NULL; ++ dns_db_attach(view->cachedb, &db); ++ (void)dns_db_getservestalettl(db, &stale_ttl); ++ dns_db_detach(&db); ++ if (found) ++ CHECK(putstr(text, "\n")); ++ CHECK(putstr(text, view->name)); ++ CHECK(putstr(text, ": ")); ++ switch (view->staleanswersok) { ++ case dns_stale_answer_yes: ++ if (stale_ttl > 0) ++ CHECK(putstr(text, "on (rndc)")); ++ else ++ CHECK(putstr(text, "off (not-cached)")); ++ break; ++ case dns_stale_answer_no: ++ CHECK(putstr(text, "off (rndc)")); ++ break; ++ case dns_stale_answer_conf: ++ if (view->staleanswersenable && stale_ttl > 0) ++ CHECK(putstr(text, "on")); ++ else if (view->staleanswersenable) ++ CHECK(putstr(text, "off (not-cached)")); ++ else ++ CHECK(putstr(text, "off")); ++ break; ++ } ++ if (stale_ttl > 0) { ++ snprintf(msg, sizeof(msg), ++ " (stale-answer-ttl=%u max-stale-ttl=%u)", ++ view->staleanswerttl, stale_ttl); ++ CHECK(putstr(text, msg)); ++ } ++ found = true; ++ } ++ isc_task_endexclusive(ns_g_server->task); ++ ++ if (!found) ++ result = ISC_R_NOTFOUND; ++ ++cleanup: ++ if (isc_buffer_usedlength(*text) > 0) ++ (void) putnull(text); ++ ++ return (result); ++} +diff --git a/bin/named/statschannel.c b/bin/named/statschannel.c +index 7e9ba43a73..92cfcc5e07 100644 +--- a/bin/named/statschannel.c ++++ b/bin/named/statschannel.c +@@ -295,6 +295,12 @@ init_desc(void) { + "QryNXRedirRLookup"); + SET_NSSTATDESC(badcookie, "sent badcookie response", "QryBADCOOKIE"); + SET_NSSTATDESC(keytagopt, "Keytag option received", "KeyTagOpt"); ++ SET_NSSTATDESC(trystale, ++ "attempts to use stale cache data after lookup failure", ++ "QryTryStale"); ++ SET_NSSTATDESC(usedstale, ++ "successful uses of stale cache data after lookup failure", ++ "QryUsedStale"); + INSIST(i == dns_nsstatscounter_max); + + /* Initialize resolver statistics */ +diff --git a/bin/rndc/rndc.c b/bin/rndc/rndc.c +index 8083654ac7..d519983b88 100644 +--- a/bin/rndc/rndc.c ++++ b/bin/rndc/rndc.c +@@ -160,6 +160,8 @@ command is one of the following:\n\ + scan Scan available network interfaces for changes.\n\ + secroots [view ...]\n\ + Write security roots to the secroots file.\n\ ++ serve-stale ( yes | no | reset ) [class [view]]\n\ ++ Control whether stale answers are returned\n\ + showzone zone [class [view]]\n\ + Print a zone's configuration.\n\ + sign zone [class [view]]\n\ +diff --git a/bin/rndc/rndc.docbook b/bin/rndc/rndc.docbook +index 06b073aaea..6ae8e5da6b 100644 +--- a/bin/rndc/rndc.docbook ++++ b/bin/rndc/rndc.docbook +@@ -688,6 +688,25 @@ + + + ++ ++ serve-stale ( on | off | reset | status) class view ++ ++ ++ Enable, disable, or reset the serving of stale answers ++ as configured in named.conf. Serving of stale answers ++ will remain disabled across named.conf ++ reloads if disabled via rndc until it is reset via rndc. ++ ++ ++ Status will report whether serving of stale answers is ++ currently enabled, disabled or not configured for a ++ view. If serving of stale records is configured then ++ the values of stale-answer-ttl and max-stale-ttl are ++ reported. ++ ++ ++ ++ + + secroots - view ... + +diff --git a/bin/tests/system/chain/prereq.sh b/bin/tests/system/chain/prereq.sh +index f3f1939b2a..9ff3f07941 100644 +--- a/bin/tests/system/chain/prereq.sh ++++ b/bin/tests/system/chain/prereq.sh +@@ -48,3 +48,10 @@ else + echo_i "This test requires the Net::DNS::Nameserver library." >&2 + exit 1 + fi ++if $PERL -e 'use Net::DNS::Nameserver;' 2>/dev/null ++then ++ : ++else ++ echo "I:This test requires the Net::DNS::Nameserver library." >&2 ++ exit 1 ++fi +diff --git a/bin/tests/system/conf.sh.in b/bin/tests/system/conf.sh.in +index 81b8e30236..166f0e77f0 100644 +--- a/bin/tests/system/conf.sh.in ++++ b/bin/tests/system/conf.sh.in +@@ -125,7 +125,7 @@ PARALLELDIRS="dnssec rpzrecurse \ + reclimit redirect resolver rndc rootkeysentinel rpz \ + rrchecker rrl rrsetorder rsabigexponent runtime \ + sfcache smartsign sortlist \ +- spf staticstub statistics statschannel stub \ ++ spf serve-stale staticstub statistics statschannel stub \ + tcp tsig tsiggss \ + unknown upforwd verify views wildcard \ + xfer xferquota zero zonechecks" +diff --git a/bin/tests/system/dyndb/driver/db.c b/bin/tests/system/dyndb/driver/db.c +index 02aa6ab2ef..a77c7de98f 100644 +--- a/bin/tests/system/dyndb/driver/db.c ++++ b/bin/tests/system/dyndb/driver/db.c +@@ -629,6 +629,8 @@ static dns_dbmethods_t sampledb_methods = { + hashsize, + NULL, + NULL, ++ NULL, ++ NULL, + }; + + /* Auxiliary driver functions. */ +diff --git a/bin/tests/system/serve-stale/.gitignore b/bin/tests/system/serve-stale/.gitignore +new file mode 100644 +index 0000000000..2272eef9ec +--- /dev/null ++++ b/bin/tests/system/serve-stale/.gitignore +@@ -0,0 +1,11 @@ ++/ans2/ans.pid ++/ans2/ans.pl ++/dig.out* ++/ns1/named.conf ++/ns3/named.conf ++/ns3/root.bk ++/rndc.out* ++named.lock ++named.pid ++named.port ++named.run +diff --git a/bin/tests/system/serve-stale/ans2/ans.pl.in b/bin/tests/system/serve-stale/ans2/ans.pl.in +new file mode 100644 +index 0000000000..2b39eca916 +--- /dev/null ++++ b/bin/tests/system/serve-stale/ans2/ans.pl.in +@@ -0,0 +1,178 @@ ++#!/usr/bin/env perl ++# ++# Copyright (C) 2014-2016 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/. ++ ++use strict; ++use warnings; ++ ++use IO::File; ++use IO::Socket; ++use Getopt::Long; ++use Net::DNS; ++use Time::HiRes qw(usleep nanosleep); ++ ++my $pidf = new IO::File "ans.pid", "w" or die "cannot open pid file: $!"; ++print $pidf "$$\n" or die "cannot write pid file: $!"; ++$pidf->close or die "cannot close pid file: $!"; ++sub rmpid { unlink "ans.pid"; exit 1; }; ++ ++$SIG{INT} = \&rmpid; ++$SIG{TERM} = \&rmpid; ++ ++my $send_response = 1; ++ ++my $localaddr = "10.53.0.2"; ++my $localport = @PORT@; ++my $udpsock = IO::Socket::INET->new(LocalAddr => "$localaddr", ++ LocalPort => $localport, Proto => "udp", Reuse => 1) or die "$!"; ++ ++# ++# Delegation ++# ++my $SOA = "example 300 IN SOA . . 0 0 0 0 300"; ++my $NS = "example 300 IN NS ns.example"; ++my $A = "ns.example 300 IN A $localaddr"; ++# ++# Records to be TTL stretched ++# ++my $TXT = "data.example 1 IN TXT \"A text record with a 1 second ttl\""; ++my $negSOA = "example 1 IN SOA . . 0 0 0 0 300"; ++ ++sub reply_handler { ++ my ($qname, $qclass, $qtype) = @_; ++ my ($rcode, @ans, @auth, @add); ++ ++ print ("request: $qname/$qtype\n"); ++ STDOUT->flush(); ++ ++ # Control whether we send a response or not. ++ # We always respond to control commands. ++ if ($qname eq "enable" ) { ++ if ($qtype eq "TXT") { ++ $send_response = 1; ++ my $rr = new Net::DNS::RR("$qname 0 $qclass TXT \"$send_response\""); ++ push @ans, $rr; ++ } ++ $rcode = "NOERROR"; ++ return ($rcode, \@ans, \@auth, \@add, { aa => 1 }); ++ } elsif ($qname eq "disable" ) { ++ if ($qtype eq "TXT") { ++ $send_response = 0; ++ my $rr = new Net::DNS::RR("$qname 0 $qclass TXT \"$send_response\""); ++ push @ans, $rr; ++ } ++ $rcode = "NOERROR"; ++ return ($rcode, \@ans, \@auth, \@add, { aa => 1 }); ++ } ++ ++ # If we are not responding to queries we are done. ++ return if (!$send_response); ++ ++ # Construct the response and send it. ++ if ($qname eq "ns.example" ) { ++ if ($qtype eq "A") { ++ my $rr = new Net::DNS::RR($A); ++ push @ans, $rr; ++ } else { ++ my $rr = new Net::DNS::RR($SOA); ++ push @auth, $rr; ++ } ++ $rcode = "NOERROR"; ++ } elsif ($qname eq "example") { ++ if ($qtype eq "NS") { ++ my $rr = new Net::DNS::RR($NS); ++ push @auth, $rr; ++ $rr = new Net::DNS::RR($A); ++ push @add, $rr; ++ } elsif ($qtype eq "SOA") { ++ my $rr = new Net::DNS::RR($SOA); ++ push @ans, $rr; ++ } else { ++ my $rr = new Net::DNS::RR($SOA); ++ push @auth, $rr; ++ } ++ $rcode = "NOERROR"; ++ } elsif ($qname eq "nodata.example") { ++ my $rr = new Net::DNS::RR($negSOA); ++ push @auth, $rr; ++ $rcode = "NOERROR"; ++ } elsif ($qname eq "data.example") { ++ if ($qtype eq "TXT") { ++ my $rr = new Net::DNS::RR($TXT); ++ push @ans, $rr; ++ } else { ++ my $rr = new Net::DNS::RR($negSOA); ++ push @auth, $rr; ++ } ++ $rcode = "NOERROR"; ++ } elsif ($qname eq "nxdomain.example") { ++ my $rr = new Net::DNS::RR($negSOA); ++ push @auth, $rr; ++ $rcode = "NXDOMAIN"; ++ } else { ++ my $rr = new Net::DNS::RR($SOA); ++ push @auth, $rr; ++ $rcode = "NXDOMAIN"; ++ } ++ ++ # mark the answer as authoritive (by setting the 'aa' flag ++ return ($rcode, \@ans, \@auth, \@add, { aa => 1 }); ++} ++ ++GetOptions( ++ 'port=i' => \$localport, ++); ++ ++my $rin; ++my $rout; ++ ++for (;;) { ++ $rin = ''; ++ vec($rin, fileno($udpsock), 1) = 1; ++ ++ select($rout = $rin, undef, undef, undef); ++ ++ if (vec($rout, fileno($udpsock), 1)) { ++ my ($buf, $request, $err); ++ $udpsock->recv($buf, 512); ++ ++ if ($Net::DNS::VERSION > 0.68) { ++ $request = new Net::DNS::Packet(\$buf, 0); ++ $@ and die $@; ++ } else { ++ my $err; ++ ($request, $err) = new Net::DNS::Packet(\$buf, 0); ++ $err and die $err; ++ } ++ ++ my @questions = $request->question; ++ my $qname = $questions[0]->qname; ++ my $qclass = $questions[0]->qclass; ++ my $qtype = $questions[0]->qtype; ++ my $id = $request->header->id; ++ ++ my ($rcode, $ans, $auth, $add, $headermask) = reply_handler($qname, $qclass, $qtype); ++ ++ if (!defined($rcode)) { ++ print " Silently ignoring query\n"; ++ next; ++ } ++ ++ my $reply = Net::DNS::Packet->new(); ++ $reply->header->qr(1); ++ $reply->header->aa(1) if $headermask->{'aa'}; ++ $reply->header->id($id); ++ $reply->header->rcode($rcode); ++ $reply->push("question", @questions); ++ $reply->push("answer", @$ans) if $ans; ++ $reply->push("authority", @$auth) if $auth; ++ $reply->push("additional", @$add) if $add; ++ ++ my $num_chars = $udpsock->send($reply->data); ++ print " Sent $num_chars bytes via UDP\n"; ++ } ++} +diff --git a/bin/tests/system/serve-stale/clean.sh b/bin/tests/system/serve-stale/clean.sh +new file mode 100644 +index 0000000000..2397326374 +--- /dev/null ++++ b/bin/tests/system/serve-stale/clean.sh +@@ -0,0 +1,15 @@ ++# Copyright (C) 2017 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/. ++ ++rm -f test.output ++rm -f dig.out.test* ++rm -f ans2/ans.pl ++rm -f ns3/root.bk ++rm -f rndc.out.test* ++rm -f ns*/named.memstats ++rm -f ns*/managed-keys.bind ++rm -f ns*/named.conf ++rm -f ns*/named.run +diff --git a/bin/tests/system/serve-stale/ns1/named1.conf.in b/bin/tests/system/serve-stale/ns1/named1.conf.in +new file mode 100644 +index 0000000000..8a75a10753 +--- /dev/null ++++ b/bin/tests/system/serve-stale/ns1/named1.conf.in +@@ -0,0 +1,35 @@ ++/* ++ * Copyright (C) 2017 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/. ++ */ ++ ++key rndc_key { ++ secret "1234abcd8765"; ++ algorithm hmac-sha256; ++}; ++ ++controls { ++ inet 10.53.0.1 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; ++}; ++ ++options { ++ query-source address 10.53.0.1; ++ notify-source 10.53.0.1; ++ transfer-source 10.53.0.1; ++ port @PORT@; ++ pid-file "named.pid"; ++ listen-on { 10.53.0.1; }; ++ listen-on-v6 { none; }; ++ recursion yes; ++ max-stale-ttl 3600; ++ stale-answer-ttl 1; ++ stale-answer-enable yes; ++}; ++ ++zone "." { ++ type master; ++ file "root.db"; ++}; +diff --git a/bin/tests/system/serve-stale/ns1/named2.conf.in b/bin/tests/system/serve-stale/ns1/named2.conf.in +new file mode 100644 +index 0000000000..072e6ec40d +--- /dev/null ++++ b/bin/tests/system/serve-stale/ns1/named2.conf.in +@@ -0,0 +1,35 @@ ++/* ++ * Copyright (C) 2017 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/. ++ */ ++ ++key rndc_key { ++ secret "1234abcd8765"; ++ algorithm hmac-sha256; ++}; ++ ++controls { ++ inet 10.53.0.1 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; ++}; ++ ++options { ++ query-source address 10.53.0.1; ++ notify-source 10.53.0.1; ++ transfer-source 10.53.0.1; ++ port @PORT@; ++ pid-file "named.pid"; ++ listen-on { 10.53.0.1; }; ++ listen-on-v6 { none; }; ++ recursion yes; ++ max-stale-ttl 7200; ++ stale-answer-ttl 2; ++ stale-answer-enable yes; ++}; ++ ++zone "." { ++ type master; ++ file "root.db"; ++}; +diff --git a/bin/tests/system/serve-stale/ns1/root.db b/bin/tests/system/serve-stale/ns1/root.db +new file mode 100644 +index 0000000000..eb9ad3ecf1 +--- /dev/null ++++ b/bin/tests/system/serve-stale/ns1/root.db +@@ -0,0 +1,5 @@ ++. 300 SOA . . 0 0 0 0 0 ++. 300 NS ns.nil. ++ns.nil. 300 A 10.53.0.1 ++example. 300 NS ns.example. ++ns.example. 300 A 10.53.0.2 +diff --git a/bin/tests/system/serve-stale/ns3/named.conf.in b/bin/tests/system/serve-stale/ns3/named.conf.in +new file mode 100644 +index 0000000000..24a3293fb9 +--- /dev/null ++++ b/bin/tests/system/serve-stale/ns3/named.conf.in +@@ -0,0 +1,35 @@ ++/* ++ * Copyright (C) 2017 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/. ++ */ ++ ++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; }; ++ recursion yes; ++ // max-stale-ttl 3600; ++ // stale-answer-ttl 3; ++}; ++ ++zone "." { ++ type slave; ++ masters { 10.53.0.1; }; ++ file "root.bk"; ++}; +diff --git a/bin/tests/system/serve-stale/prereq.sh b/bin/tests/system/serve-stale/prereq.sh +new file mode 100644 +index 0000000000..a3bbef8f03 +--- /dev/null ++++ b/bin/tests/system/serve-stale/prereq.sh +@@ -0,0 +1,38 @@ ++#!/bin/sh ++# ++# Copyright (C) 2011, 2012, 2014, 2016 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/. ++ ++SYSTEMTESTTOP=.. ++. $SYSTEMTESTTOP/conf.sh ++ ++if $PERL -e 'use Net::DNS;' 2>/dev/null ++then ++ if $PERL -e 'use Net::DNS; die if ($Net::DNS::VERSION >= 0.69 && $Net::DNS::VERSION <= 0.74);' 2>/dev/null ++ then ++ : ++ else ++ echo "I:Net::DNS versions 0.69 to 0.74 have bugs that cause this test to fail: please update." >&2 ++ exit 1 ++ fi ++else ++ echo "I:This test requires the Net::DNS library." >&2 ++ exit 1 ++fi ++if $PERL -e 'use Net::DNS::Nameserver;' 2>/dev/null ++then ++ : ++else ++ echo "I:This test requires the Net::DNS::Nameserver library." >&2 ++ exit 1 ++fi ++if $PERL -e 'use Time::HiRes;' 2>/dev/null ++then ++ : ++else ++ echo "I:This test requires the Time::HiRes library." >&2 ++ exit 1 ++fi +diff --git a/bin/tests/system/serve-stale/setup.sh b/bin/tests/system/serve-stale/setup.sh +new file mode 100644 +index 0000000000..690f43c813 +--- /dev/null ++++ b/bin/tests/system/serve-stale/setup.sh +@@ -0,0 +1,13 @@ ++#!/bin/sh ++# Copyright (C) 2017 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/. ++ ++SYSTEMTESTTOP=.. ++. $SYSTEMTESTTOP/conf.sh ++ ++copy_setports ns1/named1.conf.in ns1/named.conf ++copy_setports ans2/ans.pl.in ans2/ans.pl ++copy_setports ns3/named.conf.in ns3/named.conf +diff --git a/bin/tests/system/serve-stale/tests.sh b/bin/tests/system/serve-stale/tests.sh +new file mode 100755 +index 0000000000..201c996921 +--- /dev/null ++++ b/bin/tests/system/serve-stale/tests.sh +@@ -0,0 +1,536 @@ ++#!/bin/sh ++# ++# Copyright (C) 2000, 2001, 2004, 2007, 2009-2016 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/. ++ ++SYSTEMTESTTOP=.. ++. $SYSTEMTESTTOP/conf.sh ++ ++while getopts "p:c:" flag; do ++ case "$flag" in ++ p) port=$OPTARG ;; ++ c) controlport=$OPTARG ;; ++ *) exit 1 ;; ++ esac ++done ++ ++RNDCCMD="$RNDC -c $SYSTEMTESTTOP/common/rndc.conf -p ${CONTROLPORT} -s" ++ ++echo "RNDCCMD: ${RNDCCMD}" ++ ++status=0 ++n=0 ++ ++#echo "I:check ans.pl server ($n)" ++#$DIG -p ${PORT} @10.53.0.2 example NS ++#$DIG -p ${PORT} @10.53.0.2 example SOA ++#$DIG -p ${PORT} @10.53.0.2 ns.example A ++#$DIG -p ${PORT} @10.53.0.2 ns.example AAAA ++#$DIG -p ${PORT} @10.53.0.2 txt enable ++#$DIG -p ${PORT} @10.53.0.2 txt disable ++#$DIG -p ${PORT} @10.53.0.2 ns.example AAAA ++#$DIG -p ${PORT} @10.53.0.2 txt enable ++#$DIG -p ${PORT} @10.53.0.2 ns.example AAAA ++##$DIG -p ${PORT} @10.53.0.2 data.example TXT ++#$DIG -p ${PORT} @10.53.0.2 nodata.example TXT ++#$DIG -p ${PORT} @10.53.0.2 nxdomain.example TXT ++ ++n=`expr $n + 1` ++echo "I:prime cache data.example ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.1 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=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:prime cache nodata.example ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.1 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=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:prime cache nxdomain.example ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.1 nxdomain.example TXT > dig.out.test$n ++grep "status: NXDOMAIN" 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=`expr $status + $ret` ++ ++n=`expr $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=`expr $status + $ret` ++ ++sleep 1 ++ ++n=`expr $n + 1` ++echo "I:check 'rndc serve-stale status' ($n)" ++ret=0 ++$RNDCCMD 10.53.0.1 serve-stale status > rndc.out.test$n 2>&1 || ret=1 ++grep '_default: on (stale-answer-ttl=1 max-stale-ttl=3600)' rndc.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check stale data.example ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.1 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 "example.*1.*IN" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check stale nodata.example ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.1 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 ++grep "example.*1.*IN" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check stale nxdomain.example ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.1 nxdomain.example TXT > dig.out.test$n ++grep "status: NXDOMAIN" dig.out.test$n > /dev/null || ret=1 ++grep "ANSWER: 0," dig.out.test$n > /dev/null || ret=1 ++grep "example.*1.*IN" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:running 'rndc serve-stale off' ($n)" ++ret=0 ++$RNDCCMD 10.53.0.1 serve-stale off || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check 'rndc serve-stale status' ($n)" ++ret=0 ++$RNDCCMD 10.53.0.1 serve-stale status > rndc.out.test$n 2>&1 || ret=1 ++grep '_default: off (rndc) (stale-answer-ttl=1 max-stale-ttl=3600)' rndc.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check stale data.example (serve-stale off) ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.1 data.example TXT > dig.out.test$n ++grep "status: SERVFAIL" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check stale nodata.example (serve-stale off) ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.1 nodata.example TXT > dig.out.test$n ++grep "status: SERVFAIL" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check stale nxdomain.example (serve-stale off) ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.1 nxdomain.example TXT > dig.out.test$n ++grep "status: SERVFAIL" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:running 'rndc serve-stale on' ($n)" ++ret=0 ++$RNDCCMD 10.53.0.1 serve-stale on || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check 'rndc serve-stale status' ($n)" ++ret=0 ++$RNDCCMD 10.53.0.1 serve-stale status > rndc.out.test$n 2>&1 || ret=1 ++grep '_default: on (rndc) (stale-answer-ttl=1 max-stale-ttl=3600)' rndc.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check stale data.example (serve-stale on) ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.1 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 "example.*1.*IN" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check stale nodata.example (serve-stale on) ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.1 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 ++grep "example.*1.*IN" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check stale nxdomain.example (serve-stale on) ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.1 nxdomain.example TXT > dig.out.test$n ++grep "status: NXDOMAIN" dig.out.test$n > /dev/null || ret=1 ++grep "ANSWER: 0," dig.out.test$n > /dev/null || ret=1 ++grep "example.*1.*IN" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:running 'rndc serve-stale no' ($n)" ++ret=0 ++$RNDCCMD 10.53.0.1 serve-stale no || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check 'rndc serve-stale status' ($n)" ++ret=0 ++$RNDCCMD 10.53.0.1 serve-stale status > rndc.out.test$n 2>&1 || ret=1 ++grep '_default: off (rndc) (stale-answer-ttl=1 max-stale-ttl=3600)' rndc.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check stale data.example (serve-stale no) ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.1 data.example TXT > dig.out.test$n ++grep "status: SERVFAIL" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check stale nodata.example (serve-stale no) ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.1 nodata.example TXT > dig.out.test$n ++grep "status: SERVFAIL" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check stale nxdomain.example (serve-stale no) ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.1 nxdomain.example TXT > dig.out.test$n ++grep "status: SERVFAIL" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:running 'rndc serve-stale yes' ($n)" ++ret=0 ++$RNDCCMD 10.53.0.1 serve-stale yes || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check 'rndc serve-stale status' ($n)" ++ret=0 ++$RNDCCMD 10.53.0.1 serve-stale status > rndc.out.test$n 2>&1 || ret=1 ++grep '_default: on (rndc) (stale-answer-ttl=1 max-stale-ttl=3600)' rndc.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check stale data.example (serve-stale yes) ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.1 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 "example.*1.*IN" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check stale nodata.example (serve-stale yes) ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.1 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 ++grep "example.*1.*IN" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check stale nxdomain.example (serve-stale yes) ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.1 nxdomain.example TXT > dig.out.test$n ++grep "status: NXDOMAIN" dig.out.test$n > /dev/null || ret=1 ++grep "ANSWER: 0," dig.out.test$n > /dev/null || ret=1 ++grep "example.*1.*IN" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:running 'rndc serve-stale off' ($n)" ++ret=0 ++$RNDCCMD 10.53.0.1 serve-stale off || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:running 'rndc serve-stale reset' ($n)" ++ret=0 ++$RNDCCMD 10.53.0.1 serve-stale reset || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check 'rndc serve-stale status' ($n)" ++ret=0 ++$RNDCCMD 10.53.0.1 serve-stale status > rndc.out.test$n 2>&1 || ret=1 ++grep '_default: on (stale-answer-ttl=1 max-stale-ttl=3600)' rndc.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check stale data.example (serve-stale reset) ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.1 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 "example.*1.*IN" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check stale nodata.example (serve-stale reset) ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.1 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 ++grep "example.*1.*IN" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check stale nxdomain.example (serve-stale reset) ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.1 nxdomain.example TXT > dig.out.test$n ++grep "status: NXDOMAIN" dig.out.test$n > /dev/null || ret=1 ++grep "ANSWER: 0," dig.out.test$n > /dev/null || ret=1 ++grep "example.*1.*IN" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:running 'rndc serve-stale off' ($n)" ++ret=0 ++$RNDCCMD 10.53.0.1 serve-stale off || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check 'rndc serve-stale status' ($n)" ++ret=0 ++$RNDCCMD 10.53.0.1 serve-stale status > rndc.out.test$n 2>&1 || ret=1 ++grep '_default: off (rndc) (stale-answer-ttl=1 max-stale-ttl=3600)' rndc.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:updating ns1/named.conf ($n)" ++ret=0 ++sed -e "s/@PORT@/${PORT}/g;s/@CONTROLPORT@/${CONTROLPORT}/g" < ns1/named2.conf.in > ns1/named.conf ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:running 'rndc reload' ($n)" ++ret=0 ++$RNDCCMD 10.53.0.1 reload > rndc.out.test$n 2>&1 || ret=1 ++grep "server reload successful" rndc.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check 'rndc serve-stale status' ($n)" ++ret=0 ++$RNDCCMD 10.53.0.1 serve-stale status > rndc.out.test$n 2>&1 || ret=1 ++grep '_default: off (rndc) (stale-answer-ttl=2 max-stale-ttl=7200)' rndc.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check 'rndc serve-stale' ($n)" ++ret=0 ++$RNDCCMD 10.53.0.1 serve-stale > rndc.out.test$n 2>&1 && ret=1 ++grep "unexpected end of input" rndc.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check 'rndc serve-stale unknown' ($n)" ++ret=0 ++$RNDCCMD 10.53.0.1 serve-stale unknown > rndc.out.test$n 2>&1 && ret=1 ++grep "syntax error" rndc.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo_i "flush cache, re-enable serve-stale and query again ($n)" ++ret=0 ++$RNDCCMD 10.53.0.1 flushtree example > rndc.out.test$n.1 2>&1 || ret=1 ++$RNDCCMD 10.53.0.1 serve-stale on > rndc.out.test$n.2 2>&1 || ret=1 ++$DIG -p ${PORT} @10.53.0.1 data.example TXT > dig.out.test$n ++grep "status: SERVFAIL" 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=`expr $status + $ret` ++ ++n=`expr $n + 1` ++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=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:prime cache data.example (max-stale-ttl default) ($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 "example.*1.*IN" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:prime cache nodata.example (max-stale-ttl default) ($n)" ++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 ++grep "example.*1.*IN" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:prime cache nxdomain.example (max-stale-ttl default) ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.3 nxdomain.example TXT > dig.out.test$n ++grep "status: NXDOMAIN" dig.out.test$n > /dev/null || ret=1 ++grep "ANSWER: 0," dig.out.test$n > /dev/null || ret=1 ++grep "example.*1.*IN" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $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=`expr $status + $ret` ++ ++sleep 1 ++ ++n=`expr $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: off (stale-answer-ttl=1 max-stale-ttl=604800)' rndc.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check fail of data.example (max-stale-ttl default) ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.3 data.example TXT > dig.out.test$n ++grep "status: SERVFAIL" 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=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check fail of nodata.example (max-stale-ttl default) ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.3 nodata.example TXT > dig.out.test$n ++grep "status: SERVFAIL" 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=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check fail of nxdomain.example (max-stale-ttl default) ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.3 nxdomain.example TXT > dig.out.test$n ++grep "status: SERVFAIL" 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=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check 'rndc serve-stale on' ($n)" ++ret=0 ++$RNDCCMD 10.53.0.3 serve-stale on > rndc.out.test$n 2>&1 || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $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 (rndc) (stale-answer-ttl=1 max-stale-ttl=604800)' rndc.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check data.example (max-stale-ttl default) ($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 "example.*1.*IN" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check nodata.example (max-stale-ttl default) ($n)" ++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 ++grep "example.*1.*IN" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++n=`expr $n + 1` ++echo "I:check nxdomain.example (max-stale-ttl default) ($n)" ++ret=0 ++$DIG -p ${PORT} @10.53.0.3 nxdomain.example TXT > dig.out.test$n ++grep "status: NXDOMAIN" dig.out.test$n > /dev/null || ret=1 ++grep "ANSWER: 0," dig.out.test$n > /dev/null || ret=1 ++grep "example.*1.*IN" dig.out.test$n > /dev/null || ret=1 ++if [ $ret != 0 ]; then echo "I:failed"; fi ++status=`expr $status + $ret` ++ ++echo "I:exit status: $status" ++[ $status -eq 0 ] || exit 1 +diff --git a/doc/arm/Bv9ARM-book.xml b/doc/arm/Bv9ARM-book.xml +index c161e71058..ec1f6f591d 100644 +--- a/doc/arm/Bv9ARM-book.xml ++++ b/doc/arm/Bv9ARM-book.xml +@@ -4376,6 +4376,9 @@ badresp:1,adberr:0,findfail:0,valfail:0] + statement in the named.conf file: + + ++ [ max-stale-ttl number ; ] ++ [ stale-answer-enable yes_or_no ; ] ++ [ stale-answer-ttl number ; ] + + +
<command>options</command> Statement Definition and +@@ -4469,6 +4472,7 @@ badresp:1,adberr:0,findfail:0,valfail:0] + <command>dnssec-validation</command>, + <command>max-cache-ttl</command>, + <command>max-ncache-ttl</command>, ++ <command>max-stale-ttl</command>, + <command>max-cache-size</command>, and + <command>zero-no-soa-ttl</command>. + </para> +@@ -5480,7 +5484,6 @@ options { + </listitem> + </varlistentry> + +- + <varlistentry> + <term><command>max-zone-ttl</command></term> + <listitem> +@@ -5516,6 +5519,21 @@ options { + </listitem> + </varlistentry> + ++ <varlistentry> ++ <term><command>stale-answer-ttl</command></term> ++ <listitem> ++ <para> ++ Specifies the TTL to be returned on stale answers. ++ The default is 1 second. The minimal allowed is ++ also 1 second; a value of 0 will be updated silently ++ to 1 second. For stale answers to be returned ++ <option>max-stale-ttl</option> must be set to a ++ non zero value and they must not have been disabled ++ by <command>rndc</command>. ++ </para> ++ </listitem> ++ </varlistentry> ++ + <varlistentry> + <term><command>serial-update-method</command></term> + <listitem> +@@ -6257,6 +6275,22 @@ options { + </listitem> + </varlistentry> + ++ <varlistentry> ++ <term><command>serve-stale-enable</command></term> ++ <listitem> ++ <para> ++ Enable the returning of stale answers when the ++ nameservers for the zone are not answering. This ++ is off by default but can be enabled/disabled via ++ <command>rndc server-stale on</command> and ++ <command>rndc server-stale off</command> which ++ override the named.conf setting. <command>rndc ++ server-stale reset</command> will restore control ++ via named.conf. ++ </para> ++ </listitem> ++ </varlistentry> ++ + <varlistentry> + <term><command>nocookie-udp-size</command></term> + <listitem> +@@ -7465,14 +7499,20 @@ options { + <term><command>resolver-query-timeout</command></term> + <listitem> + <para> +- The amount of time in seconds that the resolver ++ The amount of time in milliseconds that the resolver + will spend attempting to resolve a recursive + query before failing. The default and minimum +- is <literal>10</literal> and the maximum is +- <literal>30</literal>. Setting it to ++ is <literal>10000</literal> and the maximum is ++ <literal>30000</literal>. Setting it to + <literal>0</literal> will result in the default + being used. + </para> ++ <para> ++ This value was originally specified in seconds. ++ Values less than or equal to 300 will be be treated ++ as seconds and converted to milliseconds before ++ applying the above limits. ++ </para> + </listitem> + </varlistentry> + </variablelist> +@@ -8956,6 +8996,27 @@ avoid-v6-udp-ports { 40000; range 50000 60000; }; + </listitem> + </varlistentry> + ++ <varlistentry> ++ <term><command>max-stale-ttl</command></term> ++ <listitem> ++ <para> ++ Sets the maximum time for which the server will ++ retain records past their normal expiry to ++ return them as stale records when the servers ++ for those records are not reachable. The default ++ is to not retain the record. ++ </para> ++ <para> ++ <command>rndc serve-stale</command> can be used ++ to disable and re-enable the serving of stale ++ records at runtime. Reloading or reconfiguring ++ <command>named</command> will not re-enable serving ++ of stale records if they have been disabled via ++ <command>rndc</command>. ++ </para> ++ </listitem> ++ </varlistentry> ++ + <varlistentry> + <term><command>min-roots</command></term> + <listitem> +diff --git a/doc/arm/logging-categories.xml b/doc/arm/logging-categories.xml +index 181def7077..59f6afb049 100644 +--- a/doc/arm/logging-categories.xml ++++ b/doc/arm/logging-categories.xml +@@ -311,6 +311,17 @@ + </para> + </entry> + </row> ++ <row rowsep="0"> ++ <entry colname="1"> ++ <para><command>serve-stale</command></para> ++ </entry> ++ <entry colname="2"> ++ <para> ++ Whether or not a stale answer is used ++ following a resolver failure. ++ </para> ++ </entry> ++ </row> + <row rowsep="0"> + <entry colname="1"> + <para><command>spill</command></para> +diff --git a/doc/arm/notes-rh-changes.xml b/doc/arm/notes-rh-changes.xml +index 11c3a7ccd5..ba3c2cce9c 100644 +--- a/doc/arm/notes-rh-changes.xml ++++ b/doc/arm/notes-rh-changes.xml +@@ -13,6 +13,9 @@ + <section xml:id="relnotes_rh_changes"><info><title>Red Hat Specific Changes + + ++ ++ This version includes some features not present in releases by ISC. ++ + + By default, BIND now uses the random number generation functions + in the cryptographic library (i.e., OpenSSL or a PKCS#11 +@@ -37,7 +40,16 @@ + case /dev/random will be the default + entropy source. [RT #31459] [RT #46047] + +- ++ ++ When acting as a recursive resolver, named ++ can now continue returning answers whose TTLs have expired ++ when the authoritative server is under attack and unable to ++ respond. This is controlled by the ++ stale-answer-enable, ++ stale-answer-ttl and ++ max-stale-ttl options. [RT #44790] ++ ++ + +
+ +diff --git a/doc/misc/options b/doc/misc/options +index e11beed292..fde93c7093 100644 +--- a/doc/misc/options ++++ b/doc/misc/options +@@ -225,6 +225,7 @@ options { + max-refresh-time ; + max-retry-time ; + max-rsa-exponent-size ; ++ max-stale-ttl ; + max-transfer-idle-in ; + max-transfer-idle-out ; + max-transfer-time-in ; +@@ -298,7 +299,9 @@ options { + request-sit ; // obsolete + require-server-cookie ; + reserved-sockets ; ++ resolver-nonbackoff-tries ; + resolver-query-timeout ; ++ resolver-retry-interval ; + response-policy { zone [ log ] [ max-policy-ttl + ] [ policy ( cname | disabled | drop | given | no-op + | nodata | nxdomain | passthru | tcp-only ) ] [ +@@ -328,6 +331,8 @@ options { + sit-secret ; // obsolete + sortlist { ; ... }; + stacksize ( default | unlimited | ); ++ stale-answer-enable ; ++ stale-answer-ttl ; + startup-notify-rate ; + statistics-file ; + statistics-interval ; // not yet implemented +@@ -539,6 +544,7 @@ view [ ] { + max-recursion-queries ; + max-refresh-time ; + max-retry-time ; ++ max-stale-ttl ; + max-transfer-idle-in ; + max-transfer-idle-out ; + max-transfer-time-in ; +@@ -600,7 +606,9 @@ view [ ] { + request-nsid ; + request-sit ; // obsolete + require-server-cookie ; ++ resolver-nonbackoff-tries ; + resolver-query-timeout ; ++ resolver-retry-interval ; + response-policy { zone [ log ] [ max-policy-ttl + ] [ policy ( cname | disabled | drop | given | no-op + | nodata | nxdomain | passthru | tcp-only ) ] [ +@@ -655,6 +663,8 @@ view [ ] { + sig-signing-type ; + sig-validity-interval [ ]; + sortlist { ; ... }; ++ stale-answer-enable ; ++ stale-answer-ttl ; + suppress-initial-notify ; // not yet implemented + topology { ; ... }; // not implemented + transfer-format ( many-answers | one-answer ); +diff --git a/lib/bind9/check.c b/lib/bind9/check.c +index e0803d4fa6..296e364bd9 100644 +--- a/lib/bind9/check.c ++++ b/lib/bind9/check.c +@@ -99,7 +99,8 @@ check_orderent(const cfg_obj_t *ent, isc_log_t *logctx) { + cfg_obj_log(obj, logctx, ISC_LOG_ERROR, + "rrset-order: invalid class '%s'", + r.base); +- result = ISC_R_FAILURE; ++ if (result == ISC_R_SUCCESS) ++ result = ISC_R_FAILURE; + } + } + +@@ -112,7 +113,8 @@ check_orderent(const cfg_obj_t *ent, isc_log_t *logctx) { + cfg_obj_log(obj, logctx, ISC_LOG_ERROR, + "rrset-order: invalid type '%s'", + r.base); +- result = ISC_R_FAILURE; ++ if (result == ISC_R_SUCCESS) ++ result = ISC_R_FAILURE; + } + } + +@@ -126,7 +128,8 @@ check_orderent(const cfg_obj_t *ent, isc_log_t *logctx) { + if (tresult != ISC_R_SUCCESS) { + cfg_obj_log(obj, logctx, ISC_LOG_ERROR, + "rrset-order: invalid name '%s'", str); +- result = ISC_R_FAILURE; ++ if (result == ISC_R_SUCCESS) ++ result = ISC_R_FAILURE; + } + } + +@@ -135,14 +138,16 @@ check_orderent(const cfg_obj_t *ent, isc_log_t *logctx) { + strcasecmp("order", cfg_obj_asstring(obj)) != 0) { + cfg_obj_log(ent, logctx, ISC_LOG_ERROR, + "rrset-order: keyword 'order' missing"); +- result = ISC_R_FAILURE; ++ if (result == ISC_R_SUCCESS) ++ result = ISC_R_FAILURE; + } + + obj = cfg_tuple_get(ent, "ordering"); + if (!cfg_obj_isstring(obj)) { + cfg_obj_log(ent, logctx, ISC_LOG_ERROR, + "rrset-order: missing ordering"); +- result = ISC_R_FAILURE; ++ if (result == ISC_R_SUCCESS) ++ result = ISC_R_FAILURE; + } else if (strcasecmp(cfg_obj_asstring(obj), "fixed") == 0) { + #if !DNS_RDATASET_FIXED + cfg_obj_log(obj, logctx, ISC_LOG_WARNING, +@@ -154,7 +159,8 @@ check_orderent(const cfg_obj_t *ent, isc_log_t *logctx) { + cfg_obj_log(obj, logctx, ISC_LOG_ERROR, + "rrset-order: invalid order '%s'", + cfg_obj_asstring(obj)); +- result = ISC_R_FAILURE; ++ if (result == ISC_R_SUCCESS) ++ result = ISC_R_FAILURE; + } + return (result); + } +@@ -174,7 +180,7 @@ check_order(const cfg_obj_t *options, isc_log_t *logctx) { + element = cfg_list_next(element)) + { + tresult = check_orderent(cfg_listelt_value(element), logctx); +- if (tresult != ISC_R_SUCCESS) ++ if (result == ISC_R_SUCCESS && tresult != ISC_R_SUCCESS) + result = tresult; + } + return (result); +@@ -204,7 +210,8 @@ check_dual_stack(const cfg_obj_t *options, isc_log_t *logctx) { + if (val > UINT16_MAX) { + cfg_obj_log(obj, logctx, ISC_LOG_ERROR, + "port '%u' out of range", val); +- result = ISC_R_FAILURE; ++ if (result == ISC_R_SUCCESS) ++ result = ISC_R_RANGE; + } + } + obj = cfg_tuple_get(alternates, "addresses"); +@@ -224,7 +231,8 @@ check_dual_stack(const cfg_obj_t *options, isc_log_t *logctx) { + if (tresult != ISC_R_SUCCESS) { + cfg_obj_log(obj, logctx, ISC_LOG_ERROR, + "bad name '%s'", str); +- result = ISC_R_FAILURE; ++ if (result == ISC_R_SUCCESS) ++ result = tresult; + } + obj = cfg_tuple_get(value, "port"); + if (cfg_obj_isuint32(obj)) { +@@ -232,7 +240,8 @@ check_dual_stack(const cfg_obj_t *options, isc_log_t *logctx) { + if (val > UINT16_MAX) { + cfg_obj_log(obj, logctx, ISC_LOG_ERROR, + "port '%u' out of range", val); +- result = ISC_R_FAILURE; ++ if (result == ISC_R_SUCCESS) ++ result = ISC_R_RANGE; + } + } + } +@@ -1267,7 +1276,8 @@ check_options(const cfg_obj_t *options, isc_log_t *logctx, isc_mem_t *mctx, + cfg_obj_log(obj, logctx, ISC_LOG_ERROR, + "auto-dnssec may only be activated at the " + "zone level"); +- result = ISC_R_FAILURE; ++ if (result == ISC_R_SUCCESS) ++ result = ISC_R_FAILURE; + } + } + +@@ -1287,7 +1297,7 @@ check_options(const cfg_obj_t *options, isc_log_t *logctx, isc_mem_t *mctx, + { + obj = cfg_listelt_value(element); + tresult = mustbesecure(obj, symtab, logctx, mctx); +- if (tresult != ISC_R_SUCCESS) ++ if (result == ISC_R_SUCCESS && tresult != ISC_R_SUCCESS) + result = tresult; + } + if (symtab != NULL) +@@ -1306,7 +1316,8 @@ check_options(const cfg_obj_t *options, isc_log_t *logctx, isc_mem_t *mctx, + cfg_obj_log(obj, logctx, ISC_LOG_ERROR, + "%s: invalid name '%s'", + server_contact[i], str); +- result = ISC_R_FAILURE; ++ if (result == ISC_R_SUCCESS) ++ result = ISC_R_FAILURE; + } + } + } +@@ -1326,7 +1337,8 @@ check_options(const cfg_obj_t *options, isc_log_t *logctx, isc_mem_t *mctx, + cfg_obj_log(obj, logctx, ISC_LOG_ERROR, + "disable-empty-zone: invalid name '%s'", + str); +- result = ISC_R_FAILURE; ++ if (result == ISC_R_SUCCESS) ++ result = ISC_R_FAILURE; + } + } + +@@ -1340,11 +1352,12 @@ check_options(const cfg_obj_t *options, isc_log_t *logctx, isc_mem_t *mctx, + strlen(cfg_obj_asstring(obj)) > 1024U) { + cfg_obj_log(obj, logctx, ISC_LOG_ERROR, + "'server-id' too big (>1024 bytes)"); +- result = ISC_R_FAILURE; ++ if (result == ISC_R_SUCCESS) ++ result = ISC_R_FAILURE; + } + + tresult = check_dscp(options, logctx); +- if (tresult != ISC_R_SUCCESS) ++ if (result == ISC_R_SUCCESS && tresult != ISC_R_SUCCESS) + result = tresult; + + obj = NULL; +@@ -1354,11 +1367,13 @@ check_options(const cfg_obj_t *options, isc_log_t *logctx, isc_mem_t *mctx, + if (lifetime > 604800) { /* 7 days */ + cfg_obj_log(obj, logctx, ISC_LOG_ERROR, + "'nta-lifetime' cannot exceed one week"); +- result = ISC_R_RANGE; ++ if (result == ISC_R_SUCCESS) ++ result = ISC_R_RANGE; + } else if (lifetime == 0) { + cfg_obj_log(obj, logctx, ISC_LOG_ERROR, + "'nta-lifetime' may not be zero"); +- result = ISC_R_RANGE; ++ if (result == ISC_R_SUCCESS) ++ result = ISC_R_RANGE; + } + } + +@@ -1369,7 +1384,8 @@ check_options(const cfg_obj_t *options, isc_log_t *logctx, isc_mem_t *mctx, + if (recheck > 604800) { /* 7 days */ + cfg_obj_log(obj, logctx, ISC_LOG_ERROR, + "'nta-recheck' cannot exceed one week"); +- result = ISC_R_RANGE; ++ if (result == ISC_R_SUCCESS) ++ result = ISC_R_RANGE; + } + + if (recheck > lifetime) +@@ -1387,7 +1403,8 @@ check_options(const cfg_obj_t *options, isc_log_t *logctx, isc_mem_t *mctx, + if (strcasecmp(ccalg, "aes") == 0) { + cfg_obj_log(obj, logctx, ISC_LOG_ERROR, + "cookie-algorithm: '%s' not supported", ccalg); +- result = ISC_R_NOTIMPLEMENTED; ++ if (result == ISC_R_SUCCESS) ++ result = ISC_R_NOTIMPLEMENTED; + } + #endif + +@@ -1476,7 +1493,8 @@ check_options(const cfg_obj_t *options, isc_log_t *logctx, isc_mem_t *mctx, + cfg_obj_log(obj, logctx, ISC_LOG_ERROR, + "%s out of range (%u < %u)", + fstrm[i].name, value, fstrm[i].min); +- result = ISC_R_RANGE; ++ if (result == ISC_R_SUCCESS) ++ result = ISC_R_RANGE; + } + + if (strcmp(fstrm[i].name, "fstrm-set-input-queue-size") == 0) { +@@ -1490,7 +1508,8 @@ check_options(const cfg_obj_t *options, isc_log_t *logctx, isc_mem_t *mctx, + "%s '%u' not a power-of-2", + fstrm[i].name, + cfg_obj_asuint32(obj)); +- result = ISC_R_RANGE; ++ if (result == ISC_R_SUCCESS) ++ result = ISC_R_RANGE; + } + } + } +@@ -1523,7 +1542,8 @@ check_options(const cfg_obj_t *options, isc_log_t *logctx, isc_mem_t *mctx, + "%" PRId64 "' " + "is too small", + mapsize); +- return (ISC_R_RANGE); ++ if (result == ISC_R_SUCCESS) ++ result = ISC_R_RANGE; + } else if (mapsize > (1ULL << 40)) { /* 1 terabyte */ + cfg_obj_log(obj, logctx, + ISC_LOG_ERROR, +@@ -1531,10 +1551,20 @@ check_options(const cfg_obj_t *options, isc_log_t *logctx, isc_mem_t *mctx, + "%" PRId64 "' " + "is too large", + mapsize); +- return (ISC_R_RANGE); ++ if (result == ISC_R_SUCCESS) ++ result = ISC_R_RANGE; + } + } + ++ obj = NULL; ++ (void)cfg_map_get(options, "resolver-nonbackoff-tries", &obj); ++ if (obj != NULL && cfg_obj_asuint32(obj) == 0U) { ++ cfg_obj_log(obj, logctx, ISC_LOG_ERROR, ++ "'resolver-nonbackoff-tries' must be >= 1"); ++ if (result == ISC_R_SUCCESS) ++ result = ISC_R_RANGE; ++ } ++ + return (result); + } + +diff --git a/lib/dns/cache.c b/lib/dns/cache.c +index 4701ff8574..97e427a53c 100644 +--- a/lib/dns/cache.c ++++ b/lib/dns/cache.c +@@ -138,6 +138,7 @@ struct dns_cache { + int db_argc; + char **db_argv; + size_t size; ++ dns_ttl_t serve_stale_ttl; + isc_stats_t *stats; + + /* Locked by 'filelock'. */ +@@ -167,9 +168,13 @@ overmem_cleaning_action(isc_task_t *task, isc_event_t *event); + + static inline isc_result_t + cache_create_db(dns_cache_t *cache, dns_db_t **db) { +- return (dns_db_create(cache->mctx, cache->db_type, dns_rootname, +- dns_dbtype_cache, cache->rdclass, +- cache->db_argc, cache->db_argv, db)); ++ isc_result_t result; ++ result = dns_db_create(cache->mctx, cache->db_type, dns_rootname, ++ dns_dbtype_cache, cache->rdclass, ++ cache->db_argc, cache->db_argv, db); ++ if (result == ISC_R_SUCCESS) ++ dns_db_setservestalettl(*db, cache->serve_stale_ttl); ++ return (result); + } + + isc_result_t +@@ -238,6 +243,7 @@ dns_cache_create3(isc_mem_t *cmctx, isc_mem_t *hmctx, isc_taskmgr_t *taskmgr, + cache->references = 1; + cache->live_tasks = 0; + cache->rdclass = rdclass; ++ cache->serve_stale_ttl = 0; + + cache->stats = NULL; + result = isc_stats_create(cmctx, &cache->stats, +@@ -1092,6 +1098,32 @@ dns_cache_getcachesize(dns_cache_t *cache) { + return (size); + } + ++void ++dns_cache_setservestalettl(dns_cache_t *cache, dns_ttl_t ttl) { ++ REQUIRE(VALID_CACHE(cache)); ++ ++ LOCK(&cache->lock); ++ cache->serve_stale_ttl = ttl; ++ UNLOCK(&cache->lock); ++ ++ (void)dns_db_setservestalettl(cache->db, ttl); ++} ++ ++dns_ttl_t ++dns_cache_getservestalettl(dns_cache_t *cache) { ++ dns_ttl_t ttl; ++ isc_result_t result; ++ ++ REQUIRE(VALID_CACHE(cache)); ++ ++ /* ++ * Could get it straight from the dns_cache_t, but use db ++ * to confirm the value that the db is really using. ++ */ ++ result = dns_db_getservestalettl(cache->db, &ttl); ++ return result == ISC_R_SUCCESS ? ttl : 0; ++} ++ + /* + * The cleaner task is shutting down; do the necessary cleanup. + */ +diff --git a/lib/dns/db.c b/lib/dns/db.c +index ee3e00d53c..576aa65992 100644 +--- a/lib/dns/db.c ++++ b/lib/dns/db.c +@@ -1130,3 +1130,25 @@ dns_db_nodefullname(dns_db_t *db, dns_dbnode_t *node, dns_name_t *name) { + return (ISC_R_NOTIMPLEMENTED); + return ((db->methods->nodefullname)(db, node, name)); + } ++ ++isc_result_t ++dns_db_setservestalettl(dns_db_t *db, dns_ttl_t ttl) ++{ ++ REQUIRE(DNS_DB_VALID(db)); ++ REQUIRE((db->attributes & DNS_DBATTR_CACHE) != 0); ++ ++ if (db->methods->setservestalettl != NULL) ++ return ((db->methods->setservestalettl)(db, ttl)); ++ return (ISC_R_NOTIMPLEMENTED); ++} ++ ++isc_result_t ++dns_db_getservestalettl(dns_db_t *db, dns_ttl_t *ttl) ++{ ++ REQUIRE(DNS_DB_VALID(db)); ++ REQUIRE((db->attributes & DNS_DBATTR_CACHE) != 0); ++ ++ if (db->methods->getservestalettl != NULL) ++ return ((db->methods->getservestalettl)(db, ttl)); ++ return (ISC_R_NOTIMPLEMENTED); ++} +diff --git a/lib/dns/ecdb.c b/lib/dns/ecdb.c +index 6e5a4ae4e4..3aa0ac478d 100644 +--- a/lib/dns/ecdb.c ++++ b/lib/dns/ecdb.c +@@ -588,7 +588,9 @@ static dns_dbmethods_t ecdb_methods = { + NULL, /* setcachestats */ + NULL, /* hashsize */ + NULL, /* nodefullname */ +- NULL /* getsize */ ++ NULL, /* getsize */ ++ NULL, /* setservestalettl */ ++ NULL /* getservestalettl */ + }; + + static isc_result_t +diff --git a/lib/dns/include/dns/cache.h b/lib/dns/include/dns/cache.h +index 62797dbbd4..714b78eb74 100644 +--- a/lib/dns/include/dns/cache.h ++++ b/lib/dns/include/dns/cache.h +@@ -260,6 +260,27 @@ dns_cache_getcachesize(dns_cache_t *cache); + * Get the maximum cache size. + */ + ++void ++dns_cache_setservestalettl(dns_cache_t *cache, dns_ttl_t ttl); ++/*%< ++ * Sets the maximum length of time that cached answers may be retained ++ * past their normal TTL. Default value for the library is 0, disabling ++ * the use of stale data. ++ * ++ * Requires: ++ *\li 'cache' to be valid. ++ */ ++ ++dns_ttl_t ++dns_cache_getservestalettl(dns_cache_t *cache); ++/*%< ++ * Gets the maximum length of time that cached answers may be kept past ++ * normal expiry. ++ * ++ * Requires: ++ *\li 'cache' to be valid. ++ */ ++ + isc_result_t + dns_cache_flush(dns_cache_t *cache); + /*%< +diff --git a/lib/dns/include/dns/db.h b/lib/dns/include/dns/db.h +index ae6ae36596..5079053d91 100644 +--- a/lib/dns/include/dns/db.h ++++ b/lib/dns/include/dns/db.h +@@ -197,6 +197,8 @@ typedef struct dns_dbmethods { + dns_name_t *name); + isc_result_t (*getsize)(dns_db_t *db, dns_dbversion_t *version, + uint64_t *records, uint64_t *bytes); ++ isc_result_t (*setservestalettl)(dns_db_t *db, dns_ttl_t ttl); ++ isc_result_t (*getservestalettl)(dns_db_t *db, dns_ttl_t *ttl); + } dns_dbmethods_t; + + typedef isc_result_t +@@ -255,6 +257,7 @@ struct dns_dbonupdatelistener { + #define DNS_DBFIND_FORCENSEC3 0x0080 + #define DNS_DBFIND_ADDITIONALOK 0x0100 + #define DNS_DBFIND_NOZONECUT 0x0200 ++#define DNS_DBFIND_STALEOK 0x0400 + /*@}*/ + + /*@{*/ +@@ -1685,6 +1688,38 @@ dns_db_nodefullname(dns_db_t *db, dns_dbnode_t *node, dns_name_t *name); + * \li 'db' is a valid database + * \li 'node' and 'name' are not NULL + */ ++ ++isc_result_t ++dns_db_setservestalettl(dns_db_t *db, dns_ttl_t ttl); ++/*%< ++ * Sets the maximum length of time that cached answers may be retained ++ * past their normal TTL. Default value for the library is 0, disabling ++ * the use of stale data. ++ * ++ * Requires: ++ * \li 'db' is a valid cache database. ++ * \li 'ttl' is the number of seconds to retain data past its normal expiry. ++ * ++ * Returns: ++ * \li #ISC_R_SUCCESS ++ * \li #ISC_R_NOTIMPLEMENTED - Not supported by this DB implementation. ++ */ ++ ++isc_result_t ++dns_db_getservestalettl(dns_db_t *db, dns_ttl_t *ttl); ++/*%< ++ * Gets maximum length of time that cached answers may be kept past ++ * normal TTL expiration. ++ * ++ * Requires: ++ * \li 'db' is a valid cache database. ++ * \li 'ttl' is the number of seconds to retain data past its normal expiry. ++ * ++ * Returns: ++ * \li #ISC_R_SUCCESS ++ * \li #ISC_R_NOTIMPLEMENTED - Not supported by this DB implementation. ++ */ ++ + ISC_LANG_ENDDECLS + + #endif /* DNS_DB_H */ +diff --git a/lib/dns/include/dns/rdataset.h b/lib/dns/include/dns/rdataset.h +index 5295d8e4d7..97071ed496 100644 +--- a/lib/dns/include/dns/rdataset.h ++++ b/lib/dns/include/dns/rdataset.h +@@ -128,6 +128,7 @@ struct dns_rdataset { + unsigned int magic; /* XXX ? */ + dns_rdatasetmethods_t * methods; + ISC_LINK(dns_rdataset_t) link; ++ + /* + * XXX do we need these, or should they be retrieved by methods? + * Leaning towards the latter, since they are not frequently required +@@ -136,12 +137,19 @@ struct dns_rdataset { + dns_rdataclass_t rdclass; + dns_rdatatype_t type; + dns_ttl_t ttl; ++ /* ++ * Stale ttl is used to see how long this RRset can still be used ++ * to serve to clients, after the TTL has expired. ++ */ ++ dns_ttl_t stale_ttl; + dns_trust_t trust; + dns_rdatatype_t covers; ++ + /* + * attributes + */ + unsigned int attributes; ++ + /*% + * the counter provides the starting point in the "cyclic" order. + * The value UINT32_MAX has a special meaning of "picking up a +@@ -149,11 +157,13 @@ struct dns_rdataset { + * increment the counter. + */ + uint32_t count; ++ + /* + * This RRSIG RRset should be re-generated around this time. + * Only valid if DNS_RDATASETATTR_RESIGN is set in attributes. + */ + isc_stdtime_t resign; ++ + /*@{*/ + /*% + * These are for use by the rdataset implementation, and MUST NOT +@@ -206,6 +216,7 @@ struct dns_rdataset { + #define DNS_RDATASETATTR_OPTOUT 0x00100000 /*%< OPTOUT proof */ + #define DNS_RDATASETATTR_NEGATIVE 0x00200000 + #define DNS_RDATASETATTR_PREFETCH 0x00400000 ++#define DNS_RDATASETATTR_STALE 0x01000000 + + /*% + * _OMITDNSSEC: +diff --git a/lib/dns/include/dns/resolver.h b/lib/dns/include/dns/resolver.h +index 6da41b7a5a..7b397cb6d2 100644 +--- a/lib/dns/include/dns/resolver.h ++++ b/lib/dns/include/dns/resolver.h +@@ -547,9 +547,12 @@ dns_resolver_getmustbesecure(dns_resolver_t *resolver, dns_name_t *name); + + + void +-dns_resolver_settimeout(dns_resolver_t *resolver, unsigned int seconds); ++dns_resolver_settimeout(dns_resolver_t *resolver, unsigned int timeout); + /*%< +- * Set the length of time the resolver will work on a query, in seconds. ++ * Set the length of time the resolver will work on a query, in milliseconds. ++ * ++ * 'timeout' was originally defined in seconds, and later redefined to be in ++ * milliseconds. Values less than or equal to 300 are treated as seconds. + * + * If timeout is 0, the default timeout will be applied. + * +@@ -560,7 +563,8 @@ dns_resolver_settimeout(dns_resolver_t *resolver, unsigned int seconds); + unsigned int + dns_resolver_gettimeout(dns_resolver_t *resolver); + /*%< +- * Get the current length of time the resolver will work on a query, in seconds. ++ * Get the current length of time the resolver will work on a query, ++ * in milliseconds. + * + * Requires: + * \li resolver to be valid. +@@ -582,6 +586,39 @@ dns_resolver_getzeronosoattl(dns_resolver_t *resolver); + void + dns_resolver_setzeronosoattl(dns_resolver_t *resolver, bool state); + ++unsigned int ++dns_resolver_getretryinterval(dns_resolver_t *resolver); ++ ++void ++dns_resolver_setretryinterval(dns_resolver_t *resolver, unsigned int interval); ++/*%< ++ * Sets the amount of time, in millseconds, that is waited for a reply ++ * to a server before another server is tried. Interacts with the ++ * value of dns_resolver_getnonbackofftries() by trying that number of times ++ * at this interval, before doing exponential backoff and doubling the interval ++ * on each subsequent try, to a maximum of 10 seconds. Defaults to 800 ms; ++ * silently capped at 2000 ms. ++ * ++ * Requires: ++ * \li resolver to be valid. ++ * \li interval > 0. ++ */ ++ ++unsigned int ++dns_resolver_getnonbackofftries(dns_resolver_t *resolver); ++ ++void ++dns_resolver_setnonbackofftries(dns_resolver_t *resolver, unsigned int tries); ++/*%< ++ * Sets the number of failures of getting a reply from remote servers for ++ * a query before backing off by doubling the retry interval for each ++ * subsequent request sent. Defaults to 3. ++ * ++ * Requires: ++ * \li resolver to be valid. ++ * \li tries > 0. ++ */ ++ + unsigned int + dns_resolver_getoptions(dns_resolver_t *resolver); + +diff --git a/lib/dns/include/dns/types.h b/lib/dns/include/dns/types.h +index 567e8a879e..7bf2b60d42 100644 +--- a/lib/dns/include/dns/types.h ++++ b/lib/dns/include/dns/types.h +@@ -385,6 +385,12 @@ typedef enum { + dns_updatemethod_date + } dns_updatemethod_t; + ++typedef enum { ++ dns_stale_answer_no, ++ dns_stale_answer_yes, ++ dns_stale_answer_conf ++} dns_stale_answer_t; ++ + /* + * Functions. + */ +diff --git a/lib/dns/include/dns/view.h b/lib/dns/include/dns/view.h +index c849dec154..647ca2ac08 100644 +--- a/lib/dns/include/dns/view.h ++++ b/lib/dns/include/dns/view.h +@@ -229,6 +229,9 @@ struct dns_view { + dns_dtenv_t *dtenv; /* Dnstap environment */ + dns_dtmsgtype_t dttypes; /* Dnstap message types + to log */ ++ dns_ttl_t staleanswerttl; ++ dns_stale_answer_t staleanswersok; /* rndc setting */ ++ bool staleanswersenable; /* named.conf setting */ + }; + + #define DNS_VIEW_MAGIC ISC_MAGIC('V','i','e','w') +diff --git a/lib/dns/master.c b/lib/dns/master.c +index 2a87bca3bc..ac4bb195ca 100644 +--- a/lib/dns/master.c ++++ b/lib/dns/master.c +@@ -1948,12 +1948,18 @@ load_text(dns_loadctx_t *lctx) { + + if ((lctx->options & DNS_MASTER_AGETTL) != 0) { + /* +- * Adjust the TTL for $DATE. If the RR has already +- * expired, ignore it. ++ * Adjust the TTL for $DATE. If the RR has ++ * already expired, set its TTL to 0. This ++ * should be okay even if the TTL stretching ++ * feature is not in effect, because it will ++ * just be quickly expired by the cache, and the ++ * way this was written before the patch it ++ * could potentially add 0 TTLs anyway. + */ + if (lctx->ttl < ttl_offset) +- continue; +- lctx->ttl -= ttl_offset; ++ lctx->ttl = 0; ++ else ++ lctx->ttl -= ttl_offset; + } + + /* +diff --git a/lib/dns/masterdump.c b/lib/dns/masterdump.c +index 7edef6ad9b..daf355748b 100644 +--- a/lib/dns/masterdump.c ++++ b/lib/dns/masterdump.c +@@ -81,6 +81,9 @@ struct dns_master_style { + */ + #define DNS_TOTEXT_LINEBREAK_MAXLEN 100 + ++/*% Does the rdataset 'r' contain a stale answer? */ ++#define STALE(r) (((r)->attributes & DNS_RDATASETATTR_STALE) != 0) ++ + /*% + * Context structure for a masterfile dump in progress. + */ +@@ -94,6 +97,7 @@ typedef struct dns_totext_ctx { + dns_fixedname_t origin_fixname; + uint32_t current_ttl; + bool current_ttl_valid; ++ dns_ttl_t serve_stale_ttl; + } dns_totext_ctx_t; + + LIBDNS_EXTERNAL_DATA const dns_master_style_t +@@ -386,6 +390,7 @@ totext_ctx_init(const dns_master_style_t *style, dns_totext_ctx_t *ctx) { + ctx->neworigin = NULL; + ctx->current_ttl = 0; + ctx->current_ttl_valid = false; ++ ctx->serve_stale_ttl = 0; + + return (ISC_R_SUCCESS); + } +@@ -1036,6 +1041,11 @@ dump_rdatasets_text(isc_mem_t *mctx, dns_name_t *name, + (ctx->style.flags & DNS_STYLEFLAG_NCACHE) == 0) { + /* Omit negative cache entries */ + } else { ++ if (STALE(rds)) { ++ fprintf(f, "; stale (for %u more seconds)\n", ++ (rds->stale_ttl - ++ ctx->serve_stale_ttl)); ++ } + isc_result_t result = + dump_rdataset(mctx, name, rds, ctx, + buffer, f); +@@ -1504,6 +1514,16 @@ dumpctx_create(isc_mem_t *mctx, dns_db_t *db, dns_dbversion_t *version, + dns_db_attach(db, &dctx->db); + + dctx->do_date = dns_db_iscache(dctx->db); ++ if (dctx->do_date) { ++ /* ++ * Adjust the date backwards by the serve-stale TTL, if any. ++ * This is so the TTL will be loaded correctly when next ++ * started. ++ */ ++ (void)dns_db_getservestalettl(dctx->db, ++ &dctx->tctx.serve_stale_ttl); ++ dctx->now -= dctx->tctx.serve_stale_ttl; ++ } + + if (dctx->format == dns_masterformat_text && + (dctx->tctx.style.flags & DNS_STYLEFLAG_REL_OWNER) != 0) { +@@ -1563,6 +1583,9 @@ writeheader(dns_dumpctx_t *dctx) { + * it in the zone case. + */ + if (dctx->do_date) { ++ fprintf(dctx->f, ++ "; using a %d second stale ttl\n", ++ dctx->tctx.serve_stale_ttl); + result = dns_time32_totext(dctx->now, &buffer); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + isc_buffer_usedregion(&buffer, &r); +diff --git a/lib/dns/rbtdb.c b/lib/dns/rbtdb.c +index ada888cff9..39499359c8 100644 +--- a/lib/dns/rbtdb.c ++++ b/lib/dns/rbtdb.c +@@ -488,6 +488,7 @@ typedef ISC_LIST(rdatasetheader_t) rdatasetheaderlist_t; + typedef ISC_LIST(dns_rbtnode_t) rbtnodelist_t; + + #define RDATASET_ATTR_NONEXISTENT 0x0001 ++/*%< May be potentially served as stale data. */ + #define RDATASET_ATTR_STALE 0x0002 + #define RDATASET_ATTR_IGNORE 0x0004 + #define RDATASET_ATTR_RETAIN 0x0008 +@@ -500,6 +501,8 @@ typedef ISC_LIST(dns_rbtnode_t) rbtnodelist_t; + #define RDATASET_ATTR_CASESET 0x0400 + #define RDATASET_ATTR_ZEROTTL 0x0800 + #define RDATASET_ATTR_CASEFULLYLOWER 0x1000 ++/*%< Ancient - awaiting cleanup. */ ++#define RDATASET_ATTR_ANCIENT 0x2000 + + typedef struct acache_cbarg { + dns_rdatasetadditional_t type; +@@ -550,6 +553,8 @@ struct acachectl { + (((header)->attributes & RDATASET_ATTR_ZEROTTL) != 0) + #define CASEFULLYLOWER(header) \ + (((header)->attributes & RDATASET_ATTR_CASEFULLYLOWER) != 0) ++#define ANCIENT(header) \ ++ (((header)->attributes & RDATASET_ATTR_ANCIENT) != 0) + + + #define ACTIVE(header, now) \ +@@ -609,6 +614,12 @@ typedef enum { + expire_flush + } expire_t; + ++typedef enum { ++ rdataset_ttl_fresh, ++ rdataset_ttl_stale, ++ rdataset_ttl_ancient ++} rdataset_ttl_t; ++ + typedef struct rbtdb_version { + /* Not locked */ + rbtdb_serial_t serial; +@@ -676,6 +687,12 @@ struct dns_rbtdb { + dns_dbnode_t *soanode; + dns_dbnode_t *nsnode; + ++ /* ++ * Maximum length of time to keep using a stale answer past its ++ * normal TTL expiry. ++ */ ++ dns_ttl_t serve_stale_ttl; ++ + /* + * This is a linked list used to implement the LRU cache. There will + * be node_lock_count linked lists here. Nodes in bucket 1 will be +@@ -719,6 +736,8 @@ struct dns_rbtdb { + #define RBTDB_ATTR_LOADED 0x01 + #define RBTDB_ATTR_LOADING 0x02 + ++#define KEEPSTALE(rbtdb) ((rbtdb)->serve_stale_ttl > 0) ++ + /*% + * Search Context + */ +@@ -1784,15 +1803,15 @@ rollback_node(dns_rbtnode_t *node, rbtdb_serial_t serial) { + } + + static inline void +-mark_stale_header(dns_rbtdb_t *rbtdb, rdatasetheader_t *header) { ++mark_header_ancient(dns_rbtdb_t *rbtdb, rdatasetheader_t *header) { + + /* +- * If we are already stale there is nothing to do. ++ * If we are already ancient there is nothing to do. + */ +- if ((header->attributes & RDATASET_ATTR_STALE) != 0) ++ if (ANCIENT(header)) + return; + +- header->attributes |= RDATASET_ATTR_STALE; ++ header->attributes |= RDATASET_ATTR_ANCIENT; + header->node->dirty = 1; + + /* +@@ -1833,8 +1852,8 @@ clean_cache_node(dns_rbtdb_t *rbtdb, dns_rbtnode_t *node) { + /* + * If current is nonexistent or stale, we can clean it up. + */ +- if ((current->attributes & +- (RDATASET_ATTR_NONEXISTENT|RDATASET_ATTR_STALE)) != 0) { ++ if (NONEXISTENT(current) || ANCIENT(current) || ++ (STALE(current) && ! KEEPSTALE(rbtdb))) { + if (top_prev != NULL) + top_prev->next = current->next; + else +@@ -2076,6 +2095,80 @@ delete_node(dns_rbtdb_t *rbtdb, dns_rbtnode_t *node) { + } + } + ++#if 0 ++static void ++clean_now_or_later(dns_rbtnode_t *node, dns_rbtdb_t *rbtdb, ++ rdatasetheader_t *header, rdatasetheader_t **header_prevp) ++{ ++ if (dns_rbtnode_refcurrent(node) == 0) { ++ isc_mem_t *mctx; ++ ++ /* ++ * header->down can be non-NULL if the refcount has just ++ * decremented to 0 but decrement_reference() has not performed ++ * clean_cache_node(), in which case we need to purge the stale ++ * headers first. ++ */ ++ mctx = rbtdb->common.mctx; ++ clean_stale_headers(rbtdb, mctx, header); ++ if (*header_prevp != NULL) ++ (*header_prevp)->next = header->next; ++ else ++ node->data = header->next; ++ free_rdataset(rbtdb, mctx, header); ++ } else { ++ header->attributes |= RDATASET_ATTR_STALE | ++ RDATASET_ATTR_ANCIENT; ++ node->dirty = 1; ++ *header_prevp = header; ++ } ++} ++ ++static rdataset_ttl_t ++check_ttl(dns_rbtnode_t *node, rbtdb_search_t *search, ++ rdatasetheader_t *header, rdatasetheader_t **header_prevp, ++ nodelock_t *lock, isc_rwlocktype_t *locktype) ++{ ++ dns_rbtdb_t *rbtdb = search->rbtdb; ++ ++ if (header->rdh_ttl > search->now) ++ return rdataset_ttl_fresh; ++ ++ /* ++ * This rdataset is stale, but perhaps still usable. ++ */ ++ if (KEEPSTALE(rbtdb) && ++ header->rdh_ttl + rbtdb->serve_stale_ttl > search->now) { ++ header->attributes |= RDATASET_ATTR_STALE; ++ /* Doesn't set dirty because it doesn't need removal. */ ++ return rdataset_ttl_stale; ++ } ++ ++ /* ++ * This rdataset is so stale it is no longer usable, even with ++ * KEEPSTALE. If no one else is using the node, we can clean it up ++ * right now, otherwise we mark it as ancient, and the node as dirty, ++ * so it will get cleaned up later. ++ */ ++ if ((header->rdh_ttl <= search->now - RBTDB_VIRTUAL) && ++ (*locktype == isc_rwlocktype_write || ++ NODE_TRYUPGRADE(lock) == ISC_R_SUCCESS)) { ++ /* ++ * We update the node's status only when we can get write ++ * access; otherwise, we leave others to this work. Periodical ++ * cleaning will eventually take the job as the last resort. ++ * We won't downgrade the lock, since other rdatasets are ++ * probably stale, too. ++ */ ++ *locktype = isc_rwlocktype_write; ++ clean_now_or_later(node, rbtdb, header, header_prevp); ++ } else ++ *header_prevp = header; ++ ++ return rdataset_ttl_ancient; ++} ++#endif ++ + /* + * Caller must be holding the node lock. + */ +@@ -3308,6 +3401,12 @@ bind_rdataset(dns_rbtdb_t *rbtdb, dns_rbtnode_t *node, + rdataset->attributes |= DNS_RDATASETATTR_OPTOUT; + if (PREFETCH(header)) + rdataset->attributes |= DNS_RDATASETATTR_PREFETCH; ++ if (STALE(header)) { ++ rdataset->attributes |= DNS_RDATASETATTR_STALE; ++ rdataset->stale_ttl = ++ (rbtdb->serve_stale_ttl + header->rdh_ttl) - now; ++ rdataset->ttl = 0; ++ } + rdataset->private1 = rbtdb; + rdataset->private2 = node; + raw = (unsigned char *)header + sizeof(*header); +@@ -4648,6 +4747,19 @@ check_stale_header(dns_rbtnode_t *node, rdatasetheader_t *header, + #endif + + if (!ACTIVE(header, search->now)) { ++ dns_ttl_t stale = header->rdh_ttl + ++ search->rbtdb->serve_stale_ttl; ++ /* ++ * If this data is in the stale window keep it and if ++ * DNS_DBFIND_STALEOK is not set we tell the caller to ++ * skip this record. ++ */ ++ if (KEEPSTALE(search->rbtdb) && stale > search->now) { ++ header->attributes |= RDATASET_ATTR_STALE; ++ *header_prev = header; ++ return ((search->options & DNS_DBFIND_STALEOK) == 0); ++ } ++ + /* + * This rdataset is stale. If no one else is using the + * node, we can clean it up right now, otherwise we mark +@@ -4687,7 +4799,7 @@ check_stale_header(dns_rbtnode_t *node, rdatasetheader_t *header, + node->data = header->next; + free_rdataset(search->rbtdb, mctx, header); + } else { +- mark_stale_header(search->rbtdb, header); ++ mark_header_ancient(search->rbtdb, header); + *header_prev = header; + } + } else +@@ -5125,7 +5237,7 @@ cache_find(dns_db_t *db, dns_name_t *name, dns_dbversion_t *version, + &locktype, lock, &search, + &header_prev)) { + /* Do nothing. */ +- } else if (EXISTS(header) && (!STALE(header))) { ++ } else if (EXISTS(header) && !ANCIENT(header)) { + /* + * We now know that there is at least one active + * non-stale rdataset at this node. +@@ -5603,7 +5715,7 @@ expirenode(dns_db_t *db, dns_dbnode_t *node, isc_stdtime_t now) { + * refcurrent(rbtnode) must be non-zero. This is so + * because 'node' is an argument to the function. + */ +- mark_stale_header(rbtdb, header); ++ mark_header_ancient(rbtdb, header); + if (log) + isc_log_write(dns_lctx, category, module, + level, "overmem cache: stale %s", +@@ -5611,7 +5723,7 @@ expirenode(dns_db_t *db, dns_dbnode_t *node, isc_stdtime_t now) { + } else if (force_expire) { + if (! RETAIN(header)) { + set_ttl(rbtdb, header, 0); +- mark_stale_header(rbtdb, header); ++ mark_header_ancient(rbtdb, header); + } else if (log) { + isc_log_write(dns_lctx, category, module, + level, "overmem cache: " +@@ -5868,9 +5980,9 @@ cache_findrdataset(dns_db_t *db, dns_dbnode_t *node, dns_dbversion_t *version, + * non-zero. This is so because 'node' is an + * argument to the function. + */ +- mark_stale_header(rbtdb, header); ++ mark_header_ancient(rbtdb, header); + } +- } else if (EXISTS(header) && (!STALE(header))) { ++ } else if (EXISTS(header) && !ANCIENT(header)) { + if (header->type == matchtype) + found = header; + else if (header->type == RBTDB_RDATATYPE_NCACHEANY || +@@ -6160,7 +6272,7 @@ add32(dns_rbtdb_t *rbtdb, dns_rbtnode_t *rbtnode, rbtdb_version_t *rbtversion, + topheader = topheader->next) + { + set_ttl(rbtdb, topheader, 0); +- mark_stale_header(rbtdb, topheader); ++ mark_header_ancient(rbtdb, topheader); + } + goto find_header; + } +@@ -6218,7 +6330,7 @@ add32(dns_rbtdb_t *rbtdb, dns_rbtnode_t *rbtnode, rbtdb_version_t *rbtversion, + * ncache entry. + */ + set_ttl(rbtdb, topheader, 0); +- mark_stale_header(rbtdb, topheader); ++ mark_header_ancient(rbtdb, topheader); + topheader = NULL; + goto find_header; + } +@@ -6256,8 +6368,11 @@ add32(dns_rbtdb_t *rbtdb, dns_rbtnode_t *rbtnode, rbtdb_version_t *rbtversion, + } + + /* +- * Trying to add an rdataset with lower trust to a cache DB +- * has no effect, provided that the cache data isn't stale. ++ * Trying to add an rdataset with lower trust to a cache ++ * DB has no effect, provided that the cache data isn't ++ * stale. If the cache data is stale, new lower trust ++ * data will supersede it below. Unclear what the best ++ * policy is here. + */ + if (rbtversion == NULL && trust < header->trust && + (ACTIVE(header, now) || header_nx)) { +@@ -6286,6 +6401,10 @@ add32(dns_rbtdb_t *rbtdb, dns_rbtnode_t *rbtnode, rbtdb_version_t *rbtversion, + + if ((options & DNS_DBADD_EXACT) != 0) + flags |= DNS_RDATASLAB_EXACT; ++ /* ++ * TTL use here is irrelevant to the cache; ++ * merge is only done with zonedbs. ++ */ + if ((options & DNS_DBADD_EXACTTTL) != 0 && + newheader->rdh_ttl != header->rdh_ttl) + result = DNS_R_NOTEXACT; +@@ -6329,11 +6448,12 @@ add32(dns_rbtdb_t *rbtdb, dns_rbtnode_t *rbtnode, rbtdb_version_t *rbtversion, + } + } + /* +- * Don't replace existing NS, A and AAAA RRsets +- * in the cache if they are already exist. This +- * prevents named being locked to old servers. +- * Don't lower trust of existing record if the +- * update is forced. ++ * Don't replace existing NS, A and AAAA RRsets in the ++ * cache if they are already exist. This prevents named ++ * being locked to old servers. Don't lower trust of ++ * existing record if the update is forced. Nothing ++ * special to be done w.r.t stale data; it gets replaced ++ * normally further down. + */ + if (IS_CACHE(rbtdb) && ACTIVE(header, now) && + header->type == dns_rdatatype_ns && +@@ -6508,10 +6628,10 @@ add32(dns_rbtdb_t *rbtdb, dns_rbtnode_t *rbtnode, rbtdb_version_t *rbtversion, + changed->dirty = true; + if (rbtversion == NULL) { + set_ttl(rbtdb, header, 0); +- mark_stale_header(rbtdb, header); ++ mark_header_ancient(rbtdb, header); + if (sigheader != NULL) { + set_ttl(rbtdb, sigheader, 0); +- mark_stale_header(rbtdb, sigheader); ++ mark_header_ancient(rbtdb, sigheader); + } + } + if (rbtversion != NULL && !header_nx) { +@@ -8313,6 +8433,30 @@ nodefullname(dns_db_t *db, dns_dbnode_t *node, dns_name_t *name) { + return (result); + } + ++static isc_result_t ++setservestalettl(dns_db_t *db, dns_ttl_t ttl) { ++ dns_rbtdb_t *rbtdb = (dns_rbtdb_t *)db; ++ ++ REQUIRE(VALID_RBTDB(rbtdb)); ++ REQUIRE(IS_CACHE(rbtdb)); ++ ++ /* currently no bounds checking. 0 means disable. */ ++ rbtdb->serve_stale_ttl = ttl; ++ return ISC_R_SUCCESS; ++} ++ ++static isc_result_t ++getservestalettl(dns_db_t *db, dns_ttl_t *ttl) { ++ dns_rbtdb_t *rbtdb = (dns_rbtdb_t *)db; ++ ++ REQUIRE(VALID_RBTDB(rbtdb)); ++ REQUIRE(IS_CACHE(rbtdb)); ++ ++ *ttl = rbtdb->serve_stale_ttl; ++ return ISC_R_SUCCESS; ++} ++ ++ + static dns_dbmethods_t zone_methods = { + attach, + detach, +@@ -8358,7 +8502,9 @@ static dns_dbmethods_t zone_methods = { + NULL, + hashsize, + nodefullname, +- getsize ++ getsize, ++ NULL, ++ NULL + }; + + static dns_dbmethods_t cache_methods = { +@@ -8406,7 +8552,9 @@ static dns_dbmethods_t cache_methods = { + setcachestats, + hashsize, + nodefullname, +- NULL ++ NULL, ++ setservestalettl, ++ getservestalettl + }; + + isc_result_t +@@ -8677,7 +8825,7 @@ dns_rbtdb_create + rbtdb->rpzs = NULL; + rbtdb->load_rpzs = NULL; + rbtdb->rpz_num = DNS_RPZ_INVALID_NUM; +- ++ rbtdb->serve_stale_ttl = 0; + /* + * Version Initialization. + */ +@@ -9095,7 +9243,8 @@ rdatasetiter_first(dns_rdatasetiter_t *iterator) { + * rdatasets to work. + */ + if (NONEXISTENT(header) || +- (now != 0 && now > header->rdh_ttl)) ++ (now != 0 && now > header->rdh_ttl ++ + rbtdb->serve_stale_ttl)) + header = NULL; + break; + } else +@@ -10283,7 +10432,7 @@ static inline bool + need_headerupdate(rdatasetheader_t *header, isc_stdtime_t now) { + if ((header->attributes & + (RDATASET_ATTR_NONEXISTENT | +- RDATASET_ATTR_STALE | ++ RDATASET_ATTR_ANCIENT | + RDATASET_ATTR_ZEROTTL)) != 0) + return (false); + +@@ -10389,7 +10538,7 @@ expire_header(dns_rbtdb_t *rbtdb, rdatasetheader_t *header, + bool tree_locked, expire_t reason) + { + set_ttl(rbtdb, header, 0); +- mark_stale_header(rbtdb, header); ++ mark_header_ancient(rbtdb, header); + + /* + * Caller must hold the node (write) lock. +diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c +index cefa53d904..54a624fc25 100644 +--- a/lib/dns/resolver.c ++++ b/lib/dns/resolver.c +@@ -141,16 +141,17 @@ + #endif /* WANT_QUERYTRACE */ + + #define US_PER_SEC 1000000U ++#define US_PER_MSEC 1000U + /* + * The maximum time we will wait for a single query. + */ +-#define MAX_SINGLE_QUERY_TIMEOUT 9U +-#define MAX_SINGLE_QUERY_TIMEOUT_US (MAX_SINGLE_QUERY_TIMEOUT*US_PER_SEC) ++#define MAX_SINGLE_QUERY_TIMEOUT 9000U ++#define MAX_SINGLE_QUERY_TIMEOUT_US (MAX_SINGLE_QUERY_TIMEOUT*US_PER_MSEC) + + /* + * We need to allow a individual query time to complete / timeout. + */ +-#define MINIMUM_QUERY_TIMEOUT (MAX_SINGLE_QUERY_TIMEOUT + 1U) ++#define MINIMUM_QUERY_TIMEOUT (MAX_SINGLE_QUERY_TIMEOUT + 1000U) + + /* The default time in seconds for the whole query to live. */ + #ifndef DEFAULT_QUERY_TIMEOUT +@@ -159,7 +160,7 @@ + + /* The maximum time in seconds for the whole query to live. */ + #ifndef MAXIMUM_QUERY_TIMEOUT +-#define MAXIMUM_QUERY_TIMEOUT 30 ++#define MAXIMUM_QUERY_TIMEOUT 30000 + #endif + + /* The default maximum number of recursions to follow before giving up. */ +@@ -496,6 +497,10 @@ struct dns_resolver { + unsigned int maxqueries; + isc_result_t quotaresp[2]; + ++ /* Additions for serve-stale feature. */ ++ unsigned int retryinterval; /* in milliseconds */ ++ unsigned int nonbackofftries; ++ + /* Locked by lock. */ + unsigned int references; + bool exiting; +@@ -1602,14 +1607,12 @@ fctx_setretryinterval(fetchctx_t *fctx, unsigned int rtt) { + unsigned int seconds; + unsigned int us; + ++ us = fctx->res->retryinterval * 1000; + /* +- * We retry every .8 seconds the first two times through the address +- * list, and then we do exponential back-off. ++ * Exponential backoff after the first few tries. + */ +- if (fctx->restarts < 3) +- us = 800000; +- else +- us = (800000 << (fctx->restarts - 2)); ++ if (fctx->restarts >= fctx->res->nonbackofftries) ++ us <<= (fctx->restarts - fctx->res->nonbackofftries - 1); + + /* + * Add a fudge factor to the expected rtt based on the current +@@ -4453,7 +4456,8 @@ fctx_create(dns_resolver_t *res, dns_name_t *name, dns_rdatatype_t type, + /* + * Compute an expiration time for the entire fetch. + */ +- isc_interval_set(&interval, res->query_timeout, 0); ++ isc_interval_set(&interval, res->query_timeout / 1000, ++ res->query_timeout % 1000 * 1000000); + iresult = isc_time_nowplusinterval(&fctx->expires, &interval); + if (iresult != ISC_R_SUCCESS) { + UNEXPECTED_ERROR(__FILE__, __LINE__, +@@ -8937,6 +8941,8 @@ dns_resolver_create(dns_view_t *view, + res->spillattimer = NULL; + res->zspill = 0; + res->zero_no_soa_ttl = false; ++ res->retryinterval = 30000; ++ res->nonbackofftries = 3; + res->query_timeout = DEFAULT_QUERY_TIMEOUT; + res->maxdepth = DEFAULT_RECURSION_DEPTH; + res->maxqueries = DEFAULT_MAX_QUERIES; +@@ -10263,17 +10269,20 @@ dns_resolver_gettimeout(dns_resolver_t *resolver) { + } + + void +-dns_resolver_settimeout(dns_resolver_t *resolver, unsigned int seconds) { ++dns_resolver_settimeout(dns_resolver_t *resolver, unsigned int timeout) { + REQUIRE(VALID_RESOLVER(resolver)); + +- if (seconds == 0) +- seconds = DEFAULT_QUERY_TIMEOUT; +- if (seconds > MAXIMUM_QUERY_TIMEOUT) +- seconds = MAXIMUM_QUERY_TIMEOUT; +- if (seconds < MINIMUM_QUERY_TIMEOUT) +- seconds = MINIMUM_QUERY_TIMEOUT; ++ if (timeout <= 300) ++ timeout *= 1000; ++ ++ if (timeout == 0) ++ timeout = DEFAULT_QUERY_TIMEOUT; ++ if (timeout > MAXIMUM_QUERY_TIMEOUT) ++ timeout = MAXIMUM_QUERY_TIMEOUT; ++ if (timeout < MINIMUM_QUERY_TIMEOUT) ++ timeout = MINIMUM_QUERY_TIMEOUT; + +- resolver->query_timeout = seconds; ++ resolver->query_timeout = timeout; + } + + void +@@ -10370,3 +10379,34 @@ dns_resolver_getquotaresponse(dns_resolver_t *resolver, dns_quotatype_t which) + + return (resolver->quotaresp[which]); + } ++ ++unsigned int ++dns_resolver_getretryinterval(dns_resolver_t *resolver) { ++ REQUIRE(VALID_RESOLVER(resolver)); ++ ++ return (resolver->retryinterval); ++} ++ ++void ++dns_resolver_setretryinterval(dns_resolver_t *resolver, unsigned int interval) ++{ ++ REQUIRE(VALID_RESOLVER(resolver)); ++ REQUIRE(interval > 0); ++ ++ resolver->retryinterval = ISC_MIN(interval, 2000); ++} ++ ++unsigned int ++dns_resolver_getnonbackofftries(dns_resolver_t *resolver) { ++ REQUIRE(VALID_RESOLVER(resolver)); ++ ++ return (resolver->nonbackofftries); ++} ++ ++void ++dns_resolver_setnonbackofftries(dns_resolver_t *resolver, unsigned int tries) { ++ REQUIRE(VALID_RESOLVER(resolver)); ++ REQUIRE(tries > 0); ++ ++ resolver->nonbackofftries = tries; ++} +diff --git a/lib/dns/sdb.c b/lib/dns/sdb.c +index d4c8c673c8..ee9be79cb9 100644 +--- a/lib/dns/sdb.c ++++ b/lib/dns/sdb.c +@@ -1368,7 +1368,9 @@ static dns_dbmethods_t sdb_methods = { + NULL, /* setcachestats */ + NULL, /* hashsize */ + NULL, /* nodefullname */ +- NULL /* getsize */ ++ NULL, /* getsize */ ++ NULL, /* setservestalettl */ ++ NULL /* getservestalettl */ + }; + + static isc_result_t +diff --git a/lib/dns/sdlz.c b/lib/dns/sdlz.c +index 0b9620c76c..331992ebdd 100644 +--- a/lib/dns/sdlz.c ++++ b/lib/dns/sdlz.c +@@ -1336,7 +1336,9 @@ static dns_dbmethods_t sdlzdb_methods = { + NULL, /* setcachestats */ + NULL, /* hashsize */ + NULL, /* nodefullname */ +- NULL /* getsize */ ++ NULL, /* getsize */ ++ NULL, /* setservestalettl */ ++ NULL /* getservestalettl */ + }; + + /* +diff --git a/lib/dns/tests/db_test.c b/lib/dns/tests/db_test.c +index 35cf21d0f2..bf39545d4f 100644 +--- a/lib/dns/tests/db_test.c ++++ b/lib/dns/tests/db_test.c +@@ -28,8 +28,9 @@ + + #include + #include +-#include + #include ++#include ++#include + + #include "dnstest.h" + +@@ -76,7 +77,7 @@ getoriginnode_test(void **state) { + assert_int_equal(result, ISC_R_SUCCESS); + + result = dns_db_create(mymctx, "rbt", dns_rootname, dns_dbtype_zone, +- dns_rdataclass_in, 0, NULL, &db); ++ dns_rdataclass_in, 0, NULL, &db); + assert_int_equal(result, ISC_R_SUCCESS); + + result = dns_db_getoriginnode(db, &node); +@@ -91,6 +92,197 @@ getoriginnode_test(void **state) { + isc_mem_detach(&mymctx); + } + ++/* test getservestalettl and setservestalettl */ ++static void ++getsetservestalettl_test(void **state) { ++ dns_db_t *db = NULL; ++ isc_mem_t *mymctx = NULL; ++ isc_result_t result; ++ dns_ttl_t ttl; ++ ++ UNUSED(state); ++ ++ result = isc_mem_create(0, 0, &mymctx); ++ assert_int_equal(result, ISC_R_SUCCESS); ++ ++ result = dns_db_create(mymctx, "rbt", dns_rootname, dns_dbtype_cache, ++ dns_rdataclass_in, 0, NULL, &db); ++ assert_int_equal(result, ISC_R_SUCCESS); ++ ++ ttl = 5000; ++ result = dns_db_getservestalettl(db, &ttl); ++ assert_int_equal(result, ISC_R_SUCCESS); ++ assert_int_equal(ttl, 0); ++ ++ ttl = 6 * 3600; ++ result = dns_db_setservestalettl(db, ttl); ++ assert_int_equal(result, ISC_R_SUCCESS); ++ ++ ttl = 5000; ++ result = dns_db_getservestalettl(db, &ttl); ++ assert_int_equal(result, ISC_R_SUCCESS); ++ assert_int_equal(ttl, 6 * 3600); ++ ++ dns_db_detach(&db); ++ isc_mem_detach(&mymctx); ++} ++ ++/* check DNS_DBFIND_STALEOK works */ ++static void ++dns_dbfind_staleok_test(void **state) { ++ dns_db_t *db = NULL; ++ dns_dbnode_t *node = NULL; ++ dns_fixedname_t example_fixed; ++ dns_fixedname_t found_fixed; ++ dns_name_t *example; ++ dns_name_t *found; ++ dns_rdatalist_t rdatalist; ++ dns_rdataset_t rdataset; ++ int count; ++ int pass; ++ isc_mem_t *mymctx = NULL; ++ isc_result_t result; ++ unsigned char data[] = { 0x0a, 0x00, 0x00, 0x01 }; ++ ++ UNUSED(state); ++ ++ result = isc_mem_create(0, 0, &mymctx); ++ assert_int_equal(result, ISC_R_SUCCESS); ++ ++ result = dns_db_create(mymctx, "rbt", dns_rootname, dns_dbtype_cache, ++ dns_rdataclass_in, 0, NULL, &db); ++ assert_int_equal(result, ISC_R_SUCCESS); ++ ++ example = dns_fixedname_initname(&example_fixed); ++ found = dns_fixedname_initname(&found_fixed); ++ ++ result = dns_name_fromstring(example, "example", 0, NULL); ++ assert_int_equal(result, ISC_R_SUCCESS); ++ ++ /* ++ * Pass 0: default; no stale processing permitted. ++ * Pass 1: stale processing for 1 second. ++ * Pass 2: stale turned off after being on. ++ */ ++ for (pass = 0; pass < 3; pass++) { ++ dns_rdata_t rdata = DNS_RDATA_INIT; ++ ++ /* 10.0.0.1 */ ++ rdata.data = data; ++ rdata.length = 4; ++ rdata.rdclass = dns_rdataclass_in; ++ rdata.type = dns_rdatatype_a; ++ ++ dns_rdatalist_init(&rdatalist); ++ rdatalist.ttl = 2; ++ rdatalist.type = dns_rdatatype_a; ++ rdatalist.rdclass = dns_rdataclass_in; ++ ISC_LIST_APPEND(rdatalist.rdata, &rdata, link); ++ ++ switch (pass) { ++ case 0: ++ /* default: stale processing off */ ++ break; ++ case 1: ++ /* turn on stale processing */ ++ result = dns_db_setservestalettl(db, 1); ++ assert_int_equal(result, ISC_R_SUCCESS); ++ break; ++ case 2: ++ /* turn off stale processing */ ++ result = dns_db_setservestalettl(db, 0); ++ assert_int_equal(result, ISC_R_SUCCESS); ++ break; ++ } ++ ++ dns_rdataset_init(&rdataset); ++ result = dns_rdatalist_tordataset(&rdatalist, &rdataset); ++ assert_int_equal(result, ISC_R_SUCCESS); ++ ++ result = dns_db_findnode(db, example, true, &node); ++ assert_int_equal(result, ISC_R_SUCCESS); ++ ++ result = dns_db_addrdataset(db, node, NULL, 0, &rdataset, 0, ++ NULL); ++ assert_int_equal(result, ISC_R_SUCCESS); ++ ++ dns_db_detachnode(db, &node); ++ dns_rdataset_disassociate(&rdataset); ++ ++ result = dns_db_find(db, example, NULL, dns_rdatatype_a, ++ 0, 0, &node, found, &rdataset, NULL); ++ assert_int_equal(result, ISC_R_SUCCESS); ++ ++ /* ++ * May loop for up to 2 seconds performing non stale lookups. ++ */ ++ count = 0; ++ do { ++ count++; ++ assert_in_range(count, 0, 20); /* loop sanity */ ++ assert_int_equal(rdataset.attributes & ++ DNS_RDATASETATTR_STALE, 0); ++ assert_true(rdataset.ttl > 0); ++ dns_db_detachnode(db, &node); ++ dns_rdataset_disassociate(&rdataset); ++ ++ usleep(100000); /* 100 ms */ ++ ++ result = dns_db_find(db, example, NULL, ++ dns_rdatatype_a, 0, 0, ++ &node, found, &rdataset, NULL); ++ } while (result == ISC_R_SUCCESS); ++ ++ assert_int_equal(result, ISC_R_NOTFOUND); ++ ++ /* ++ * Check whether we can get stale data. ++ */ ++ result = dns_db_find(db, example, NULL, dns_rdatatype_a, ++ DNS_DBFIND_STALEOK, 0, ++ &node, found, &rdataset, NULL); ++ switch (pass) { ++ case 0: ++ assert_int_equal(result, ISC_R_NOTFOUND); ++ break; ++ case 1: ++ /* ++ * Should loop for 1 second with stale lookups then ++ * stop. ++ */ ++ count = 0; ++ do { ++ count++; ++ assert_in_range(count, 0, 49); /* loop sanity */ ++ assert_int_equal(result, ISC_R_SUCCESS); ++ assert_int_equal(rdataset.ttl, 0); ++ assert_int_equal(rdataset.attributes & ++ DNS_RDATASETATTR_STALE, ++ DNS_RDATASETATTR_STALE); ++ dns_db_detachnode(db, &node); ++ dns_rdataset_disassociate(&rdataset); ++ ++ usleep(100000); /* 100 ms */ ++ ++ result = dns_db_find(db, example, NULL, ++ dns_rdatatype_a, ++ DNS_DBFIND_STALEOK, ++ 0, &node, found, ++ &rdataset, NULL); ++ } while (result == ISC_R_SUCCESS); ++ assert_in_range(count, 1, 10); ++ assert_int_equal(result, ISC_R_NOTFOUND); ++ break; ++ case 2: ++ assert_int_equal(result, ISC_R_NOTFOUND); ++ break; ++ } ++ } ++ ++ dns_db_detach(&db); ++ isc_mem_detach(&mymctx); ++} ++ + /* database class */ + static void + class_test(void **state) { +@@ -213,6 +405,8 @@ int + main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(getoriginnode_test), ++ cmocka_unit_test(getsetservestalettl_test), ++ cmocka_unit_test(dns_dbfind_staleok_test), + cmocka_unit_test_setup_teardown(class_test, + _setup, _teardown), + cmocka_unit_test_setup_teardown(dbtype_test, +diff --git a/lib/dns/view.c b/lib/dns/view.c +index a1a4301b5d..abf6a4cce9 100644 +--- a/lib/dns/view.c ++++ b/lib/dns/view.c +@@ -229,6 +229,9 @@ dns_view_create(isc_mem_t *mctx, dns_rdataclass_t rdclass, + view->flush = false; + view->dlv = NULL; + view->maxudp = 0; ++ view->staleanswerttl = 1; ++ view->staleanswersok = dns_stale_answer_conf; ++ view->staleanswersenable = false; + view->nocookieudp = 0; + view->maxbits = 0; + view->v4_aaaa = dns_aaaa_ok; +diff --git a/lib/isccfg/namedconf.c b/lib/isccfg/namedconf.c +index 7bad989be1..bbf4b45c10 100644 +--- a/lib/isccfg/namedconf.c ++++ b/lib/isccfg/namedconf.c +@@ -1778,6 +1778,7 @@ view_clauses[] = { + { "max-ncache-ttl", &cfg_type_uint32, 0 }, + { "max-recursion-depth", &cfg_type_uint32, 0 }, + { "max-recursion-queries", &cfg_type_uint32, 0 }, ++ { "max-stale-ttl", &cfg_type_ttlval, 0 }, + { "max-udp-size", &cfg_type_uint32, 0 }, + { "message-compression", &cfg_type_boolean, 0 }, + { "min-roots", &cfg_type_uint32, CFG_CLAUSEFLAG_NOTIMP }, +@@ -1806,7 +1807,9 @@ view_clauses[] = { + { "request-nsid", &cfg_type_boolean, 0 }, + { "request-sit", &cfg_type_boolean, CFG_CLAUSEFLAG_OBSOLETE }, + { "require-server-cookie", &cfg_type_boolean, 0 }, ++ { "resolver-nonbackoff-tries", &cfg_type_uint32, 0 }, + { "resolver-query-timeout", &cfg_type_uint32, 0 }, ++ { "resolver-retry-interval", &cfg_type_uint32, 0 }, + { "response-policy", &cfg_type_rpz, 0 }, + { "rfc2308-type1", &cfg_type_boolean, CFG_CLAUSEFLAG_NYI }, + { "root-delegation-only", &cfg_type_optional_exclude, 0 }, +@@ -1815,6 +1818,8 @@ view_clauses[] = { + { "send-cookie", &cfg_type_boolean, 0 }, + { "servfail-ttl", &cfg_type_ttlval, 0 }, + { "sortlist", &cfg_type_bracketed_aml, 0 }, ++ { "stale-answer-enable", &cfg_type_boolean, 0 }, ++ { "stale-answer-ttl", &cfg_type_ttlval, 0 }, + { "suppress-initial-notify", &cfg_type_boolean, CFG_CLAUSEFLAG_NYI }, + { "topology", &cfg_type_bracketed_aml, CFG_CLAUSEFLAG_NOTIMP }, + { "transfer-format", &cfg_type_transferformat, 0 }, +-- +2.20.1 + diff --git a/bind.spec b/bind.spec index df712b5..4cdd3fa 100644 --- a/bind.spec +++ b/bind.spec @@ -61,7 +61,7 @@ Summary: The Berkeley Internet Name Domain (BIND) DNS (Domain Name System) serv Name: bind License: MPLv2.0 Version: 9.11.12 -Release: 3%{?PATCHVER:.%{PATCHVER}}%{?PREVER:.%{PREVER}}%{?dist} +Release: 4%{?PATCHVER:.%{PATCHVER}}%{?PREVER:.%{PREVER}}%{?dist} Epoch: 32 Url: https://www.isc.org/downloads/bind/ # @@ -158,6 +158,7 @@ Patch173:bind-9.11-rh1732883.patch Patch174:bind-9.11-json-c.patch Patch175:bind-9.11-fips-disable.patch Patch176: bind-9.11-rh1768258.patch +Patch177: bind-9.11-serve-stale.patch # SDB patches Patch11: bind-9.3.2b2-sdbsrc.patch @@ -550,6 +551,7 @@ are used for building ISC DHCP. %patch174 -p1 -b .json-c %patch175 -p1 -b .rh1709553 %patch176 -p1 -b .rh1768258 +%patch177 -p1 -b .serve-stale mkdir lib/dns/tests/testdata/dstrandom cp -a %{SOURCE50} lib/dns/tests/testdata/dstrandom/random.data @@ -1565,6 +1567,9 @@ fi; %changelog +* Wed Nov 06 2019 Petr Menšík - 32:9.11.12-4 +- Backported serve-stale feature + * Wed Nov 06 2019 Petr Menšík - 32:9.11.12-3 - Fix wrong default GeoIP directory (#1768258)