fix: usr: Fix slow speed of NSEC3 optout large delegation zone signing

BIND 9.20 takes much more time signing a large delegation zone with NSEC3 optout compared to version 9.18. This has been restored.

Closes #5672

Merge branch '5672-nsec3-optout-takes-too-long' into 'main'

See merge request isc-projects/bind9!11354
This commit is contained in:
Matthijs Mekking 2025-12-10 13:54:46 +00:00
commit d67dcac70e
12 changed files with 346 additions and 19 deletions

View file

@ -0,0 +1 @@
../../_common/controls.conf.in

View file

@ -0,0 +1,41 @@
/*
* 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.
*/
options {
port @PORT@;
pid-file "named.pid";
listen-on { 10.53.0.2; };
listen-on-v6 { none; };
allow-transfer { any; };
recursion no;
dnssec-validation no;
ixfr-from-differences yes;
sig-signing-nodes 900;
sig-signing-signatures 900;
};
include "controls.conf";
dnssec-policy "optout" {
keys {
csk lifetime unlimited algorithm ecdsa256;
};
nsec3param iterations 0 optout yes salt-length 0;
};
zone "test" {
type primary;
file "test.db";
dnssec-policy "optout";
inline-signing yes;
};

View file

@ -0,0 +1,22 @@
; 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.
$TTL 3600
@ IN SOA ns2.test. hostmaster.test. 1 7200 3600 24796800 3600
IN NS ns2
ns2 IN A 10.53.0.2
a IN A 127.0.0.1
$GENERATE 1-50000 child$ IN NS ns.example.
child303 IN DS 7250 13 2 A30B3F78B6DDE9A4A9A2AD0C805518B4F49EC62E7D3F4531D33DE697 CDA01CB2

View file

@ -0,0 +1,14 @@
#!/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

View file

@ -0,0 +1,108 @@
#!/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 os
import re
import sys
import isctest
import pytest
pytest.importorskip("dns", minversion="2.0.0")
import dns.exception
import dns.message
import dns.name
import dns.query
import dns.rcode
import dns.rdataclass
import dns.rdatatype
pytestmark = [
pytest.mark.skipif(
sys.version_info < (3, 7), reason="Python >= 3.7 required [GL #3001]"
),
pytest.mark.extra_artifacts(
[
"*.out",
"ns2/*.infile",
"ns2/*.signed",
"ns2/*.jnl",
"ns2/*.jbk",
"ns2/controls.conf",
"ns2/dsset-*",
"ns2/K*",
]
),
]
def has_nsec3param(zone, response):
match = rf"{re.escape(zone)}\.\s+\d+\s+IN\s+NSEC3PARAM\s+1\s+0\s+0\s+-"
for rr in response.answer:
if re.search(match, rr.to_text()):
return True
return False
def do_query(server, qname, qtype, tcp=False):
msg = isctest.query.create(qname, qtype)
query_func = isctest.query.tcp if tcp else isctest.query.udp
response = query_func(msg, server.ip, expected_rcode=dns.rcode.NOERROR)
return response
def do_xfr(server, qname):
xfr = dns.zone.Zone(origin=f"{qname}.", relativize=False)
dns.query.inbound_xfr(
where=server.ip, txn_manager=xfr, port=int(os.environ["PORT"])
)
return xfr
def verify_zone(zone, transfer):
verify = os.getenv("VERIFY")
assert verify is not None
filename = f"{zone}.out"
with open(filename, "w", encoding="utf-8") as file:
file.write(transfer.to_text())
# dnssec-verify command with default arguments.
verify_cmd = [verify, "-z", "-o", zone, filename]
verifier = isctest.run.cmd(verify_cmd)
if verifier.rc != 0:
isctest.log.error(f"dnssec-verify {zone} failed")
return verifier.rc == 0
def test_optout(ns2):
zone = "test"
# Wait until the provided zone is signed and then verify its DNSSEC data.
def check_nsec3param():
response = do_query(ns2, zone, "NSEC3PARAM")
return has_nsec3param(zone, response)
# check zone is fully signed.
isctest.run.retry_with_timeout(check_nsec3param, timeout=300)
# check if zone if DNSSEC valid.
transfer = do_xfr(ns2, zone)
assert verify_zone(zone, transfer)

View file

