fix: usr: Fix nxdomain-redirect combined with dns64

When a resolver was configured with both `nxdomain-redirect` and `dns64`
in the same view, an AAAA query for a nonexistent name could abort
`named`. The combination failed whenever the redirect zone held A
records but no AAAA records.  The server now serves the empty AAAA
response from the redirect zone as-is, instead of attempting DNS64
synthesis on top of it.

Closes #5789

Merge branch '5789-fix-nxdomain-redirect-dns64-assert' into 'main'

See merge request isc-projects/bind9!12059
This commit is contained in:
Ondřej Surý 2026-05-28 11:11:57 +02:00
commit be4f6ad202
16 changed files with 254 additions and 0 deletions

View file

@ -0,0 +1,29 @@
// NS10 — validating recursor used to drive the synth-from-dnssec entry
// into query_redirect (covering NSEC from ns9 synthesizes NXDOMAIN
// locally, then nxdomain-redirect+dns64 hit the same buggy path as ns7).
options {
port @PORT@;
listen-on port @PORT@ { 10.53.0.10; };
pid-file "named.pid";
nxdomain-redirect redirect;
dnssec-validation yes;
synth-from-dnssec yes;
dns64 64:ff9b::/96 {
clients { any; };
mapped { any; };
suffix ::;
};
};
include "trusted.conf";
zone "." {
type hint;
file "root.hints";
};
zone "redirect" {
type primary;
file "redirect.db";
};

View file

@ -0,0 +1,5 @@
$TTL 300
@ IN SOA ns.redirect. admin.redirect. 1 3600 900 604800 86400
@ IN NS ns.redirect.
ns IN A 10.53.0.10
* IN A 203.0.113.1

View file

@ -0,0 +1,2 @@
. 518400 IN NS a.root-servers.nil.
a.root-servers.nil. 518400 IN A 10.53.0.9

View file

@ -0,0 +1,24 @@
// NS7
options {
port @PORT@;
listen-on port @PORT@ { 10.53.0.7; };
pid-file "named.pid";
nxdomain-redirect redirect;
dnssec-validation no;
dns64 64:ff9b::/96 {
clients { any; };
mapped { any; };
suffix ::;
};
};
zone "." {
type primary;
file "root.db";
};
zone "redirect" {
type primary;
file "redirect.db";
};

View file

@ -0,0 +1,5 @@
$TTL 300
@ IN SOA ns.redirect. admin.redirect. 1 3600 900 604800 86400
@ IN NS ns.redirect.
ns IN A 10.53.0.7
* IN A 203.0.113.1

View file

@ -0,0 +1,3 @@
. 86400 IN SOA a.root-servers.nil. hostmaster.example.net. 2019022100 1800 900 604800 86400
. 518400 IN NS a.root-servers.nil.
a.root-servers.nil. 518400 IN A 10.53.0.7

View file

@ -0,0 +1,19 @@
// NS8
options {
port @PORT@;
listen-on port @PORT@ { 10.53.0.8; };
pid-file "named.pid";
nxdomain-redirect redirect;
dnssec-validation no;
dns64 64:ff9b::/96 {
clients { any; };
mapped { any; };
suffix ::;
};
};
zone "." {
type hint;
file "root.hints";
};

View file

@ -0,0 +1,2 @@
. 518400 IN NS a.root-servers.nil.
a.root-servers.nil. 518400 IN A 10.53.0.9

View file

@ -0,0 +1,20 @@
// NS9 — signed-root upstream for ns8 (advisor cached path) and ns10
// (synth-from-dnssec path)
options {
port @PORT@;
listen-on port @PORT@ { 10.53.0.9; };
pid-file "named.pid";
dnssec-validation no;
recursion no;
};
zone "." {
type primary;
file "root.db.signed";
};
zone "redirect" {
type primary;
file "redirect.db";
};

View file

@ -0,0 +1,5 @@
$TTL 300
@ IN SOA ns.redirect. admin.redirect. 1 3600 900 604800 86400
@ IN NS ns.redirect.
ns IN A 10.53.0.9
* IN A 203.0.113.1

View file

@ -0,0 +1,3 @@
. 86400 IN SOA a.root-servers.nil. hostmaster.example.net. 2019022100 1800 900 604800 86400
. 518400 IN NS a.root-servers.nil.
a.root-servers.nil. 518400 IN A 10.53.0.9

View file

@ -0,0 +1,27 @@
#!/bin/sh -e
# 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.
. ../../conf.sh
zone=.
infile=root.db.in
zonefile=root.db
key1=$($KEYGEN -q -a $DEFAULT_ALGORITHM $zone)
key2=$($KEYGEN -q -a $DEFAULT_ALGORITHM -fk $zone)
cat $infile $key1.key $key2.key >$zonefile
$SIGNER -P -g -O full -o $zone $zonefile >sign.ns9.root.out
keyfile_to_static_keys $key2 >../ns10/trusted.conf

