sleep 2
-# Check that if we don't have stale data for a domain name, we will
-# not answer anything until the resolver query timeout.
-n=$((n + 1))
-echo_i "check notincache.example TXT times out (max-stale-ttl default) ($n)"
-ret=0
-$DIG -p ${PORT} +tries=1 +timeout=2 @10.53.0.3 notfound.example TXT >dig.out.test$n 2>&1 && ret=1
-grep "timed out" dig.out.test$n >/dev/null || ret=1
-grep ";; no servers could be reached" dig.out.test$n >/dev/null || ret=1
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
+# Note: the "notincache.example TXT times out" step (the original test
+# 120) has been moved to the pytest suite in serve_stale_tcp/, since
+# the resolver now legitimately escalates to TCP after repeated UDP
+# timeouts and the perl mock ans2 only listens on UDP.
echo_i "sending queries for tests $((n + 1))-$((n + 4))..."
$DIG -p ${PORT} @10.53.0.3 data.example TXT >dig.out.test$((n + 1)) &
$DIG -p ${PORT} @10.53.0.3 othertype.example CAA >dig.out.test$((n + 2)) &
$DIG -p ${PORT} @10.53.0.3 nodata.example TXT >dig.out.test$((n + 3)) &
$DIG -p ${PORT} @10.53.0.3 nxdomain.example TXT >dig.out.test$((n + 4)) &
-$DIG -p ${PORT} @10.53.0.3 notfound.example TXT >dig.out.test$((n + 5)) &
wait
if [ $ret != 0 ]; then echo_i "failed"; fi
status=$((status + ret))
-# The notfound.example check is different than nxdomain.example because
-# we didn't send a prime query to add notfound.example to the cache.
-# Independently, EDE 22 is sent as the authoritative server doesn't respond.
-n=$((n + 1))
-echo_i "check notfound.example TXT (max-stale-ttl default) ($n)"
-ret=0
-grep "status: SERVFAIL" dig.out.test$n >/dev/null || ret=1
-grep "EDE: 22 (No Reachable Authority)" dig.out.test$n >/dev/null || ret=1
-grep "EDE: 3 (Stale Answer)" dig.out.test$n >/dev/null && ret=1
-grep "ANSWER: 0," dig.out.test$n >/dev/null || ret=1
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
+# Note: the "notfound.example TXT" SERVFAIL+EDE 22 step (the original
+# test 125) has been moved to the pytest suite in serve_stale_tcp/;
+# see the comment above where test 120 was removed.
#
# Now test server with serve-stale answers disabled.
status=$((status + ret))
if [ $ret != 0 ]; then echo_i "failed"; fi
-#############################################
-# Test for stale-answer-client-timeout off. #
-#############################################
-echo_i "test stale-answer-client-timeout (off)"
+check_server_responds() {
+ $DIG -p ${PORT} @10.53.0.3 version.bind txt ch >dig.out.test$n || return 1
+ grep "status: NOERROR" dig.out.test$n >/dev/null || return 1
+}
+
+##############################################################
+# Test for stale-answer-client-timeout off and CNAME record. #
+##############################################################
+# The standalone "stale-answer-client-timeout off" test (the original
+# test 163) has been moved to the pytest suite in serve_stale_tcp/;
+# see the comment where test 120 was removed. Its configuration
+# (named3.conf) is still used as the base for the CNAME case below.
+echo_i "test stale-answer-client-timeout (0) and CNAME record"
n=$((n + 1))
echo_i "updating ns3/named3.conf ($n)"
if [ $ret != 0 ]; then echo_i "failed"; fi
status=$((status + ret))
-# Send a query, auth server is disabled, we will enable it after a while in
-# order to receive an answer before resolver-query-timeout expires. Since
-# stale-answer-client-timeout is disabled we must receive an answer from
-# authoritative server.
-echo_i "sending query for test $((n + 2))"
-$DIG -p ${PORT} @10.53.0.3 data.example TXT >dig.out.test$((n + 2)) &
-sleep 1
-
n=$((n + 1))
echo_i "enable responses from authoritative server ($n)"
ret=0
if [ $ret != 0 ]; then echo_i "failed"; fi
status=$((status + ret))
-# Wait until dig is done.
-wait
-
-n=$((n + 1))
-echo_i "check data.example TXT comes from authoritative server (stale-answer-client-timeout off) ($n)"
-grep "status: NOERROR" dig.out.test$n >/dev/null || ret=1
-grep "EDE" dig.out.test$n >/dev/null && ret=1
-grep "ANSWER: 1," dig.out.test$n >/dev/null || ret=1
-grep "data\.example\..*[12].*IN.*TXT.*A text record with a 2 second ttl" dig.out.test$n >/dev/null || ret=1
-if [ $ret != 0 ]; then echo_i "failed"; fi
-status=$((status + ret))
-
-check_server_responds() {
- $DIG -p ${PORT} @10.53.0.3 version.bind txt ch >dig.out.test$n || return 1
- grep "status: NOERROR" dig.out.test$n >/dev/null || return 1
-}
-
-##############################################################
-# Test for stale-answer-client-timeout off and CNAME record. #
-##############################################################
-echo_i "test stale-answer-client-timeout (0) and CNAME record"
-
n=$((n + 1))
echo_i "prime cache shortttl.cname.example (stale-answer-client-timeout off) ($n)"
ret=0
--- /dev/null
+# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+#
+# SPDX-License-Identifier: MPL-2.0
+#
+# 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 https://mozilla.org/MPL/2.0/.
+#
+# See the COPYRIGHT file distributed with this work for additional
+# information regarding copyright ownership.
+
+from isctest.asyncserver import ControllableAsyncDnsServer, ToggleResponsesCommand
+
+
+def main() -> None:
+ server = ControllableAsyncDnsServer()
+ server.install_control_command(ToggleResponsesCommand())
+ server.run()
+
+
+if __name__ == "__main__":
+ main()
--- /dev/null
+; Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+;
+; SPDX-License-Identifier: MPL-2.0
+;
+; 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 https://mozilla.org/MPL/2.0/.
+;
+; See the COPYRIGHT file distributed with this work for additional
+; information regarding copyright ownership.
+
+@ 300 SOA ns.example. root.example. 1 3600 1800 604800 300
+@ 300 NS ns.example.
+ns.example. 300 A 10.53.0.3
+data 2 TXT "A text record with a 2 second ttl"
--- /dev/null
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ *
+ * 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 https://mozilla.org/MPL/2.0/.
+ *
+ * See the COPYRIGHT file distributed with this work for additional
+ * information regarding copyright ownership.
+ */
+
+key rndc_key {
+ secret "1234abcd8765";
+ algorithm @DEFAULT_HMAC@;
+};
+
+controls {
+ inet 10.53.0.1 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
+};
+
+options {
+ port @PORT@;
+ pid-file "named.pid";
+
+ listen-on { 10.53.0.1; };
+ listen-on-v6 { none; };
+ query-source address 10.53.0.1;
+ notify-source 10.53.0.1;
+ transfer-source 10.53.0.1;
+
+ recursion no;
+ dnssec-validation no;
+};
+
+zone "." {
+ type primary;
+ file "root.db";
+};
--- /dev/null
+; Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+;
+; SPDX-License-Identifier: MPL-2.0
+;
+; 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 https://mozilla.org/MPL/2.0/.
+;
+; See the COPYRIGHT file distributed with this work for additional
+; information regarding copyright ownership.
+
+. 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.3
--- /dev/null
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ *
+ * 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 https://mozilla.org/MPL/2.0/.
+ *
+ * See the COPYRIGHT file distributed with this work for additional
+ * information regarding copyright ownership.
+ */
+
+key rndc_key {
+ secret "1234abcd8765";
+ algorithm @DEFAULT_HMAC@;
+};
+
+controls {
+ inet 10.53.0.2 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
+};
+
+options {
+ port @PORT@;
+ pid-file "named.pid";
+
+ listen-on { 10.53.0.2; };
+ listen-on-v6 { none; };
+ query-source address 10.53.0.2;
+ notify-source 10.53.0.2;
+ transfer-source 10.53.0.2;
+
+ recursion yes;
+ dnssec-validation no;
+ qname-minimization off;
+
+ stale-answer-enable yes;
+ stale-cache-enable yes;
+ stale-answer-ttl 3;
+ stale-refresh-time 0;
+ max-stale-ttl 3600;
+ stale-answer-client-timeout off;
+};
+
+zone "." {
+ type hint;
+ file "../../_common/root.hint";
+};
--- /dev/null
+# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+#
+# SPDX-License-Identifier: MPL-2.0
+#
+# 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 https://mozilla.org/MPL/2.0/.
+#
+# See the COPYRIGHT file distributed with this work for additional
+# information regarding copyright ownership.
+
+import threading
+import time
+
+import dns.edns
+import dns.exception
+import dns.name
+import dns.rdataclass
+import dns.rdatatype
+import pytest
+
+import isctest
+
+pytestmark = pytest.mark.extra_artifacts(
+ [
+ "ans*/ans.run",
+ ]
+)
+
+
+def _toggle(mode: str) -> None:
+ msg = isctest.query.create(f"{mode}.send-responses._control.", "TXT", dnssec=False)
+ isctest.query.udp(msg, "10.53.0.3", attempts=1)
+
+
+def test_no_stale_data_times_out():
+ """Verify the resolver does not answer until the query timeout.
+
+ With the authoritative server unresponsive and the queried name
+ absent from the cache, dig must time out instead of receiving a
+ fast SERVFAIL (the original test 120 in serve_stale/tests.sh).
+ """
+
+ _toggle("disable")
+ msg = isctest.query.create("notincache.example.", "TXT", dnssec=False)
+ start = time.monotonic()
+ with pytest.raises(dns.exception.Timeout):
+ isctest.query.udp(msg, "10.53.0.2", timeout=3, attempts=1)
+ assert time.monotonic() - start >= 3
+
+
+def test_servfail_with_ede22():
+ """Verify SERVFAIL carries EDE 22 (and not EDE 3) when auth is unreachable.
+
+ With the authoritative server unresponsive and no cached data to
+ serve stale, the resolver must return SERVFAIL with EDE 22 (No
+ Reachable Authority) and must not attach EDE 3 (Stale Answer)
+ (the original test 125 in serve_stale/tests.sh).
+ """
+
+ _toggle("disable")
+ msg = isctest.query.create("notfound.example.", "TXT", dnssec=False)
+ res = isctest.query.udp(msg, "10.53.0.2", timeout=15, attempts=1)
+ isctest.check.servfail(res)
+ isctest.check.ede(res, dns.edns.EDECode.NO_REACHABLE_AUTHORITY)
+ assert not any(
+ opt.otype == dns.edns.OptionType.EDE
+ and opt.code == dns.edns.EDECode.STALE_ANSWER
+ for opt in res.options
+ ), "unexpected stale-answer EDE in SERVFAIL response"
+ assert len(res.answer) == 0
+
+
+def test_authoritative_answer_after_reenable():
+ """Verify the resolver waits for auth to recover instead of failing fast.
+
+ Prime the cache, let the TTL expire, disable the authoritative
+ server, issue a query, and re-enable the authoritative server
+ while the query is still in flight. The resolver must return an
+ authoritative NOERROR answer with no EDE attached, not a stale
+ answer or SERVFAIL (the original test 163 in serve_stale/tests.sh).
+ """
+
+ _toggle("enable")
+ msg = isctest.query.create("data.example.", "TXT", dnssec=False)
+ isctest.check.noerror(isctest.query.udp(msg, "10.53.0.2", timeout=5))
+
+ # allow the 2s TTL to expire
+ time.sleep(3)
+
+ _toggle("disable")
+
+ timer = threading.Timer(1.0, _toggle, args=("enable",))
+ timer.start()
+ try:
+ res = isctest.query.udp(msg, "10.53.0.2", timeout=15, attempts=1)
+ finally:
+ timer.join()
+
+ isctest.check.noerror(res)
+ isctest.check.noede(res)
+ answer = res.find_rrset(
+ res.answer,
+ dns.name.from_text("data.example."),
+ dns.rdataclass.IN,
+ dns.rdatatype.TXT,
+ )
+ assert "A text record with a 2 second ttl" in str(answer[0])