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.
This commit is contained in:
Colin Vidal 2026-06-01 17:38:18 +02:00 committed by Ondřej Surý
parent 1fe179973e
commit d238b8eece
No known key found for this signature in database
GPG key ID: 2820F37E873DEA41
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)