View file

@ -20,3 +20,4 @@ cp ns2/example.db.in ns2/example.db
cp ns4/example.db.in ns4/example.db
(cd ns3 && $SHELL sign.sh)
(cd ns5 && $SHELL sign.sh)
(cd ns9 && $SHELL sign.sh)

View file

@ -0,0 +1,102 @@
#!/usr/bin/python3
# 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 pytest
import isctest
pytestmark = pytest.mark.extra_artifacts(
[
"dig.out.*",
"ns1/K*",
"ns1/*.signed",
"ns1/dsset-nsec3.",
"ns1/dsset-signed.",
"ns1/nsec3.db",
"ns1/signed.db",
"ns2/example.db",
"ns2/named.stats",
"ns2/redirect.db",
"ns3/K*",
"ns3/*.signed",
"ns3/dsset-nsec3.",
"ns3/dsset-signed.",
"ns3/nsec3.db",
"ns3/signed.db",
"ns4/example.db",
"ns4/named.stats",
"ns5/K*",
"ns5/dsset-*",
"ns5/*.signed",
"ns5/root.db",
"ns5/sign.ns5.*",
"ns5/signed.db",
"ns6/signed.db.signed",
"ns9/K*",
"ns9/dsset-*",
"ns9/root.db",
"ns9/root.db.signed",
"ns9/sign.ns9.*",
"ns10/trusted.conf",
]
)
def _no_crash(server, qname):
# DO=0 so nxdomain-redirect is not skipped on validated upstream
# responses; the documented landing-page deployment serves
# non-DNSSEC clients.
msg = isctest.query.create(qname, "AAAA", dnssec=False)
response = isctest.query.tcp(msg, server.ip)
isctest.check.noerror(response)
def _alive(server):
msg = isctest.query.create("ns.redirect.", "A", dnssec=False)
response = isctest.query.tcp(msg, server.ip)
isctest.check.noerror(response)
def test_nxdomain_redirect_dns64_authoritative(ns7):
# Direct AAAA to a server that is authoritative for both '.' (NXDOMAIN)
# and the redirect zone (wildcard A only). Reproduces the
# INSIST(!qctx->is_zone) abort in query_notfound() entered via
# authoritative NXDOMAIN.
_no_crash(ns7, "no-exist.")
_alive(ns7)
def test_nxdomain_redirect_dns64_recursive(ns8):
# Recursive resolver: the upstream returns a real NOERROR-empty AAAA
# for '*.redirect.', which the resolver caches as NCACHENXRRSET.
# Reproduces the REQUIRE(rdataset->methods != NULL) abort in
# dns_rdataset_first() reached via the disassociated rdataset on the
# second pass through redirect2().
_no_crash(ns8, "no-exist.")
_alive(ns8)
def test_nxdomain_redirect_dns64_synth_from_dnssec(ns10):
# Validating recursor with synth-from-dnssec. Prime the NSEC chain
# with an A query (the redirect zone serves a wildcard A, so this
# path returns successfully without entering the DNS64 fallback). A
# subsequent AAAA query for a different nonexistent name is then
# synthesized via query_coveringnsec() and reaches query_redirect()
# through the third documented entry. Same downstream bug as the
# authoritative path.
msg = isctest.query.create("prime.", "A", dnssec=False)
response = isctest.query.tcp(msg, ns10.ip)
isctest.check.noerror(response)
_no_crash(ns10, "trigger.")
_alive(ns10)

View file

@ -38,6 +38,12 @@ pytestmark = pytest.mark.extra_artifacts(
"ns5/sign.ns5.*",
"ns5/signed.db",
"ns6/signed.db.signed",
"ns9/K*",
"ns9/dsset-*",
"ns9/root.db",
"ns9/root.db.signed",
"ns9/sign.ns9.*",
"ns10/trusted.conf",
]
)

View file

@ -8902,6 +8902,7 @@ query_nodata(query_ctx_t *qctx, isc_result_t res) {
#endif /* ifdef dns64_bis_return_excluded_addresses */
} else if ((result == DNS_R_NXRRSET || result == DNS_R_NCACHENXRRSET) &&
!ISC_LIST_EMPTY(qctx->view->dns64) && !qctx->nxrewrite &&
!qctx->redirected &&
qctx->client->message->rdclass == dns_rdataclass_in &&
qctx->qtype == dns_rdatatype_aaaa)
{