@ -58,6 +58,14 @@ dns__dbiterator_seek(dns_dbiterator_t *iterator,
return iterator->methods->seek(iterator, name DNS__DB_FLARG_PASS);
}
isc_result_t
dns__dbiterator_seek3(dns_dbiterator_t *iterator,
const dns_name_t *name DNS__DB_FLARG) {
REQUIRE(DNS_DBITERATOR_VALID(iterator));
return iterator->methods->seek3(iterator, name DNS__DB_FLARG_PASS);
}
isc_result_t
dns__dbiterator_prev(dns_dbiterator_t *iterator DNS__DB_FLARG) {
REQUIRE(DNS_DBITERATOR_VALID(iterator));

View file

@ -70,6 +70,8 @@ typedef struct dns_dbiteratormethods {
isc_result_t (*last)(dns_dbiterator_t *iterator DNS__DB_FLARG);
isc_result_t (*seek)(dns_dbiterator_t *iterator,
const dns_name_t *name DNS__DB_FLARG);
isc_result_t (*seek3)(dns_dbiterator_t *iterator,
const dns_name_t *name DNS__DB_FLARG);
isc_result_t (*prev)(dns_dbiterator_t *iterator DNS__DB_FLARG);
isc_result_t (*next)(dns_dbiterator_t *iterator DNS__DB_FLARG);
isc_result_t (*current)(dns_dbiterator_t *iterator,
@ -189,6 +191,30 @@ dns__dbiterator_seek(dns_dbiterator_t *iterator,
*\li Other results are possible, depending on the DB implementation.
*/
#define dns_dbiterator_seek3(iterator, name) \
dns__dbiterator_seek3(iterator, name DNS__DB_FILELINE)
isc_result_t
dns__dbiterator_seek3(dns_dbiterator_t *iterator,
const dns_name_t *name DNS__DB_FLARG);
/*%<
* Move the node cursor to the node with NSEC3 name 'name'.
* If not found, the iterator is set to the next name.
*
* Requires:
*\li 'iterator' is a valid iterator.
*
*\li 'name' is a valid name.
*
* Returns:
*\li #ISC_R_SUCCESS
*\li #ISC_R_NOTFOUND
*\li #ISC_R_NOMORE There are no NSEC3 nodes in the database.
*\li #ISC_R_NOTIMPLEMENTED
* (this function is only implemented for NSEC3 only iterators)
*
*\li Other results are possible, depending on the DB implementation.
*/
#define dns_dbiterator_prev(iterator) \
dns__dbiterator_prev(iterator DNS__DB_FILELINE)
isc_result_t

View file

@ -565,7 +565,24 @@ dns_nsec3_addnsec3(dns_db_t *db, dns_dbversion_t *version,
* Create the node if it doesn't exist and hold
* a reference to it until we have added the NSEC3.
*/
CHECK(dns_db_findnsec3node(db, hashname, true, &newnode));
result = dns_db_findnsec3node(db, hashname, false, &newnode);
if (result != ISC_R_SUCCESS) {
isc_result_t tresult;
CHECK(dns_db_createiterator(db, DNS_DB_NSEC3ONLY, &dbit));
tresult = dns_dbiterator_seek3(dbit, hashname);
CHECK(dns_dbiterator_pause(dbit));
if (tresult != ISC_R_SUCCESS) {
/* Nothing in the NSEC3 space yet. */
if (!unsecure) {
goto addnsec3;
}
goto cleanup;
}
goto find_previous;
}
/*
* Seek the iterator to the 'newnode'.
@ -610,6 +627,7 @@ dns_nsec3_addnsec3(dns_db_t *db, dns_dbversion_t *version,
}
}
find_previous:
/*
* Find the previous NSEC3 (if any) and update it if required.
*/
@ -694,6 +712,10 @@ addnsec3:
/*
* Create the NSEC3 RDATA.
*/
if (newnode == NULL) {
CHECK(dns_db_findnsec3node(db, hashname, true, &newnode));
}
CHECK(dns_db_findnode(db, name, false, &node));
CHECK(dns_nsec3_buildrdata(db, version, node, hash, flags, iterations,
salt, salt_length, nexthash, next_length,

View file

@ -374,6 +374,9 @@ static isc_result_t
dbiterator_seek(dns_dbiterator_t *iterator,
const dns_name_t *name DNS__DB_FLARG);
static isc_result_t
dbiterator_seek3(dns_dbiterator_t *iterator,
const dns_name_t *name DNS__DB_FLARG);
static isc_result_t
dbiterator_prev(dns_dbiterator_t *iterator DNS__DB_FLARG);
static isc_result_t
dbiterator_next(dns_dbiterator_t *iterator DNS__DB_FLARG);
@ -386,9 +389,10 @@ static isc_result_t
dbiterator_origin(dns_dbiterator_t *iterator, dns_name_t *name);
static dns_dbiteratormethods_t dbiterator_methods = {
dbiterator_destroy, dbiterator_first, dbiterator_last,
dbiterator_seek, dbiterator_prev, dbiterator_next,
dbiterator_current, dbiterator_pause, dbiterator_origin
dbiterator_destroy, dbiterator_first, dbiterator_last,
dbiterator_seek, dbiterator_seek3, dbiterator_prev,
dbiterator_next, dbiterator_current, dbiterator_pause,
dbiterator_origin
};
/*
@ -3634,6 +3638,12 @@ dbiterator_seek(dns_dbiterator_t *iterator,
return result;
}
static isc_result_t
dbiterator_seek3(dns_dbiterator_t *iterator ISC_ATTR_UNUSED,
const dns_name_t *name ISC_ATTR_UNUSED DNS__DB_FLARG) {
return ISC_R_NOTIMPLEMENTED;
}
static isc_result_t
dbiterator_prev(dns_dbiterator_t *iterator ISC_ATTR_UNUSED DNS__DB_FLARG) {
return ISC_R_NOTIMPLEMENTED;

View file

@ -345,6 +345,9 @@ static isc_result_t
dbiterator_seek(dns_dbiterator_t *iterator,
const dns_name_t *name DNS__DB_FLARG);
static isc_result_t
dbiterator_seek3(dns_dbiterator_t *iterator,
const dns_name_t *name DNS__DB_FLARG);
static isc_result_t
dbiterator_prev(dns_dbiterator_t *iterator DNS__DB_FLARG);
static isc_result_t
dbiterator_next(dns_dbiterator_t *iterator DNS__DB_FLARG);
@ -357,9 +360,10 @@ static isc_result_t
dbiterator_origin(dns_dbiterator_t *iterator, dns_name_t *name);
static dns_dbiteratormethods_t dbiterator_methods = {
dbiterator_destroy, dbiterator_first, dbiterator_last,
dbiterator_seek, dbiterator_prev, dbiterator_next,
dbiterator_current, dbiterator_pause, dbiterator_origin
dbiterator_destroy, dbiterator_first, dbiterator_last,
dbiterator_seek, dbiterator_seek3, dbiterator_prev,
dbiterator_next, dbiterator_current, dbiterator_pause,
dbiterator_origin
};
typedef struct qpdb_dbiterator {
@ -4357,6 +4361,53 @@ dbiterator_seek(dns_dbiterator_t *iterator,
return result;
}
static isc_result_t
dbiterator_seek3(dns_dbiterator_t *iterator,
const dns_name_t *name DNS__DB_FLARG) {
isc_result_t result;
qpdb_dbiterator_t *qpdbiter = (qpdb_dbiterator_t *)iterator;
if (qpdbiter->result != ISC_R_SUCCESS &&
qpdbiter->result != ISC_R_NOTFOUND &&
qpdbiter->result != DNS_R_PARTIALMATCH &&
qpdbiter->result != ISC_R_NOMORE)
{
return qpdbiter->result;
}
if (qpdbiter->nsec3mode != nsec3only) {
return ISC_R_NOTIMPLEMENTED;
}
dereference_iter_node(qpdbiter DNS__DB_FLARG_PASS);
result = dns_qp_lookup(qpdbiter->snap, name, DNS_DBNAMESPACE_NSEC3,
&qpdbiter->iter, NULL, (void **)&qpdbiter->node,
NULL);
switch (result) {
case ISC_R_SUCCESS:
reference_iter_node(qpdbiter DNS__DB_FLARG_PASS);
break;
case DNS_R_PARTIALMATCH:
/* dbiterator_next() will dereference the node */
reference_iter_node(qpdbiter DNS__DB_FLARG_PASS);
result = dbiterator_next(iterator);
if (result == ISC_R_NOMORE) {
result = dbiterator_first(iterator);
}
break;
case ISC_R_NOTFOUND:
default:
break;
}
qpdbiter->result = result;
return qpdbiter->result;
}
static isc_result_t
dbiterator_prev(dns_dbiterator_t *iterator DNS__DB_FLARG) {
isc_result_t result;

View file

@ -210,6 +210,9 @@ static isc_result_t
dbiterator_seek(dns_dbiterator_t *iterator,
const dns_name_t *name DNS__DB_FLARG);
static isc_result_t
dbiterator_seek3(dns_dbiterator_t *iterator,
const dns_name_t *name DNS__DB_FLARG);
static isc_result_t
dbiterator_prev(dns_dbiterator_t *iterator DNS__DB_FLARG);
static isc_result_t
dbiterator_next(dns_dbiterator_t *iterator DNS__DB_FLARG);
@ -222,9 +225,10 @@ static isc_result_t
dbiterator_origin(dns_dbiterator_t *iterator, dns_name_t *name);
static dns_dbiteratormethods_t dbiterator_methods = {
dbiterator_destroy, dbiterator_first, dbiterator_last,
dbiterator_seek, dbiterator_prev, dbiterator_next,
dbiterator_current, dbiterator_pause, dbiterator_origin
dbiterator_destroy, dbiterator_first, dbiterator_last,
dbiterator_seek, dbiterator_seek3, dbiterator_prev,
dbiterator_next, dbiterator_current, dbiterator_pause,
dbiterator_origin
};
/*
@ -1149,6 +1153,12 @@ dbiterator_seek(dns_dbiterator_t *iterator,
return ISC_R_NOTFOUND;
}
static isc_result_t
dbiterator_seek3(dns_dbiterator_t *iterator ISC_ATTR_UNUSED,
const dns_name_t *name ISC_ATTR_UNUSED DNS__DB_FLARG) {
return ISC_R_NOTIMPLEMENTED;
}
static isc_result_t
dbiterator_prev(dns_dbiterator_t *iterator DNS__DB_FLARG) {
sdlz_dbiterator_t *sdlziter = (sdlz_dbiterator_t *)iterator;

View file

@ -182,10 +182,10 @@ ISC_RUN_TEST_IMPL(reverse_nsec3) {
/* seek: walk database starting at a particular node */
static void
test_seek_node(const char *filename, int flags, int nodes) {
isc_result_t result;
test_seek_node(const char *filename, bool nsec3, int flags, int nodes) {
isc_result_t result, result3;
dns_db_t *db = NULL;
dns_dbiterator_t *iter = NULL;
dns_dbiterator_t *iter = NULL, *iter3 = NULL;
dns_dbnode_t *node = NULL;
dns_name_t *name, *seekname;
dns_fixedname_t f1, f2;
@ -200,6 +200,9 @@ test_seek_node(const char *filename, int flags, int nodes) {
result = dns_db_createiterator(db, flags, &iter);
assert_int_equal(result, ISC_R_SUCCESS);
result3 = dns_db_createiterator(db, flags, &iter3);
assert_int_equal(result3, ISC_R_SUCCESS);
result = make_name("c." TEST_ORIGIN, seekname);
assert_int_equal(result, ISC_R_SUCCESS);
@ -207,6 +210,14 @@ test_seek_node(const char *filename, int flags, int nodes) {
if (flags == DNS_DB_NSEC3ONLY) {
/* "c" isn't in the NSEC3 tree but the origin node is */
assert_int_equal(result, DNS_R_PARTIALMATCH);
/* NSEC3 iterator */
result3 = dns_dbiterator_seek3(iter3, seekname);
if (nsec3) {
assert_int_equal(result3, ISC_R_SUCCESS);
} else {
assert_int_equal(result3, ISC_R_NOMORE);
}
} else {
assert_int_equal(result, ISC_R_SUCCESS);
}
@ -244,26 +255,29 @@ test_seek_node(const char *filename, int flags, int nodes) {
assert_int_equal(i, nodes);
dns_dbiterator_destroy(&iter);
dns_dbiterator_destroy(&iter3);
dns_db_detach(&db);
}
ISC_RUN_TEST_IMPL(seek_node) {
UNUSED(state);
test_seek_node(TESTS_DIR "/testdata/dbiterator/zone1.data", 0, 9);
test_seek_node(TESTS_DIR "/testdata/dbiterator/zone1.data",
test_seek_node(TESTS_DIR "/testdata/dbiterator/zone1.data", false, 0,
9);
test_seek_node(TESTS_DIR "/testdata/dbiterator/zone1.data", false,
DNS_DB_NONSEC3, 9);
test_seek_node(TESTS_DIR "/testdata/dbiterator/zone1.data",
test_seek_node(TESTS_DIR "/testdata/dbiterator/zone1.data", false,
DNS_DB_NSEC3ONLY, 0);
}
ISC_RUN_TEST_IMPL(seek_node_nsec3) {
UNUSED(state);
test_seek_node(TESTS_DIR "/testdata/dbiterator/zone2.data", 0, 29);
test_seek_node(TESTS_DIR "/testdata/dbiterator/zone2.data",
test_seek_node(TESTS_DIR "/testdata/dbiterator/zone2.data", true, 0,
29);
test_seek_node(TESTS_DIR "/testdata/dbiterator/zone2.data", true,
DNS_DB_NONSEC3, 9);
test_seek_node(TESTS_DIR "/testdata/dbiterator/zone2.data",
test_seek_node(TESTS_DIR "/testdata/dbiterator/zone2.data", true,
DNS_DB_NSEC3ONLY, 0);
}