System test covering SERVFAIL cache and CNAME

Add a system test for the case where resolution SERVFAILs because the
fetch context reaches the `max-query-count` threshold while following a
CNAME.

Resolving the CNAME target independently should still work, because the
SERVFAIL cache stores the original query name rather than the target.

(cherry picked from commit d238b8eece)
This commit is contained in:
Colin Vidal 2026-06-01 17:38:18 +02:00
parent 294df42043
commit a404d72d36
3 changed files with 113 additions and 0 deletions

View file

@ -0,0 +1,53 @@
# 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 dns.name
import dns.rcode
import dns.rdataclass
import dns.rdatatype
import dns.rrset
from isctest.asyncserver import AsyncDnsServer, QnameQtypeHandler, StaticResponseHandler
def rrset(
qname: dns.name.Name | str,
rtype: dns.rdatatype.RdataType,
rdata: str,
ttl: int = 300,
) -> dns.rrset.RRset:
return dns.rrset.from_text(qname, ttl, dns.rdataclass.IN, rtype, rdata)
class Tld1Handler(QnameQtypeHandler, StaticResponseHandler):
qnames = ["foo.tld1."]
qtypes = [dns.rdatatype.A]
answer = [rrset("foo.tld1.", dns.rdatatype.CNAME, "tld2.")]
class Tld2Handler(QnameQtypeHandler, StaticResponseHandler):
qnames = ["tld2."]
qtypes = [dns.rdatatype.A]
answer = [rrset("tld2.", dns.rdatatype.A, "1.2.3.4")]
def main() -> None:
server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR)
server.install_response_handlers(
Tld1Handler(),
Tld2Handler(),
)
server.run()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,26 @@
options {
query-source address @ns.ip@;
port @PORT@;
pid-file "named.pid";
listen-on { @ns.ip@; };
listen-on-v6 { none; };
recursion yes;
dnssec-validation no;
qname-minimization off;
servfail-ttl 5;
max-query-count 2;
};
{% include "_common/controls.conf.j2" %}
zone "tld1." {
type forward;
forward only;
forwarders { 10.53.0.1; };
};
zone "tld2." {
type forward;
forward only;
forwarders { 10.53.0.1; };
};

View file

@ -0,0 +1,34 @@
# 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 dns.message
import isctest
# A SERVFAIL produced while following a CNAME must be cached against the
# original query name, not the CNAME target.
#
# ans1 serves "foo.tld1 CNAME tld2" and "tld2 A 1.2.3.4"; ns2 forwards
# both zones to it with "max-query-count 2". Resolving "foo.tld1/A"
# follows the CNAME to "tld2" and then exhausts the query budget, so the
# client gets SERVFAIL. That failure must be cached under the original
# name ("foo.tld1"), so a subsequent direct query for the CNAME target
# ("tld2") is not blocked by the SERVFAIL cache and resolves normally.
def test_sfcache_cname(ns2):
msg = dns.message.make_query("foo.tld1.", "A")
res = isctest.query.udp(msg, ns2.ip)
isctest.check.servfail(res)
msg = dns.message.make_query("tld2.", "A")
res = isctest.query.udp(msg, ns2.ip)
isctest.check.noerror(res)